From 0cc3d1eee3bdefde87412583da4b487ff0c6ad4f Mon Sep 17 00:00:00 2001 From: James Gunn Date: Mon, 5 Feb 2024 10:33:50 +0000 Subject: [PATCH] Add admin function for editing DOB --- .../Pages/Admin/EditUserDateOfBirth.cshtml | 23 +++ .../Pages/Admin/EditUserDateOfBirth.cshtml.cs | 81 +++++++++ .../Pages/Admin/User.cshtml | 3 + .../Admin/EditUserDateOfBirthTests.cs | 169 ++++++++++++++++++ 4 files changed, 276 insertions(+) create mode 100644 dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/EditUserDateOfBirth.cshtml create mode 100644 dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/EditUserDateOfBirth.cshtml.cs create mode 100644 dotnet-authserver/tests/TeacherIdentity.AuthServer.Tests/EndpointTests/Admin/EditUserDateOfBirthTests.cs diff --git a/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/EditUserDateOfBirth.cshtml b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/EditUserDateOfBirth.cshtml new file mode 100644 index 000000000..b00e9bb3b --- /dev/null +++ b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/EditUserDateOfBirth.cshtml @@ -0,0 +1,23 @@ +@page "/admin/users/{userId}/date-of-birth" +@model TeacherIdentity.AuthServer.Pages.Admin.EditUserDateOfBirthModel +@{ + ViewBag.Title = Html.DisplayNameFor(m => m.DateOfBirth); +} + +@section BeforeContent { + +} + +
+
+
+ + + + + + + Continue +
+
+
diff --git a/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/EditUserDateOfBirth.cshtml.cs b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/EditUserDateOfBirth.cshtml.cs new file mode 100644 index 000000000..4f834968c --- /dev/null +++ b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/EditUserDateOfBirth.cshtml.cs @@ -0,0 +1,81 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using TeacherIdentity.AuthServer.Events; +using TeacherIdentity.AuthServer.Models; + +namespace TeacherIdentity.AuthServer.Pages.Admin; + +[Authorize(AuthorizationPolicies.GetAnIdentitySupport)] +public class EditUserDateOfBirthModel : PageModel +{ + private readonly TeacherIdentityServerDbContext _dbContext; + private readonly IClock _clock; + + public EditUserDateOfBirthModel(TeacherIdentityServerDbContext dbContext, IClock clock) + { + _dbContext = dbContext; + _clock = clock; + } + + [FromRoute] + public Guid UserId { get; set; } + + [BindProperty] + [Display(Name = "Change date of birth")] + [Required(ErrorMessage = "Enter a date of birth")] + public DateOnly? DateOfBirth { get; set; } + + public void OnGet() + { + } + + public async Task OnPost() + { + if (!ModelState.IsValid) + { + return this.PageWithErrors(); + } + + var user = await _dbContext.Users.Where(u => u.UserType == UserType.Default && u.UserId == UserId).SingleAsync(); + + var changes = user.DateOfBirth != DateOfBirth ? UserUpdatedEventChanges.DateOfBirth : UserUpdatedEventChanges.None; + + user.DateOfBirth = DateOfBirth!.Value; + + if (changes != UserUpdatedEventChanges.None) + { + _dbContext.AddEvent(new UserUpdatedEvent() + { + Source = UserUpdatedEventSource.SupportUi, + UpdatedByClientId = null, + UpdatedByUserId = User.GetUserId(), + CreatedUtc = _clock.UtcNow, + User = Events.User.FromModel(user), + Changes = changes + }); + + await _dbContext.SaveChangesAsync(); + + TempData.SetFlashSuccess("Date of birth changed successfully"); + } + + return RedirectToPage("User", new { UserId }); + } + + public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + var user = await _dbContext.Users.Where(u => u.UserType == UserType.Default && u.UserId == UserId).SingleOrDefaultAsync(); + + if (user is null) + { + context.Result = NotFound(); + return; + } + + await next(); + } +} diff --git a/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/User.cshtml b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/User.cshtml index c3e2823f6..973a493c1 100644 --- a/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/User.cshtml +++ b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/User.cshtml @@ -80,6 +80,9 @@ Date of birth @(Model.DateOfBirth.HasValue ? Model.DateOfBirth.Value.ToString("d MMMM yyyy") : "") + + Change + DQT record diff --git a/dotnet-authserver/tests/TeacherIdentity.AuthServer.Tests/EndpointTests/Admin/EditUserDateOfBirthTests.cs b/dotnet-authserver/tests/TeacherIdentity.AuthServer.Tests/EndpointTests/Admin/EditUserDateOfBirthTests.cs new file mode 100644 index 000000000..09dde7d9b --- /dev/null +++ b/dotnet-authserver/tests/TeacherIdentity.AuthServer.Tests/EndpointTests/Admin/EditUserDateOfBirthTests.cs @@ -0,0 +1,169 @@ +using Microsoft.EntityFrameworkCore; +using TeacherIdentity.AuthServer.Events; + +namespace TeacherIdentity.AuthServer.Tests.EndpointTests.Admin; + +public class EditUserDateOfBirthTests : TestBase +{ + public EditUserDateOfBirthTests(HostFixture hostFixture) + : base(hostFixture) + { + } + + [Fact] + public async Task Get_UnauthenticatedUser_RedirectsToSignIn() + { + var user = await TestData.CreateUser(userType: Models.UserType.Default); + + await UnauthenticatedUser_RedirectsToSignIn(HttpMethod.Get, $"/admin/users/{user.UserId}/date-of-birth"); + } + + [Fact] + public async Task Get_AuthenticatedUserDoesNotHavePermission_ReturnsForbidden() + { + var user = await TestData.CreateUser(userType: Models.UserType.Default); + + await AuthenticatedUserDoesNotHavePermission_ReturnsForbidden(HttpMethod.Get, $"/admin/users/{user.UserId}/date-of-birth"); + } + + [Fact] + public async Task Get_UserDoesNotExist_ReturnsNotFound() + { + // Arrange + var userId = Guid.NewGuid(); + var request = new HttpRequestMessage(HttpMethod.Get, $"/admin/users/{userId}/date-of-birth"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Get_UserIsNotDefaultType_ReturnsNotFound() + { + // Arrange + var user = await TestData.CreateUser(userType: Models.UserType.Staff); + var request = new HttpRequestMessage(HttpMethod.Get, $"/admin/users/{user.UserId}/date-of-birth"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Get_ValidRequest_RendersExpectedContent() + { + // Arrange + var user = await TestData.CreateUser(userType: Models.UserType.Default); + var request = new HttpRequestMessage(HttpMethod.Get, $"/admin/users/{user.UserId}/date-of-birth"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); + } + + [Fact] + public async Task Post_UnauthenticatedUser_RedirectsToSignIn() + { + var user = await TestData.CreateUser(userType: Models.UserType.Default); + + await UnauthenticatedUser_RedirectsToSignIn(HttpMethod.Post, $"/admin/users/{user.UserId}/date-of-birth"); + } + + [Fact] + public async Task Post_AuthenticatedUserDoesNotHavePermission_ReturnsForbidden() + { + var user = await TestData.CreateUser(userType: Models.UserType.Default); + + await AuthenticatedUserDoesNotHavePermission_ReturnsForbidden(HttpMethod.Post, $"/admin/users/{user.UserId}/date-of-birth"); + } + + [Fact] + public async Task Post_UserDoesNotExist_ReturnsNotFound() + { + // Arrange + var userId = Guid.NewGuid(); + var request = new HttpRequestMessage(HttpMethod.Post, $"/admin/users/{userId}/date-of-birth"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Post_UserIsNotDefaultType_ReturnsNotFound() + { + // Arrange + var user = await TestData.CreateUser(userType: Models.UserType.Staff); + var request = new HttpRequestMessage(HttpMethod.Post, $"/admin/users/{user.UserId}/date-of-birth"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Theory] + [InlineData(false, false, UserUpdatedEventChanges.None)] + [InlineData(true, true, UserUpdatedEventChanges.DateOfBirth)] + public async Task Post_ValidRequest_SetsUserNameEmitsEventAndRedirects( + bool changeDateOfBirth, + bool expectEvent, + UserUpdatedEventChanges expectedChanges) + { + // Arrange + var user = await TestData.CreateUser(userType: Models.UserType.Default); + + var newDateOfBirth = changeDateOfBirth ? user.DateOfBirth!.Value.AddDays(1) : user.DateOfBirth!.Value; + + var request = new HttpRequestMessage(HttpMethod.Post, $"/admin/users/{user.UserId}/date-of-birth") + { + Content = new FormUrlEncodedContentBuilder() + { + { "DateOfBirth.Day", newDateOfBirth.Day }, + { "DateOfBirth.Month", newDateOfBirth.Month }, + { "DateOfBirth.Year", newDateOfBirth.Year }, + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"/admin/users/{user.UserId}", response.Headers.Location?.OriginalString); + + await TestData.WithDbContext(async dbContext => + { + user = await dbContext.Users.SingleAsync(u => u.UserId == user.UserId); + Assert.Equal(newDateOfBirth, user.DateOfBirth); + }); + + if (expectEvent) + { + EventObserver.AssertEventsSaved( + e => + { + var userUpdatedEvent = Assert.IsType(e); + Assert.Equal(Clock.UtcNow, userUpdatedEvent.CreatedUtc); + Assert.Equal(UserUpdatedEventSource.SupportUi, userUpdatedEvent.Source); + Assert.Equal(expectedChanges, userUpdatedEvent.Changes); + Assert.Equal(user.UserId, userUpdatedEvent.User.UserId); + Assert.Equal(TestUsers.AdminUserWithAllRoles.UserId, userUpdatedEvent.UpdatedByUserId); + }); + } + else + { + EventObserver.AssertEventsSaved(); + } + } +}