diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/JourneyNames.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/JourneyNames.cs index e4ee0a226..63635169e 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/JourneyNames.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/JourneyNames.cs @@ -12,4 +12,5 @@ public static class JourneyNames public const string DeleteMq = nameof(DeleteMq); public const string AddAlert = nameof(AddAlert); public const string EditAlertStartDate = nameof(EditAlertStartDate); + public const string EditAlertEndDate = nameof(EditAlertEndDate); } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/AddAlert/CheckAnswers.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/AddAlert/CheckAnswers.cshtml.cs index 14a4bde8c..28310c07d 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/AddAlert/CheckAnswers.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/AddAlert/CheckAnswers.cshtml.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.RazorPages; using TeachingRecordSystem.Core.DataStore.Postgres; -using TeachingRecordSystem.Core.DataStore.Postgres.Models; using TeachingRecordSystem.Core.Services.Files; namespace TeachingRecordSystem.SupportUi.Pages.Alerts.AddAlert; @@ -48,7 +47,7 @@ public async Task OnPost() { var now = clock.UtcNow; - var alert = new Alert() + var alert = new Core.DataStore.Postgres.Models.Alert() { AlertId = Guid.NewGuid(), CreatedOn = now, diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/Alert/Index.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/Alert/Index.cshtml new file mode 100644 index 000000000..508698e19 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/Alert/Index.cshtml @@ -0,0 +1,4 @@ +@page "/alerts/{alertId}/{handler?}" +@model TeachingRecordSystem.SupportUi.Pages.Alerts.Alert.IndexModel +@{ +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/Alert/Index.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/Alert/Index.cshtml.cs new file mode 100644 index 000000000..1a9f9775c --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/Alert/Index.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace TeachingRecordSystem.SupportUi.Pages.Alerts.Alert; + +public class IndexModel : PageModel +{ + [FromRoute] + public Guid AlertId { get; set; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/AlertChangeEndDateReasonOption.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/AlertChangeEndDateReasonOption.cs new file mode 100644 index 000000000..f9bd25eda --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/AlertChangeEndDateReasonOption.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate; + +public enum AlertChangeEndDateReasonOption +{ + [Display(Name = "Incorrect end date")] + IncorrectEndDate, + [Display(Name = "Change of end date")] + ChangeOfEndDate, + [Display(Name = "Another reason")] + AnotherReason +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/CheckAnswers.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/CheckAnswers.cshtml new file mode 100644 index 000000000..ecf0da2d8 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/CheckAnswers.cshtml @@ -0,0 +1,60 @@ +@page "/alerts/{alertId}/end-date/check-answers/{handler?}" +@model TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate.CheckAnswersModel +@{ + ViewBag.Title = "Check details and confirm change"; +} + +@section BeforeContent { + Back +} + +
+
+
+ Change previous alert - @Model.PersonName +

@ViewBag.Title

+ + + + New end date + @Model.NewEndDate!.Value.ToString("d MMMM yyyy") + + Change + + + + Current end date + @Model.CurrentEndDate?.ToString("d MMMM yyyy") + + + Reason for change + + + Change + + + + Evidence + + @if (Model.UploadedEvidenceFileUrl is not null) + { + @($"{Model.EvidenceFileName} (opens in new tab)") + } + else + { + + } + + + Change + + + + +
+ Confirm change + Cancel and return to record +
+
+
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/CheckAnswers.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/CheckAnswers.cshtml.cs new file mode 100644 index 000000000..f108cbb48 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/CheckAnswers.cshtml.cs @@ -0,0 +1,121 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; +using TeachingRecordSystem.Core.DataStore.Postgres; +using TeachingRecordSystem.Core.Services.Files; + +namespace TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate; + +[Journey(JourneyNames.EditAlertEndDate), RequireJourneyInstance] +public class CheckAnswersModel( + TrsDbContext dbContext, + TrsLinkGenerator linkGenerator, + IFileService fileService, + IClock clock) : PageModel +{ + private static readonly TimeSpan _fileUrlExpiresAfter = TimeSpan.FromMinutes(15); + + public JourneyInstance? JourneyInstance { get; set; } + + [FromRoute] + public Guid AlertId { get; set; } + + [FromQuery] + public bool FromCheckAnswers { get; set; } + + public Guid PersonId { get; set; } + + public string? PersonName { get; set; } + + public DateOnly? NewEndDate { get; set; } + + public DateOnly? CurrentEndDate { get; set; } + + public string? ChangeReason { get; set; } + + public string? EvidenceFileName { get; set; } + + public string? EvidenceFileSizeDescription { get; set; } + + public string? UploadedEvidenceFileUrl { get; set; } + + public async Task OnPost() + { + var now = clock.UtcNow; + + var alert = await dbContext.Alerts + .SingleAsync(a => a.AlertId == AlertId); + + var changes = NewEndDate != alert.EndDate ? + AlertUpdatedEventChanges.EndDate : + AlertUpdatedEventChanges.None; + + if (changes != AlertUpdatedEventChanges.None) + { + var oldAlertEventModel = EventModels.Alert.FromModel(alert); + + alert.EndDate = NewEndDate; + alert.UpdatedOn = now; + + var updatedEvent = new AlertUpdatedEvent() + { + EventId = Guid.NewGuid(), + CreatedUtc = now, + RaisedBy = User.GetUserId(), + PersonId = PersonId, + Alert = EventModels.Alert.FromModel(alert), + OldAlert = oldAlertEventModel, + ChangeReason = ChangeReason, + EvidenceFile = JourneyInstance!.State.EvidenceFileId is Guid fileId ? + new EventModels.File() + { + FileId = fileId, + Name = JourneyInstance.State.EvidenceFileName! + } : + null, + Changes = changes + }; + + dbContext.AddEvent(updatedEvent); + + await dbContext.SaveChangesAsync(); + } + + await JourneyInstance!.CompleteAsync(); + TempData.SetFlashSuccess("Alert changed"); + + return Redirect(linkGenerator.PersonAlerts(PersonId)); + } + + public async Task OnPostCancel() + { + await JourneyInstance!.DeleteAsync(); + return Redirect(linkGenerator.Alert(AlertId)); + } + + public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + if (!JourneyInstance!.State.IsComplete) + { + context.Result = Redirect(linkGenerator.AlertEditEndDate(AlertId, JourneyInstance.InstanceId)); + return; + } + + var personInfo = context.HttpContext.GetCurrentPersonFeature(); + var alertInfo = context.HttpContext.GetCurrentAlertFeature(); + + PersonId = personInfo.PersonId; + PersonName = personInfo.Name; + NewEndDate = JourneyInstance!.State.EndDate; + CurrentEndDate = alertInfo.Alert.EndDate; + ChangeReason = JourneyInstance.State.ChangeReason != AlertChangeEndDateReasonOption.AnotherReason ? + JourneyInstance.State.ChangeReason!.GetDisplayName() : + JourneyInstance!.State.ChangeReasonDetail; + EvidenceFileName = JourneyInstance.State.EvidenceFileName; + UploadedEvidenceFileUrl = JourneyInstance!.State.EvidenceFileId is not null ? + await fileService.GetFileUrl(JourneyInstance!.State.EvidenceFileId!.Value, _fileUrlExpiresAfter) : + null; + + await next(); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/EditAlertEndDateState.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/EditAlertEndDateState.cs new file mode 100644 index 000000000..698663425 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/EditAlertEndDateState.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate; + +public class EditAlertEndDateState +{ + public bool Initialized { get; set; } + + public DateOnly? CurrentEndDate { get; set; } + + public DateOnly? EndDate { get; set; } + + public AlertChangeEndDateReasonOption? ChangeReason { get; set; } + + public string? ChangeReasonDetail { get; set; } + + public bool? UploadEvidence { get; set; } + + public Guid? EvidenceFileId { get; set; } + + public string? EvidenceFileName { get; set; } + + public string? EvidenceFileSizeDescription { get; set; } + + [JsonIgnore] + [MemberNotNullWhen(true, nameof(EndDate), nameof(ChangeReason), nameof(UploadEvidence), nameof(EvidenceFileId))] + public bool IsComplete => EndDate is not null && + ChangeReason.HasValue && + (ChangeReason.Value == AlertChangeEndDateReasonOption.AnotherReason ? !string.IsNullOrWhiteSpace(ChangeReasonDetail) : true) && + UploadEvidence.HasValue && + (!UploadEvidence.Value || (UploadEvidence.Value && EvidenceFileId.HasValue)); + + public void EnsureInitialized(CurrentAlertFeature alertInfo) + { + if (Initialized) + { + return; + } + + EndDate = CurrentEndDate = alertInfo.Alert.EndDate; + Initialized = true; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Index.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Index.cshtml new file mode 100644 index 000000000..730088997 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Index.cshtml @@ -0,0 +1,28 @@ +@page "/alerts/{alertId}/end-date/{handler?}" +@model TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate.IndexModel +@{ + ViewBag.Title = "Enter a new end date"; +} + +@section BeforeContent { + Back +} + +
+
+
+ Change previous alert - @Model.PersonName + + + + + + + +
+ Continue + Cancel and return to record +
+
+ +
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Index.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Index.cshtml.cs new file mode 100644 index 000000000..47131440a --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Index.cshtml.cs @@ -0,0 +1,91 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate; + +[Journey(JourneyNames.EditAlertEndDate), ActivatesJourney, RequireJourneyInstance] +public class IndexModel(TrsLinkGenerator linkGenerator, IClock clock) : PageModel +{ + public JourneyInstance? JourneyInstance { get; set; } + + [FromRoute] + public Guid AlertId { get; set; } + + [FromQuery] + public bool FromCheckAnswers { get; set; } + + public Guid PersonId { get; set; } + + public string? PersonName { get; set; } + + [BindProperty] + [Display(Name = "Enter a new end date")] + public DateOnly? EndDate { get; set; } + + public DateOnly? CurrentEndDate { get; set; } + + public DateOnly? StartDate { get; set; } + + public void OnGet() + { + EndDate = JourneyInstance!.State.EndDate; + } + + public async Task OnPost() + { + if (EndDate is null) + { + ModelState.AddModelError(nameof(EndDate), "Enter an end date"); + } + else if (EndDate > clock.Today) + { + ModelState.AddModelError(nameof(EndDate), "End date cannot be in the future"); + } + else if (EndDate <= StartDate) + { + ModelState.AddModelError(nameof(EndDate), "End date must be after the start date"); + } + else if (EndDate == CurrentEndDate) + { + ModelState.AddModelError(nameof(EndDate), "Enter a different end date"); + } + + if (!ModelState.IsValid) + { + return this.PageWithErrors(); + } + + await JourneyInstance!.UpdateStateAsync(state => state.EndDate = EndDate); + + return Redirect(FromCheckAnswers + ? linkGenerator.AlertEditEndDateCheckAnswers(AlertId, JourneyInstance.InstanceId) + : linkGenerator.AlertEditEndDateReason(AlertId, JourneyInstance!.InstanceId)); + } + + public async Task OnPostCancel() + { + await JourneyInstance!.DeleteAsync(); + return Redirect(linkGenerator.Alert(AlertId)); + } + + public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + var alertInfo = context.HttpContext.GetCurrentAlertFeature(); + if (alertInfo.Alert.EndDate is null) + { + context.Result = BadRequest(); + return; + } + + var personInfo = context.HttpContext.GetCurrentPersonFeature(); + + JourneyInstance!.State.EnsureInitialized(alertInfo); + + PersonId = personInfo.PersonId; + PersonName = personInfo.Name; + CurrentEndDate = alertInfo.Alert.EndDate; + StartDate = alertInfo.Alert.StartDate; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Reason.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Reason.cshtml new file mode 100644 index 000000000..2afa98dc8 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Reason.cshtml @@ -0,0 +1,65 @@ +@page "/alerts/{alertId}/end-date/change-reason/{handler?}" +@model TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate.ReasonModel +@{ + ViewBag.Title = "Why are you changing the end date?"; +} + +@section BeforeContent { + Back +} + +
+
+
+ Change previous alert - @Model.PersonName +

@ViewBag.Title

+ + + + + + @AlertChangeEndDateReasonOption.IncorrectEndDate.GetDisplayName() + + + @AlertChangeEndDateReasonOption.ChangeOfEndDate.GetDisplayName() + + + @AlertChangeEndDateReasonOption.AnotherReason.GetDisplayName() + + + + + + + + + + + + Yes + + @if (Model.EvidenceFileId is not null) + { + Currently uploaded file +

+ @($"{Model.EvidenceFileName} ({Model.EvidenceFileSizeDescription})") +

+ } + + Upload a file + +
+
+ + No + +
+
+ +
+ Continue + Cancel and return to record +
+
+
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Reason.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Reason.cshtml.cs new file mode 100644 index 000000000..8f28ba319 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/EndDate/Reason.cshtml.cs @@ -0,0 +1,146 @@ +using System.ComponentModel.DataAnnotations; +using Humanizer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; +using TeachingRecordSystem.Core.Services.Files; +using TeachingRecordSystem.SupportUi.Infrastructure.DataAnnotations; + +namespace TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate; + +[Journey(JourneyNames.EditAlertEndDate), RequireJourneyInstance] +public class ReasonModel(TrsLinkGenerator linkGenerator, IFileService fileService) : PageModel +{ + public const int MaxFileSizeMb = 50; + + private static readonly TimeSpan _fileUrlExpiresAfter = TimeSpan.FromMinutes(15); + + public JourneyInstance? JourneyInstance { get; set; } + + [FromRoute] + public Guid AlertId { get; set; } + + [FromQuery] + public bool FromCheckAnswers { get; set; } + + public Guid PersonId { get; set; } + + public string? PersonName { get; set; } + + [BindProperty] + [Display(Name = "Select a reason")] + [Required(ErrorMessage = "Select a reason")] + public AlertChangeEndDateReasonOption? ChangeReason { get; set; } + + [BindProperty] + [Display(Name = "Enter details")] + public string? ChangeReasonDetail { get; set; } + + [BindProperty] + [Display(Name = "Upload evidence")] + [Required(ErrorMessage = "Select yes if you want to upload evidence")] + public bool? UploadEvidence { get; set; } + + [BindProperty] + [EvidenceFile] + [FileSize(MaxFileSizeMb * 1024 * 1024, ErrorMessage = "The selected file must be smaller than 50MB")] + public IFormFile? EvidenceFile { get; set; } + + public Guid? EvidenceFileId { get; set; } + + public string? EvidenceFileName { get; set; } + + public string? EvidenceFileSizeDescription { get; set; } + + public string? UploadedEvidenceFileUrl { get; set; } + + public async Task OnGet() + { + ChangeReason = JourneyInstance!.State.ChangeReason; + ChangeReasonDetail = JourneyInstance?.State.ChangeReasonDetail; + UploadEvidence = JourneyInstance?.State.UploadEvidence; + EvidenceFileId = JourneyInstance!.State.EvidenceFileId; + EvidenceFileName = JourneyInstance!.State.EvidenceFileName; + EvidenceFileSizeDescription = JourneyInstance!.State.EvidenceFileSizeDescription; + UploadedEvidenceFileUrl = JourneyInstance?.State.EvidenceFileId is not null ? + await fileService.GetFileUrl(JourneyInstance.State.EvidenceFileId.Value, _fileUrlExpiresAfter) : + null; + } + + public async Task OnPost() + { + if (ChangeReason == AlertChangeEndDateReasonOption.AnotherReason && string.IsNullOrWhiteSpace(ChangeReasonDetail)) + { + ModelState.AddModelError(nameof(ChangeReasonDetail), "Enter details"); + } + + if (UploadEvidence == true && EvidenceFileId is null && EvidenceFile is null) + { + ModelState.AddModelError(nameof(EvidenceFile), "Select a file"); + } + + if (!ModelState.IsValid) + { + return this.PageWithErrors(); + } + + if (UploadEvidence == true) + { + if (EvidenceFile is not null) + { + if (EvidenceFileId is not null) + { + await fileService.DeleteFile(EvidenceFileId.Value); + } + + using var stream = EvidenceFile.OpenReadStream(); + var evidenceFileId = await fileService.UploadFile(stream, EvidenceFile.ContentType); + await JourneyInstance!.UpdateStateAsync(state => + { + state.EvidenceFileId = evidenceFileId; + state.EvidenceFileName = EvidenceFile.FileName; + state.EvidenceFileSizeDescription = EvidenceFile.Length.Bytes().Humanize(); + }); + } + } + else if (EvidenceFileId is not null) + { + await fileService.DeleteFile(EvidenceFileId.Value); + await JourneyInstance!.UpdateStateAsync(state => + { + state.EvidenceFileId = null; + state.EvidenceFileName = null; + state.EvidenceFileSizeDescription = null; + }); + } + + await JourneyInstance!.UpdateStateAsync(state => + { + state.ChangeReason = ChangeReason; + state.ChangeReasonDetail = ChangeReasonDetail; + state.UploadEvidence = UploadEvidence; + }); + + return Redirect(linkGenerator.AlertEditEndDateCheckAnswers(AlertId, JourneyInstance!.InstanceId)); + } + + public async Task OnPostCancel() + { + await JourneyInstance!.DeleteAsync(); + return Redirect(linkGenerator.Alert(AlertId)); + } + + public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + if (JourneyInstance!.State.EndDate is null) + { + context.Result = Redirect(linkGenerator.AlertEditEndDate(AlertId, JourneyInstance.InstanceId)); + return; + } + + var personInfo = context.HttpContext.GetCurrentPersonFeature(); + + PersonId = personInfo.PersonId; + PersonName = personInfo.Name; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/StartDate/Index.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/StartDate/Index.cshtml.cs index 93f1a5c60..39705e922 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/StartDate/Index.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Alerts/EditAlert/StartDate/Index.cshtml.cs @@ -21,7 +21,7 @@ public class IndexModel(TrsLinkGenerator linkGenerator, IClock clock) : PageMode public string? PersonName { get; set; } [BindProperty] - [Display(Name = "Start date")] + [Display(Name = "Enter a new start date")] public DateOnly? StartDate { get; set; } public DateOnly? CurrentStartDate { get; set; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Program.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Program.cs index ab2d0c38a..a51161b96 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Program.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Program.cs @@ -245,6 +245,12 @@ requestDataKeys: ["alertId"], appendUniqueKey: true)); + options.JourneyRegistry.RegisterJourney(new JourneyDescriptor( + JourneyNames.EditAlertEndDate, + typeof(TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate.EditAlertEndDateState), + requestDataKeys: ["alertId"], + appendUniqueKey: true)); + options.JourneyRegistry.RegisterJourney(new JourneyDescriptor( JourneyNames.AddMq, typeof(TeachingRecordSystem.SupportUi.Pages.Mqs.AddMq.AddMqState), diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs index 5255be9ee..f21570dba 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TrsLinkGenerator.cs @@ -73,6 +73,24 @@ public string AlertEditStartDateCheckAnswers(Guid alertId, JourneyInstanceId jou public string AlertEditStartDateCheckAnswersCancel(Guid alertId, JourneyInstanceId journeyInstanceId) => GetRequiredPathByPage("/Alerts/EditAlert/StartDate/CheckAnswers", "cancel", routeValues: new { alertId }, journeyInstanceId: journeyInstanceId); + public string AlertEditEndDate(Guid alertId, JourneyInstanceId? journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Alerts/EditAlert/EndDate/Index", routeValues: new { alertId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string AlertEditEndDateCancel(Guid alertId, JourneyInstanceId journeyInstanceId) => + GetRequiredPathByPage("/Alerts/EditAlert/EndDate/Index", "cancel", routeValues: new { alertId }, journeyInstanceId: journeyInstanceId); + + public string AlertEditEndDateReason(Guid alertId, JourneyInstanceId journeyInstanceId, bool? fromCheckAnswers = null) => + GetRequiredPathByPage("/Alerts/EditAlert/EndDate/Reason", routeValues: new { alertId, fromCheckAnswers }, journeyInstanceId: journeyInstanceId); + + public string AlertEditEndDateReasonCancel(Guid alertId, JourneyInstanceId journeyInstanceId) => + GetRequiredPathByPage("/Alerts/EditAlert/EndDate/Reason", "cancel", routeValues: new { alertId }, journeyInstanceId: journeyInstanceId); + + public string AlertEditEndDateCheckAnswers(Guid alertId, JourneyInstanceId journeyInstanceId) => + GetRequiredPathByPage("/Alerts/EditAlert/EndDate/CheckAnswers", routeValues: new { alertId }, journeyInstanceId: journeyInstanceId); + + public string AlertEditEndDateCheckAnswersCancel(Guid alertId, JourneyInstanceId journeyInstanceId) => + GetRequiredPathByPage("/Alerts/EditAlert/EndDate/CheckAnswers", "cancel", routeValues: new { alertId }, journeyInstanceId: journeyInstanceId); + public string AlertClose(Guid alertId, JourneyInstanceId? journeyInstanceId) => GetRequiredPathByPage("/Alerts/CloseAlert/Index", routeValues: new { alertId }, journeyInstanceId: journeyInstanceId); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/AlertTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/AlertTests.cs index fc564519f..115de4eaf 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/AlertTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/AlertTests.cs @@ -24,7 +24,7 @@ public async Task AddAlert() await page.AssertOnAddAlertTypePage(); - await page.CheckAsync($"text={alertType.Name}"); + await page.CheckAsync($"label:text-is('{alertType.Name}')"); await page.ClickContinueButton(); @@ -50,7 +50,7 @@ public async Task AddAlert() await page.FillAsync("label:text-is('Why are you adding this alert?')", reason); - await page.CheckAsync("text=Yes"); + await page.CheckAsync("label:text-is('Yes')"); await page .GetByLabel("Upload a file") .SetInputFilesAsync( @@ -100,7 +100,7 @@ public async Task EditAlertStartDate() await page.CheckAsync("text=Another reason"); await page.FillAsync("label:text-is('Enter details')", changeReason); - await page.CheckAsync("text=Yes"); + await page.CheckAsync("label:text-is('Yes')"); await page .GetByLabel("Upload a file") .SetInputFilesAsync( @@ -121,4 +121,55 @@ await page await page.AssertFlashMessage("Alert changed"); } + + [Fact] + public async Task EditAlertEndDate() + { + var startDate = TestData.Clock.Today.AddDays(-50); + var endDate = TestData.Clock.Today.AddDays(-10); + var person = await TestData.CreatePerson(b => b.WithAlert(a => a.WithStartDate(startDate).WithEndDate(endDate))); + var personId = person.PersonId; + var alertId = person.Alerts.First().AlertId; + var newEndDate = TestData.Clock.Today.AddDays(-5); + var changeReason = TestData.GenerateLoremIpsum(); + var evidenceFileName = "evidence.jpg"; + var evidenceFileMimeType = "image/jpeg"; + + await using var context = await HostFixture.CreateBrowserContext(); + var page = await context.NewPageAsync(); + + await page.GoToEditAlertEndDatePage(alertId); + + await page.AssertOnEditAlertEndDatePage(alertId); + + await page.FillDateInput(newEndDate); + + await page.ClickContinueButton(); + + await page.AssertOnEditAlertEndDateChangeReasonPage(alertId); + + await page.CheckAsync("label:text-is('Another reason')"); + await page.FillAsync("label:text-is('Enter details')", changeReason); + + await page.CheckAsync("label:text-is('Yes')"); + await page + .GetByLabel("Upload a file") + .SetInputFilesAsync( + new FilePayload() + { + Name = evidenceFileName, + MimeType = evidenceFileMimeType, + Buffer = TestData.JpegImage + }); + + await page.ClickContinueButton(); + + await page.AssertOnEditAlertEndDateCheckAnswersPage(alertId); + + await page.ClickConfirmChangeButton(); + + await page.AssertOnPersonAlertsPage(personId); + + await page.AssertFlashMessage("Alert changed"); + } } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/PageExtensions.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/PageExtensions.cs index 6cccff141..54b13fad8 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/PageExtensions.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/PageExtensions.cs @@ -41,6 +41,11 @@ public static async Task GoToEditAlertStartDatePage(this IPage page, Guid alertI await page.GotoAsync($"/alerts/{alertId}/start-date"); } + public static async Task GoToEditAlertEndDatePage(this IPage page, Guid alertId) + { + await page.GotoAsync($"/alerts/{alertId}/end-date"); + } + public static async Task GoToAddMqPage(this IPage page, Guid personId) { await page.GotoAsync($"/mqs/add?personId={personId}"); @@ -177,6 +182,22 @@ public static async Task AssertOnEditAlertStartDateCheckAnswersPage(this IPage p await page.WaitForUrlPathAsync($"/alerts/{alertId}/start-date/check-answers"); } + public static async Task AssertOnEditAlertEndDatePage(this IPage page, Guid alertId) + { + await page.WaitForUrlPathAsync($"/alerts/{alertId}/end-date"); + } + + public static async Task AssertOnEditAlertEndDateChangeReasonPage(this IPage page, Guid alertId) + { + await page.WaitForUrlPathAsync($"/alerts/{alertId}/end-date/change-reason"); + } + + public static async Task AssertOnEditAlertEndDateCheckAnswersPage(this IPage page, Guid alertId) + { + await page.WaitForUrlPathAsync($"/alerts/{alertId}/end-date/check-answers"); + } + + public static async Task AssertOnAlertDetailPage(this IPage page, Guid alertId) { await page.WaitForUrlPathAsync($"/alerts/{alertId}"); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Alerts/EditAlert/EndDate/CheckAnswersTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Alerts/EditAlert/EndDate/CheckAnswersTests.cs new file mode 100644 index 000000000..0c2d9d00c --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Alerts/EditAlert/EndDate/CheckAnswersTests.cs @@ -0,0 +1,262 @@ +using TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate; + +namespace TeachingRecordSystem.SupportUi.Tests.PageTests.Alerts.EditAlert.EndDate; + +public class CheckAnswersTests(HostFixture hostFixture) : TestBase(hostFixture) +{ + [Fact] + public async Task Get_WithAlertIdForNonExistentAlert_ReturnsNotFound() + { + // Arrange + var person = await TestData.CreatePerson(); + var alertId = Guid.NewGuid(); + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Get_MissingDataInJourneyState_RedirectsToIndexPage() + { + // Arrange + var person = await TestData.CreatePerson(b => b.WithAlert()); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/alerts/{alertId}/end-date", response.Headers.Location?.OriginalString); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Get_WithValidJourneyState_ReturnsOk(bool populateOptional) + { + // Arrange + var startDate = TestData.Clock.Today.AddDays(-50); + var databaseEndDate = TestData.Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var changeReason = populateOptional ? AlertChangeEndDateReasonOption.AnotherReason : AlertChangeEndDateReasonOption.IncorrectEndDate; + var changeReasonDetail = populateOptional ? "Some details" : null; + var evidenceFileId = Guid.NewGuid(); + var evidenceFileName = "test.pdf"; + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate, + ChangeReason = changeReason, + ChangeReasonDetail = changeReasonDetail, + UploadEvidence = populateOptional ? true : false, + EvidenceFileId = populateOptional ? evidenceFileId : null, + EvidenceFileName = populateOptional ? evidenceFileName : null + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + var doc = await AssertEx.HtmlResponse(response); + Assert.Equal(journeyEndDate.ToString("d MMMM yyyy"), doc.GetElementByTestId("new-end-date")!.TextContent); + Assert.Equal(databaseEndDate.ToString("d MMMM yyyy"), doc.GetElementByTestId("current-end-date")!.TextContent); + if (changeReason == AlertChangeEndDateReasonOption.AnotherReason) + { + Assert.Equal(changeReasonDetail, doc.GetElementByTestId("change-reason")!.TextContent); + } + else + { + Assert.Equal(changeReason.GetDisplayName(), doc.GetElementByTestId("change-reason")!.TextContent); + } + Assert.Equal(populateOptional ? $"{evidenceFileName} (opens in new tab)" : "-", doc.GetElementByTestId("uploaded-evidence-link")!.TextContent); + } + + [Fact] + public async Task Post_WithAlertIdForNonExistentAlert_ReturnsNotFound() + { + // Arrange + var person = await TestData.CreatePerson(); + var alertId = Guid.NewGuid(); + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Post_MissingDataInJourneyState_RedirectsToIndexPage() + { + // Arrange + var person = await TestData.CreatePerson(b => b.WithAlert()); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/alerts/{alertId}/end-date", response.Headers.Location?.OriginalString); + } + + [Theory] + [InlineData(AlertChangeEndDateReasonOption.IncorrectEndDate)] + [InlineData(AlertChangeEndDateReasonOption.ChangeOfEndDate)] + [InlineData(AlertChangeEndDateReasonOption.AnotherReason)] + public async Task Post_Confirm_UpdatesAlertCreatesEventCompletesJourneyAndRedirectsWithFlashMessage(AlertChangeEndDateReasonOption changeReason) + { + // Arrange + var startDate = TestData.Clock.Today.AddDays(-50); + var databaseEndDate = TestData.Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var changeReasonDetail = "Some reason or other"; + var evidenceFileId = Guid.NewGuid(); + var evidenceFileName = "test.pdf"; + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var originalAlert = person.Alerts.Single(); + var alertId = originalAlert.AlertId; + + EventPublisher.Clear(); + + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate, + ChangeReason = changeReason, + ChangeReasonDetail = changeReasonDetail, + UploadEvidence = true, + EvidenceFileId = evidenceFileId, + EvidenceFileName = evidenceFileName + }); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + + var redirectResponse = await response.FollowRedirect(HttpClient); + var redirectDoc = await redirectResponse.GetDocument(); + AssertEx.HtmlDocumentHasFlashSuccess(redirectDoc, "Alert changed"); + + await WithDbContext(async dbContext => + { + var updatedAlert = await dbContext.Alerts.FirstOrDefaultAsync(a => a.AlertId == alertId); + Assert.Equal(journeyEndDate, updatedAlert!.EndDate); + }); + + EventPublisher.AssertEventsSaved(e => + { + var expectedAlertUpdatedEvent = new AlertUpdatedEvent() + { + EventId = Guid.Empty, + CreatedUtc = Clock.UtcNow, + RaisedBy = GetCurrentUserId(), + PersonId = person.PersonId, + Alert = new() + { + AlertId = alertId, + AlertTypeId = originalAlert.AlertTypeId, + Details = originalAlert.Details, + ExternalLink = originalAlert.ExternalLink, + StartDate = originalAlert.StartDate, + EndDate = journeyEndDate + }, + OldAlert = new() + { + AlertId = alertId, + AlertTypeId = originalAlert.AlertTypeId, + Details = originalAlert.Details, + ExternalLink = originalAlert.ExternalLink, + StartDate = originalAlert.StartDate, + EndDate = databaseEndDate + }, + ChangeReason = changeReason == AlertChangeEndDateReasonOption.AnotherReason ? changeReasonDetail : changeReason.GetDisplayName(), + EvidenceFile = new() + { + FileId = evidenceFileId, + Name = evidenceFileName + }, + Changes = AlertUpdatedEventChanges.EndDate + }; + + var actualAlertUpdatedEvent = Assert.IsType(e); + Assert.Equivalent(expectedAlertUpdatedEvent with { EventId = actualAlertUpdatedEvent.EventId }, actualAlertUpdatedEvent); + }); + + journeyInstance = await ReloadJourneyInstance(journeyInstance); + Assert.True(journeyInstance.Completed); + } + + [Fact] + public async Task Post_Cancel_DeletesJourneyAndRedirects() + { + // Arrange + var startDate = TestData.Clock.Today.AddDays(-50); + var databaseEndDate = TestData.Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var changeReason = AlertChangeEndDateReasonOption.IncorrectEndDate; + var changeReasonDetail = "Some reason or other"; + var evidenceFileId = Guid.NewGuid(); + var evidenceFileName = "test.pdf"; + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate, + ChangeReason = changeReason, + ChangeReasonDetail = changeReasonDetail, + UploadEvidence = true, + EvidenceFileId = evidenceFileId, + EvidenceFileName = evidenceFileName + }); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/check-answers/cancel?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/alerts/{alertId}", response.Headers.Location?.OriginalString); + + journeyInstance = await ReloadJourneyInstance(journeyInstance); + Assert.Null(journeyInstance); + } + + private async Task> CreateJourneyInstance(Guid alertId, EditAlertEndDateState? state = null) => + await CreateJourneyInstance( + JourneyNames.EditAlertEndDate, + state ?? new EditAlertEndDateState(), + new KeyValuePair("alertId", alertId)); +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Alerts/EditAlert/EndDate/IndexTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Alerts/EditAlert/EndDate/IndexTests.cs new file mode 100644 index 000000000..13eff7010 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Alerts/EditAlert/EndDate/IndexTests.cs @@ -0,0 +1,282 @@ +using TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate; + +namespace TeachingRecordSystem.SupportUi.Tests.PageTests.Alerts.EditAlert.EndDate; + +public class IndexTests(HostFixture hostFixture) : TestBase(hostFixture) +{ + [Fact] + public async Task Get_WithAlertIdForNonExistentAlert_ReturnsNotFound() + { + // Arrange + var person = await TestData.CreatePerson(); + var alertId = Guid.NewGuid(); + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Get_WhenAlertHasNoEndDateSet_ReturnsBadRequest() + { + // Arrange + var startDate = new DateOnly(2021, 10, 5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Get_ValidRequestWithUninitializedJourneyState_PopulatesModelFromDatabase() + { + // Arrange + var startDate = Clock.Today.AddDays(-50); + var databaseEndDate = Clock.Today.AddDays(-10); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + var doc = await AssertEx.HtmlResponse(response); + Assert.Equal($"{databaseEndDate:%d}", doc.GetElementById("EndDate.Day")?.GetAttribute("value")); + Assert.Equal($"{databaseEndDate:%M}", doc.GetElementById("EndDate.Month")?.GetAttribute("value")); + Assert.Equal($"{databaseEndDate:yyyy}", doc.GetElementById("EndDate.Year")?.GetAttribute("value")); + } + + [Fact] + public async Task Get_ValidRequestWithInitializedJourneyState_PopulatesModelFromJourneyState() + { + // Arrange + var startDate = Clock.Today.AddDays(-50); + var databaseEndDate = Clock.Today.AddDays(-10); + var journeyEndDate = Clock.Today.AddDays(-5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + var doc = await AssertEx.HtmlResponse(response); + Assert.Equal($"{journeyEndDate:%d}", doc.GetElementById("EndDate.Day")?.GetAttribute("value")); + Assert.Equal($"{journeyEndDate:%M}", doc.GetElementById("EndDate.Month")?.GetAttribute("value")); + Assert.Equal($"{journeyEndDate:yyyy}", doc.GetElementById("EndDate.Year")?.GetAttribute("value")); + } + + [Fact] + public async Task Post_WithAlertIdForNonExistentAlert_ReturnsNotFound() + { + // Arrange + var person = await TestData.CreatePerson(); + var alertId = Guid.NewGuid(); + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Post_WhenAlertHasNoEndDateSet_ReturnsBadRequest() + { + // Arrange + var startDate = Clock.Today.AddDays(-50); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Post_WhenNoEndDateIsEntered_ReturnsError() + { + var startDate = Clock.Today.AddDays(-50); + var databaseEndDate = Clock.Today.AddDays(-10); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "EndDate", "Enter an end date"); + } + + [Fact] + public async Task Post_WhenEndDateIsInTheFuture_ReturnsError() + { + var startDate = Clock.Today.AddDays(-50); + var databaseEndDate = Clock.Today.AddDays(-10); + var futureDate = Clock.Today.AddDays(2); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder() + { + { "EndDate.Day", $"{futureDate:%d}" }, + { "EndDate.Month", $"{futureDate:%M}" }, + { "EndDate.Year", $"{futureDate:yyyy}" }, + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "EndDate", "End date cannot be in the future"); + } + + [Fact] + public async Task Post_WhenEndDateIsBeforeStartDate_ReturnsError() + { + var startDate = Clock.Today.AddDays(-50); + var databaseEndDate = Clock.Today.AddDays(-10); + var newEndDate = Clock.Today.AddDays(-51); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder() + { + { "EndDate.Day", $"{newEndDate:%d}" }, + { "EndDate.Month", $"{newEndDate:%M}" }, + { "EndDate.Year", $"{newEndDate:yyyy}" }, + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "EndDate", "End date must be after the start date"); + } + + [Fact] + public async Task Post_WhenEndDateIsUnchanged_ReturnsError() + { + var startDate = Clock.Today.AddDays(-50); + var databaseEndDate = Clock.Today.AddDays(-10); + var newEndDate = databaseEndDate; + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder() + { + { "EndDate.Day", $"{newEndDate:%d}" }, + { "EndDate.Month", $"{newEndDate:%M}" }, + { "EndDate.Year", $"{newEndDate:yyyy}" }, + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "EndDate", "Enter a different end date"); + } + + [Fact] + public async Task Post_WhenEndDateIsEntered_RedirectsToChangeReasonPage() + { + var startDate = Clock.Today.AddDays(-50); + var databaseEndDate = Clock.Today.AddDays(-10); + var newEndDate = Clock.Today.AddDays(-5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date?personId={person.PersonId}&{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder() + { + { "EndDate.Day", $"{newEndDate:%d}" }, + { "EndDate.Month", $"{newEndDate:%M}" }, + { "EndDate.Year", $"{newEndDate:yyyy}" }, + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/alerts/{alertId}/end-date/change-reason", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Post_Cancel_DeletesJourneyAndRedirects() + { + var startDate = Clock.Today.AddDays(-50); + var databaseEndDate = Clock.Today.AddDays(-10); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/cancel?personId={person.PersonId}&{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/alerts/{alertId}", response.Headers.Location?.OriginalString); + + journeyInstance = await ReloadJourneyInstance(journeyInstance); + Assert.Null(journeyInstance); + } + + private async Task> CreateJourneyInstance(Guid alertId, EditAlertEndDateState? state = null) => + await CreateJourneyInstance( + JourneyNames.EditAlertEndDate, + state ?? new EditAlertEndDateState(), + new KeyValuePair("alertId", alertId)); +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Alerts/EditAlert/EndDate/ReasonTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Alerts/EditAlert/EndDate/ReasonTests.cs new file mode 100644 index 000000000..ff3420d6d --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/Alerts/EditAlert/EndDate/ReasonTests.cs @@ -0,0 +1,318 @@ +using TeachingRecordSystem.SupportUi.Pages.Alerts.EditAlert.EndDate; + +namespace TeachingRecordSystem.SupportUi.Tests.PageTests.Alerts.EditAlert.EndDate; + +public class ReasonTests(HostFixture hostFixture) : TestBase(hostFixture) +{ + [Fact] + public async Task Get_WithAlertIdForNonExistentAlert_ReturnsNotFound() + { + // Arrange + var person = await TestData.CreatePerson(); + var alertId = Guid.NewGuid(); + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Get_MissingDataInJourneyState_RedirectsToIndexPage() + { + // Arrange + var person = await TestData.CreatePerson(b => b.WithAlert()); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Get_ValidRequestWithPopulatedDataInJourneyState_ReturnsOK() + { + // Arrange + var startDate = TestData.Clock.Today.AddDays(-50); + var databaseEndDate = TestData.Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); + } + + [Fact] + public async Task Post_WithAlertIdForNonExistentAlert_ReturnsNotFound() + { + // Arrange + var person = await TestData.CreatePerson(); + var alertId = Guid.NewGuid(); + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Post_MissingDataInJourneyState_RedirectsToIndexPage() + { + // Arrange + var person = await TestData.CreatePerson(b => b.WithAlert()); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance(alertId); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/alerts/{alertId}/end-date?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Post_WhenNoChangeReasonIsSelected_ReturnsError() + { + // Arrange + var startDate = TestData.Clock.Today.AddDays(-50); + var databaseEndDate = TestData.Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate + }); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContent(new Dictionary + { + ["UploadEvidence"] = "False" + }) + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "ChangeReason", "Select a reason"); + } + + [Fact] + public async Task Post_WhenChangeReasonAnotherReasonIsSelectedAndDetailsAreEmpty_ReturnsError() + { + // Arrange + var startDate = TestData.Clock.Today.AddDays(-50); + var databaseEndDate = TestData.Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate + }); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContent(new Dictionary + { + ["ChangeReason"] = "AnotherReason", + ["ChangeReasonDetail"] = "", + ["UploadEvidence"] = "False" + }) + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "ChangeReasonDetail", "Enter details"); + } + + [Fact] + public async Task Post_WhenUploadEvidenceOptionIsYesAndNoFileIsSelected_ReturnsError() + { + // Arrange + var startDate = TestData.Clock.Today.AddDays(-50); + var databaseEndDate = TestData.Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate + }); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContent(new Dictionary + { + ["ChangeReason"] = "AnotherReason", + ["ChangeReasonDetail"] = "Some details", + ["UploadEvidence"] = "True" + }) + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "EvidenceFile", "Select a file"); + } + + [Fact] + public async Task Post_WhenEvidenceFileIsInvalidType_ReturnsError() + { + // Arrange + var startDate = TestData.Clock.Today.AddDays(-50); + var databaseEndDate = TestData.Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate + }); + + var multipartContent = CreateFormFileUpload(".cs"); + multipartContent.Add(new StringContent(AlertChangeEndDateReasonOption.AnotherReason.ToString()), "ChangeReason"); + multipartContent.Add(new StringContent("My change reason detail"), "ChangeReasonDetail"); + multipartContent.Add(new StringContent("True"), "UploadEvidence"); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = multipartContent + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "EvidenceFile", "The selected file must be a BMP, CSV, DOC, DOCX, EML, JPEG, JPG, MBOX, MSG, ODS, ODT, PDF, PNG, TIF, TXT, XLS or XLSX"); + } + + [Fact] + public async Task Post_WhenValidInput_RedirectsToCheckAnswersPage() + { + // Arrange + var startDate = TestData.Clock.Today.AddDays(-50); + var databaseEndDate = TestData.Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate + }); + + var multipartContent = CreateFormFileUpload(".pdf"); + multipartContent.Add(new StringContent(AlertChangeEndDateReasonOption.AnotherReason.ToString()), "ChangeReason"); + multipartContent.Add(new StringContent("My change reason detail"), "ChangeReasonDetail"); + multipartContent.Add(new StringContent("True"), "UploadEvidence"); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/change-reason?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = multipartContent + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/alerts/{alertId}/end-date/check-answers?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Post_Cancel_DeletesJourneyAndRedirects() + { + var startDate = Clock.Today.AddDays(-50); + var databaseEndDate = Clock.Today.AddDays(-10); + var journeyEndDate = TestData.Clock.Today.AddDays(-5); + var person = await TestData.CreatePerson(b => b.WithAlert(q => q.WithStartDate(startDate).WithEndDate(databaseEndDate))); + var alertId = person.Alerts.Single().AlertId; + var journeyInstance = await CreateJourneyInstance( + alertId, + new EditAlertEndDateState() + { + Initialized = true, + EndDate = journeyEndDate + }); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/alerts/{alertId}/end-date/cancel?personId={person.PersonId}&{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/alerts/{alertId}", response.Headers.Location?.OriginalString); + + journeyInstance = await ReloadJourneyInstance(journeyInstance); + Assert.Null(journeyInstance); + } + + private MultipartFormDataContent CreateFormFileUpload(string fileExtension) + { + var byteArrayContent = new ByteArrayContent(new byte[] { }); + byteArrayContent.Headers.Add("Content-Type", "application/octet-stream"); + + var multipartContent = new MultipartFormDataContent + { + { byteArrayContent, "EvidenceFile", $"evidence{fileExtension}" } + }; + + return multipartContent; + } + private async Task> CreateJourneyInstance(Guid alertId, EditAlertEndDateState? state = null) => + await CreateJourneyInstance( + JourneyNames.EditAlertEndDate, + state ?? new EditAlertEndDateState(), + new KeyValuePair("alertId", alertId)); +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/TeachingRecordSystem.SupportUi.Tests.csproj b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/TeachingRecordSystem.SupportUi.Tests.csproj index e6342a87f..20de7d567 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/TeachingRecordSystem.SupportUi.Tests.csproj +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/TeachingRecordSystem.SupportUi.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0