diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/AuthorizeAccessLinkGenerator.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/AuthorizeAccessLinkGenerator.cs index 33e4931fd..c23b467bf 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/AuthorizeAccessLinkGenerator.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/AuthorizeAccessLinkGenerator.cs @@ -17,6 +17,9 @@ public string NationalInsuranceNumberContinueWithout(JourneyInstanceId journeyIn public string Trn(JourneyInstanceId journeyInstanceId) => GetRequiredPathByPage("/Trn", journeyInstanceId: journeyInstanceId); + public string NotFound(JourneyInstanceId journeyInstanceId) => + GetRequiredPathByPage("/NotFound", journeyInstanceId: journeyInstanceId); + protected virtual string GetRequiredPathByPage(string page, string? handler = null, object? routeValues = null, JourneyInstanceId? journeyInstanceId = null) { var url = GetRequiredPathByPage(page, handler, routeValues); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NationalInsuranceNumber.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NationalInsuranceNumber.cshtml index 17185d7d2..60cd11172 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NationalInsuranceNumber.cshtml +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NationalInsuranceNumber.cshtml @@ -6,7 +6,7 @@
-
+ @@ -14,7 +14,7 @@ @* Pressing the Enter key will submit the first submit button - make sure it's the default action *@ - + I cannot provide my National Insurance number

You can find a lost National Insurance number (opens in new tab).

diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NationalInsuranceNumber.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NationalInsuranceNumber.cshtml.cs index dd2b92a77..be7036b1d 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NationalInsuranceNumber.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NationalInsuranceNumber.cshtml.cs @@ -16,6 +16,8 @@ public class NationalInsuranceNumberModel(SignInJourneyHelper helper, AuthorizeA [Required(ErrorMessage = "Enter a National Insurance number")] public string? NationalInsuranceNumber { get; set; } + public bool PreviouslyAnsweredCannotProvide { get; set; } + public void OnGet() { NationalInsuranceNumber = JourneyInstance!.State.NationalInsuranceNumber; @@ -46,8 +48,10 @@ public async Task OnPost() { return helper.GetNextPage(JourneyInstance).ToActionResult(); } - - return RedirectToNextPage(); + else + { + return RedirectToNextPage(); + } } public async Task OnPostContinueWithout() @@ -75,6 +79,8 @@ public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) // Already matched to a Teaching Record context.Result = Redirect(helper.GetSafeRedirectUri(JourneyInstance)); } + + PreviouslyAnsweredCannotProvide = state.NationalInsuranceNumberSpecified && state.NationalInsuranceNumber is null; } private IActionResult RedirectToNextPage() => Redirect(linkGenerator.Trn(JourneyInstance!.InstanceId)); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml new file mode 100644 index 000000000..ac799266f --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml @@ -0,0 +1,11 @@ +@page "/not-found" +@model TeachingRecordSystem.AuthorizeAccess.Pages.NotFoundModel +@{ + ViewBag.Title = "[PLACEHOLDER] Not found"; +} + +@section BeforeContent { + +} + +

@ViewBag.Title

diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml.cs new file mode 100644 index 000000000..a276234f1 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/NotFound.cshtml.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; +using TeachingRecordSystem.FormFlow; + +namespace TeachingRecordSystem.AuthorizeAccess.Pages; + +[Journey(SignInJourneyState.JourneyName), RequireJourneyInstance] +public class NotFoundModel(SignInJourneyHelper helper) : PageModel +{ + public JourneyInstance? JourneyInstance { get; set; } + + public void OnGet() + { + } + + public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + var state = JourneyInstance!.State; + + if (state.OneLoginAuthenticationTicket is null || !state.IdentityVerified) + { + // Not authenticated/verified with One Login + context.Result = BadRequest(); + } + else if (!state.NationalInsuranceNumberSpecified || !state.TrnSpecified) + { + // Not specified NINO or TRN + context.Result = helper.GetNextPage(JourneyInstance).ToActionResult(); + } + else if (state.AuthenticationTicket is not null) + { + // Already matched to a Teaching Record + context.Result = Redirect(helper.GetSafeRedirectUri(JourneyInstance)); + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Shared/_Layout.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Shared/_Layout.cshtml index eb1f3c2b4..11d3ed5cb 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Shared/_Layout.cshtml +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Shared/_Layout.cshtml @@ -2,4 +2,8 @@ Layout = "_GovUkPageTemplate"; } +@section BeforeContent { + @RenderSection("BeforeContent", required: false) +} + @RenderBody() diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Trn.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Trn.cshtml index 6c22a0823..21afc243c 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Trn.cshtml +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Trn.cshtml @@ -1,4 +1,29 @@ @page "/trn" @model TeachingRecordSystem.AuthorizeAccess.Pages.TrnModel @{ + ViewBag.Title = "What is your teacher reference number (TRN)?"; } + +@section BeforeContent { + +} + +
+
+ +

@ViewBag.Title

+ +

A TRN is 7 digits long, for example 4567814

+

It might include the letters RP and a slash, for example RP99/12345

+

It’s previously been known as a QTS, GTC, DfE, DfES and DCSF number

+ + + + Continue + +
+
diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Trn.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Trn.cshtml.cs index 510b5e251..a3e8c51eb 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Trn.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Pages/Trn.cshtml.cs @@ -1,12 +1,63 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.RazorPages; using TeachingRecordSystem.FormFlow; namespace TeachingRecordSystem.AuthorizeAccess.Pages; [Journey(SignInJourneyState.JourneyName), RequireJourneyInstance] -public class TrnModel : PageModel +public class TrnModel(SignInJourneyHelper helper, AuthorizeAccessLinkGenerator linkGenerator) : PageModel { + public JourneyInstance? JourneyInstance { get; set; } + + [BindProperty] + [Display(Name = "Teacher reference number (TRN)")] + [Required(ErrorMessage = "Enter your TRN")] + [RegularExpression(@"\A\D*(\d{1}\D*){7}\D*\Z", ErrorMessage = "Your TRN should contain 7 digits")] + public string? Trn { get; set; } + public void OnGet() { + Trn = JourneyInstance!.State.Trn; + } + + public async Task OnPost() + { + if (!ModelState.IsValid) + { + return this.PageWithErrors(); + } + + await JourneyInstance!.UpdateStateAsync(state => + { + state.TrnSpecified = true; + state.Trn = Trn; + }); + + if (await helper.TryMatchToTeachingRecord(JourneyInstance!)) + { + return helper.GetNextPage(JourneyInstance).ToActionResult(); + } + else + { + return Redirect(linkGenerator.NotFound(JourneyInstance.InstanceId)); + } + } + + public override void OnPageHandlerExecuting(PageHandlerExecutingContext context) + { + var state = JourneyInstance!.State; + + if (state.OneLoginAuthenticationTicket is null || !state.IdentityVerified) + { + // Not authenticated/verified with One Login + context.Result = BadRequest(); + } + else if (state.AuthenticationTicket is not null) + { + // Already matched to a Teaching Record + context.Result = Redirect(helper.GetSafeRedirectUri(JourneyInstance)); + } } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs index 94bf76b17..fbf3605ec 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/SignInJourneyHelper.cs @@ -103,14 +103,8 @@ public async Task CreateOrUpdateOneLoginUser(JourneyInstance new NextPageInfo(GetSafeRedirectUri(journeyInstance)), - // Authenticated with OneLogin, identity verification succeeded, person lookup failed - // TODO - - // Authenticated with OneLogin, identity verification succeeded, TRN not yet specified - // TODO - - // Authenticated with OneLogin, identity verification succeeded, NINO not yet specified - { OneLoginAuthenticationTicket: not null } => new NextPageInfo(linkGenerator.NationalInsuranceNumber(journeyInstance.InstanceId)), + // Authenticated with OneLogin, identity verification succeeded, not yet matched to teaching record + { OneLoginAuthenticationTicket: not null, AuthenticationTicket: null } => new NextPageInfo(linkGenerator.NationalInsuranceNumber(journeyInstance.InstanceId)), // Authenticated with OneLogin, identity verification failed // TODO diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NationalInsuranceNumberTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NationalInsuranceNumberTests.cs index c1f29dc99..643903a18 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NationalInsuranceNumberTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NationalInsuranceNumberTests.cs @@ -110,9 +110,9 @@ public async Task Post_NotAuthenticatedWithOneLogin_ReturnsBadRequest() var request = new HttpRequestMessage(HttpMethod.Post, $"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}") { Content = new FormUrlEncodedContentBuilder - { - { "NationalInsuranceNumber", nationalInsuranceNumber } - } + { + { "NationalInsuranceNumber", nationalInsuranceNumber } + } }; // Act @@ -158,7 +158,7 @@ public async Task Post_AlreadyAuthenticated_RedirectsToStateRedirectUri() var journeyInstance = await CreateJourneyInstance(state); var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); - var nationalInsuranceNumber = Faker.Identification.UkNationalInsuranceNumber(); + var nationalInsuranceNumber = person.NationalInsuranceNumber!; var oneLoginUser = await TestData.CreateOneLoginUser(personId: person.PersonId, firstName: person.FirstName, lastName: person.LastName, dateOfBirth: person.DateOfBirth); var ticket = CreateOneLoginAuthenticationTicket(oneLoginUser); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotFoundTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotFoundTests.cs new file mode 100644 index 000000000..f39d95f08 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/NotFoundTests.cs @@ -0,0 +1,142 @@ +namespace TeachingRecordSystem.AuthorizeAccess.Tests.PageTests; + +public class NotFoundTests(HostFixture hostFixture) : TestBase(hostFixture) +{ + [Fact] + public async Task Get_NotAuthenticatedWithOneLogin_ReturnsBadRequest() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/not-found?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Get_NotVerifiedWithOneLogin_ReturnsBadRequest() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var ticket = CreateOneLoginAuthenticationTicket(createCoreIdentityVc: false); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/not-found?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Get_AlreadyAuthenticated_RedirectsToStateRedirectUri() + { + // Arrange + var redirectUri = "/"; + var state = new SignInJourneyState(redirectUri, authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var person = await TestData.CreatePerson(b => b.WithTrn()); + var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); + + var ticket = CreateOneLoginAuthenticationTicket(oneLoginUser); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/not-found?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"{redirectUri}?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Get_NationalInsuranceNumberNotSpecified_RedirectsToStartOfMatchingQuestions() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var ticket = CreateOneLoginAuthenticationTicket(); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + await journeyInstance.UpdateStateAsync(state => + { + state.NationalInsuranceNumberSpecified = false; + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/not-found?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Get_TrnNotSpecified_RedirectsToStartOfMatchingQuestions() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var ticket = CreateOneLoginAuthenticationTicket(); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + await journeyInstance.UpdateStateAsync(state => + { + state.NationalInsuranceNumberSpecified = true; + state.TrnSpecified = false; + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/not-found?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"/national-insurance-number?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Get_ValidRequest_RendersExpectedContent() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var ticket = CreateOneLoginAuthenticationTicket(); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + await journeyInstance.UpdateStateAsync(async state => + { + state.NationalInsuranceNumberSpecified = true; + state.Trn = await TestData.GenerateTrn(); + state.TrnSpecified = true; + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/not-found?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); + + var doc = await response.GetDocument(); + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/TrnTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/TrnTests.cs new file mode 100644 index 000000000..5b00fd590 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.AuthorizeAccess.Tests/PageTests/TrnTests.cs @@ -0,0 +1,327 @@ +using Microsoft.EntityFrameworkCore; +using TeachingRecordSystem.TestCommon; + +namespace TeachingRecordSystem.AuthorizeAccess.Tests.PageTests; + +public class TrnTests(HostFixture hostFixture) : TestBase(hostFixture) +{ + [Fact] + public async Task Get_NotAuthenticatedWithOneLogin_ReturnsBadRequest() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Get_NotVerifiedWithOneLogin_ReturnsBadRequest() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var ticket = CreateOneLoginAuthenticationTicket(createCoreIdentityVc: false); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Get_AlreadyAuthenticated_RedirectsToStateRedirectUri() + { + // Arrange + var redirectUri = "/"; + var state = new SignInJourneyState(redirectUri, authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var person = await TestData.CreatePerson(b => b.WithTrn()); + var oneLoginUser = await TestData.CreateOneLoginUser(person.PersonId); + + var ticket = CreateOneLoginAuthenticationTicket(oneLoginUser); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"{redirectUri}?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Get_ValidRequest_RendersExpectedContent(bool haveExistingValueInState) + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var ticket = CreateOneLoginAuthenticationTicket(); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + var existingTrn = haveExistingValueInState ? await TestData.GenerateTrn() : null; + + await journeyInstance.UpdateStateAsync(state => + { + state.NationalInsuranceNumberSpecified = true; + + if (existingTrn is not null) + { + state.Trn = existingTrn; + state.TrnSpecified = true; + } + }); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status200OK, (int)response.StatusCode); + + var doc = await response.GetDocument(); + Assert.Equal(existingTrn ?? "", doc.GetElementById("Trn")?.GetAttribute("value")); + } + + [Fact] + public async Task Post_NotAuthenticatedWithOneLogin_ReturnsBadRequest() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + await journeyInstance.UpdateStateAsync(state => state.NationalInsuranceNumberSpecified = true); + + var trn = await TestData.GenerateTrn(); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder + { + { "Trn", trn } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Post_NotVerifiedWithOneLogin_ReturnsBadRequest() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var trn = await TestData.GenerateTrn(); + + var ticket = CreateOneLoginAuthenticationTicket(createCoreIdentityVc: false); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder + { + { "Trn", trn } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Post_AlreadyAuthenticated_RedirectsToStateRedirectUri() + { + // Arrange + var redirectUri = "/"; + var state = new SignInJourneyState(redirectUri, authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); + var trn = person.Trn!; + var oneLoginUser = await TestData.CreateOneLoginUser(personId: person.PersonId, firstName: person.FirstName, lastName: person.LastName, dateOfBirth: person.DateOfBirth); + + var ticket = CreateOneLoginAuthenticationTicket(oneLoginUser); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder + { + { "Trn", trn } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"{redirectUri}?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task Post_EmptyTrn_RendersError() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); + var trn = ""; + var oneLoginUser = await TestData.CreateOneLoginUser(personId: null, firstName: person.FirstName, lastName: person.LastName, dateOfBirth: person.DateOfBirth); + + var ticket = CreateOneLoginAuthenticationTicket(oneLoginUser); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + await journeyInstance.UpdateStateAsync(state => state.NationalInsuranceNumberSpecified = true); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder + { + { "Trn", trn } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "Trn", "Enter your TRN"); + } + + [Fact] + public async Task Post_InvalidTrn_RendersError() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); + var trn = "xxx"; + var oneLoginUser = await TestData.CreateOneLoginUser(personId: null, firstName: person.FirstName, lastName: person.LastName, dateOfBirth: person.DateOfBirth); + + var ticket = CreateOneLoginAuthenticationTicket(oneLoginUser); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + await journeyInstance.UpdateStateAsync(state => state.NationalInsuranceNumberSpecified = true); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder + { + { "Trn", trn } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "Trn", "Your TRN should contain 7 digits"); + } + + [Fact] + public async Task Post_ValidTrnButLookupFailed_UpdatesStateAndRedirectsToNotFoundPage() + { + // Arrange + var state = new SignInJourneyState(redirectUri: "/", authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); + var trn = await TestData.GenerateTrn(); + var oneLoginUser = await TestData.CreateOneLoginUser(personId: null, firstName: person.FirstName, lastName: person.LastName, dateOfBirth: person.DateOfBirth); + + var ticket = CreateOneLoginAuthenticationTicket(oneLoginUser); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + await journeyInstance.UpdateStateAsync(state => state.NationalInsuranceNumberSpecified = true); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder + { + { "Trn", trn } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"/not-found?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + + Assert.Equal(trn, state.Trn); + Assert.True(state.TrnSpecified); + Assert.Null(state.AuthenticationTicket); + } + + [Fact] + public async Task Post_ValidTrnAndLookupSucceeded_UpdatesStateUpdatesOneLoginUserCompletesAuthenticationAndRedirectsToStateRedirectUri() + { + // Arrange + var redirectUri = "/"; + var state = new SignInJourneyState(redirectUri, authenticationProperties: null); + var journeyInstance = await CreateJourneyInstance(state); + + var person = await TestData.CreatePerson(b => b.WithTrn().WithNationalInsuranceNumber()); + var trn = person.Trn!; + var oneLoginUser = await TestData.CreateOneLoginUser(personId: null, firstName: person.FirstName, lastName: person.LastName, dateOfBirth: person.DateOfBirth); + + var ticket = CreateOneLoginAuthenticationTicket(oneLoginUser); + await GetSignInJourneyHelper().OnSignedInWithOneLogin(journeyInstance, ticket); + + await journeyInstance.UpdateStateAsync(state => state.NationalInsuranceNumberSpecified = true); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/trn?{journeyInstance.GetUniqueIdQueryParameter()}") + { + Content = new FormUrlEncodedContentBuilder + { + { "Trn", trn } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.Equal($"{redirectUri}?{journeyInstance.GetUniqueIdQueryParameter()}", response.Headers.Location?.OriginalString); + + Assert.Equal(trn, state.Trn); + Assert.True(state.TrnSpecified); + Assert.NotNull(state.AuthenticationTicket); + + oneLoginUser = await WithDbContext(dbContext => dbContext.OneLoginUsers.SingleAsync(u => u.Subject == oneLoginUser.Subject)); + Assert.Equal(Clock.UtcNow, oneLoginUser.FirstSignIn); + Assert.Equal(Clock.UtcNow, oneLoginUser.LastSignIn); + Assert.Equal(person.PersonId, oneLoginUser.PersonId); + } +}