From d23243a70720bc8870609c33e4ea20423bdf1632 Mon Sep 17 00:00:00 2001 From: Sam C <156680559+sam-c-dfe@users.noreply.github.com> Date: Wed, 10 Jul 2024 12:30:51 +0100 Subject: [PATCH 01/15] Fix/EYQB-320: Default headings (#236) * Updated the default wording * Moved the default headings out into Contentful. Added new e2e test to validate it works --- .../Entities/QualificationListPage.cs | 4 ++++ .../Content/MockContentfulService.cs | 4 +++- .../Controllers/QualificationDetailsController.cs | 8 +++++--- .../Models/Content/FilterModel.cs | 4 ++-- .../e2e/pages/qualification-list-spec.cy.js | 14 ++++++++++++-- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Dfe.EarlyYearsQualification.Content/Entities/QualificationListPage.cs b/src/Dfe.EarlyYearsQualification.Content/Entities/QualificationListPage.cs index 66500adc..01f79d72 100644 --- a/src/Dfe.EarlyYearsQualification.Content/Entities/QualificationListPage.cs +++ b/src/Dfe.EarlyYearsQualification.Content/Entities/QualificationListPage.cs @@ -25,4 +25,8 @@ public class QualificationListPage public string SearchCriteriaHeading { get; init; } = string.Empty; public Document? PostSearchCriteriaContent { get; init; } + + public string AnyLevelHeading { get; init; } = string.Empty; + + public string AnyAwardingOrganisationHeading { get; init; } = string.Empty; } \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Mock/Content/MockContentfulService.cs b/src/Dfe.EarlyYearsQualification.Mock/Content/MockContentfulService.cs index 8d43a093..1b3ca99c 100644 --- a/src/Dfe.EarlyYearsQualification.Mock/Content/MockContentfulService.cs +++ b/src/Dfe.EarlyYearsQualification.Mock/Content/MockContentfulService.cs @@ -221,7 +221,9 @@ public Task> GetQualifications() SingleQualificationFoundText = "qualification found", PreSearchBoxContent = ContentfulContentHelper.Text("Pre search box content"), PostQualificationListContent = ContentfulContentHelper.Text("Post qualification list content"), - PostSearchCriteriaContent = ContentfulContentHelper.Text("Post search criteria content") + PostSearchCriteriaContent = ContentfulContentHelper.Text("Post search criteria content"), + AnyLevelHeading = "Any level", + AnyAwardingOrganisationHeading = "Various awarding organisations" }); } diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs index 6e4bc97f..f3d3942d 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs @@ -88,7 +88,7 @@ private async Task MapList(QualificationListPage content { var basicQualificationsModels = GetBasicQualificationsModels(qualifications); - var filterModel = GetFilterModel(); + var filterModel = GetFilterModel(content); return new QualificationListModel { @@ -109,11 +109,13 @@ private async Task MapList(QualificationListPage content }; } - private FilterModel GetFilterModel() + private FilterModel GetFilterModel(QualificationListPage content) { var filterModel = new FilterModel { - Country = userJourneyCookieService.GetWhereWasQualificationAwarded()! + Country = userJourneyCookieService.GetWhereWasQualificationAwarded()!, + Level = content.AnyLevelHeading, + AwardingOrganisation = content.AnyAwardingOrganisationHeading }; var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationAwarded(); diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/Content/FilterModel.cs b/src/Dfe.EarlyYearsQualification.Web/Models/Content/FilterModel.cs index 1e6b3e4b..43bc4fb6 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Models/Content/FilterModel.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Models/Content/FilterModel.cs @@ -6,7 +6,7 @@ public class FilterModel public string StartDate { get; set; } = string.Empty; - public string Level { get; set; } = "Any level"; + public string Level { get; set; } = string.Empty; - public string AwardingOrganisation { get; set; } = "Any organisation"; + public string AwardingOrganisation { get; set; } = string.Empty; } \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/qualification-list-spec.cy.js b/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/qualification-list-spec.cy.js index faa2f7fd..6c38970d 100644 --- a/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/qualification-list-spec.cy.js +++ b/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/qualification-list-spec.cy.js @@ -2,12 +2,13 @@ describe("A spec used to test the qualification list page", () => { beforeEach(() => { cy.setCookie('auth-secret', Cypress.env('auth_secret')); - // Value is '{"WhereWasQualificationAwarded":"england","WhenWasQualificationAwarded":"6/2022","LevelOfQualification":"3","WhatIsTheAwardingOrganisation":"NCFE"}' encoded - cy.setCookie('user_journey', '%7B%22WhereWasQualificationAwarded%22%3A%22england%22%2C%22WhenWasQualificationAwarded%22%3A%226%2F2022%22%2C%22LevelOfQualification%22%3A%223%22%2C%22WhatIsTheAwardingOrganisation%22%3A%22NCFE%22%7D'); }) // Mock details found in Dfe.EarlyYearsQualification.Mock.Content.MockContentfulService. it("Checks the details are showing on the page", () => { + // Value is '{"WhereWasQualificationAwarded":"england","WhenWasQualificationAwarded":"6/2022","LevelOfQualification":"3","WhatIsTheAwardingOrganisation":"NCFE"}' encoded + cy.setCookie('user_journey', '%7B%22WhereWasQualificationAwarded%22%3A%22england%22%2C%22WhenWasQualificationAwarded%22%3A%226%2F2022%22%2C%22LevelOfQualification%22%3A%223%22%2C%22WhatIsTheAwardingOrganisation%22%3A%22NCFE%22%7D'); + cy.visit("/qualifications"); cy.get("#your-search-header").should("contain.text", "Your search"); @@ -26,4 +27,13 @@ describe("A spec used to test the qualification list page", () => { cy.get(".level").first().should("contain.text", "Level"); cy.get(".awarding-org").first().should("contain.text", "Awarding organisation"); }) + + it("Shows the default headings when any level and no awarding organisation selected", () => { + // Value is '{"WhereWasQualificationAwarded":"england","WhenWasQualificationAwarded":"6/2022","LevelOfQualification":"0","WhatIsTheAwardingOrganisation":""}' encoded + cy.setCookie('user_journey', '%7B%22WhereWasQualificationAwarded%22%3A%22england%22%2C%22WhenWasQualificationAwarded%22%3A%226%2F2022%22%2C%22LevelOfQualification%22%3A%220%22%2C%22WhatIsTheAwardingOrganisation%22%3A%22%22%7D'); + cy.visit("/qualifications"); + + cy.get("#filter-level").should("contain.text", "Any level"); + cy.get("#filter-org").should("contain.text", "Various awarding organisations"); + }) }) \ No newline at end of file From 6a3d5b9b8a2a5e308537ef8b3ac3fc3fed9a46ec Mon Sep 17 00:00:00 2001 From: RobertGHippo Date: Wed, 10 Jul 2024 12:36:07 +0100 Subject: [PATCH 02/15] Some more unit test for ContentfulContentService. --- .../Services/ContentfulContentServiceTests.cs | 311 +++++++++++++++--- 1 file changed, 259 insertions(+), 52 deletions(-) diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/ContentfulContentServiceTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/ContentfulContentServiceTests.cs index 1b434330..7adf771c 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/ContentfulContentServiceTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/ContentfulContentServiceTests.cs @@ -36,7 +36,7 @@ public void BeforeEachTest() } [TestMethod] - public void GetStartPage_PageFound_ReturnsExpectedResult() + public async Task GetStartPage_PageFound_ReturnsExpectedResult() { var startPage = new StartPage { CtaButtonText = "CtaButton" }; @@ -51,14 +51,14 @@ public void GetStartPage_PageFound_ReturnsExpectedResult() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetStartPage().Result; + var result = await service.GetStartPage(); result.Should().NotBeNull(); result.Should().BeSameAs(startPage); } [TestMethod] - public void GetStartPage_NoContent_ReturnsNull() + public async Task GetStartPage_NoContent_ReturnsNull() { var pages = new ContentfulCollection { Items = new List() }; // NB: If "pages.Items" is ever null, the iterator built into ContentfulCollection will throw an exception @@ -72,7 +72,7 @@ public void GetStartPage_NoContent_ReturnsNull() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetStartPage().Result; + var result = await service.GetStartPage(); _logger.VerifyWarning("No start page entry returned"); @@ -80,7 +80,7 @@ public void GetStartPage_NoContent_ReturnsNull() } [TestMethod] - public void GetStartPage_NullPages_ReturnsNull() + public async Task GetStartPage_NullPages_ReturnsNull() { _clientMock.Setup(client => client.GetEntriesByType( @@ -91,7 +91,7 @@ public void GetStartPage_NullPages_ReturnsNull() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetStartPage().Result; + var result = await service.GetStartPage(); _logger.VerifyWarning("No start page entry returned"); @@ -99,7 +99,7 @@ public void GetStartPage_NullPages_ReturnsNull() } [TestMethod] - public void GetAccessibilityStatementPage_NoContent_ReturnsNull() + public async Task GetAccessibilityStatementPage_NoContent_ReturnsNull() { var pages = new ContentfulCollection { Items = new List() }; @@ -113,7 +113,7 @@ public void GetAccessibilityStatementPage_NoContent_ReturnsNull() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetAccessibilityStatementPage().Result; + var result = await service.GetAccessibilityStatementPage(); _logger.VerifyWarning("No accessibility statement page entry returned"); @@ -121,7 +121,7 @@ public void GetAccessibilityStatementPage_NoContent_ReturnsNull() } [TestMethod] - public void GetAccessibilityStatementPage_NullPages_ReturnsNull() + public async Task GetAccessibilityStatementPage_NullPages_ReturnsNull() { _clientMock.Setup(client => client.GetEntriesByType( @@ -132,7 +132,7 @@ public void GetAccessibilityStatementPage_NullPages_ReturnsNull() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetAccessibilityStatementPage().Result; + var result = await service.GetAccessibilityStatementPage(); _logger.VerifyWarning("No accessibility statement page entry returned"); @@ -140,7 +140,7 @@ public void GetAccessibilityStatementPage_NullPages_ReturnsNull() } [TestMethod] - public void GetAccessibilityStatementPage_PageFound_ReturnsExpectedResult() + public async Task GetAccessibilityStatementPage_PageFound_ReturnsExpectedResult() { var accessibilityStatementPage = new AccessibilityStatementPage { Heading = "Heading" }; @@ -156,14 +156,14 @@ public void GetAccessibilityStatementPage_PageFound_ReturnsExpectedResult() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetAccessibilityStatementPage().Result; + var result = await service.GetAccessibilityStatementPage(); result.Should().NotBeNull(); result.Should().BeSameAs(accessibilityStatementPage); } [TestMethod] - public void GetCookiesPage_NoContent_ReturnsNull() + public async Task GetCookiesPage_NoContent_ReturnsNull() { var pages = new ContentfulCollection { Items = new List() }; @@ -176,7 +176,7 @@ public void GetCookiesPage_NoContent_ReturnsNull() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetCookiesPage().Result; + var result = await service.GetCookiesPage(); _logger.VerifyWarning("No cookies page entry returned"); @@ -184,7 +184,7 @@ public void GetCookiesPage_NoContent_ReturnsNull() } [TestMethod] - public void GetCookiesPage_NullPages_ReturnsNull() + public async Task GetCookiesPage_NullPages_ReturnsNull() { _clientMock.Setup(client => client.GetEntriesByType( @@ -195,7 +195,7 @@ public void GetCookiesPage_NullPages_ReturnsNull() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetCookiesPage().Result; + var result = await service.GetCookiesPage(); _logger.VerifyWarning("No cookies page entry returned"); @@ -203,7 +203,7 @@ public void GetCookiesPage_NullPages_ReturnsNull() } [TestMethod] - public void GetCookiesPage_PageFound_ReturnsExpectedResult() + public async Task GetCookiesPage_PageFound_ReturnsExpectedResult() { var cookiesPage = new CookiesPage { @@ -224,14 +224,14 @@ public void GetCookiesPage_PageFound_ReturnsExpectedResult() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetCookiesPage().Result; + var result = await service.GetCookiesPage(); result.Should().NotBeNull(); result.Should().BeSameAs(cookiesPage); } [TestMethod] - public void GetNavigationLinks_NoContent_LogsWarningAndReturns() + public async Task GetNavigationLinks_NoContent_LogsWarningAndReturns() { _clientMock.Setup(client => client.GetEntriesByType( @@ -242,7 +242,7 @@ public void GetNavigationLinks_NoContent_LogsWarningAndReturns() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetNavigationLinks().Result; + var result = await service.GetNavigationLinks(); _logger.VerifyWarning("No navigation links returned"); @@ -250,7 +250,7 @@ public void GetNavigationLinks_NoContent_LogsWarningAndReturns() } [TestMethod] - public void GetNavigationLinks_Null_LogsWarningAndReturns() + public async Task GetNavigationLinks_Null_LogsWarningAndReturns() { _clientMock.Setup(client => client.GetEntriesByType( @@ -261,7 +261,7 @@ public void GetNavigationLinks_Null_LogsWarningAndReturns() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetNavigationLinks().Result; + var result = await service.GetNavigationLinks(); _logger.VerifyWarning("No navigation links returned"); @@ -269,7 +269,7 @@ public void GetNavigationLinks_Null_LogsWarningAndReturns() } [TestMethod] - public void GetNavigationLinks_LinksFound_ReturnsListOfLinks() + public async Task GetNavigationLinks_LinksFound_ReturnsListOfLinks() { var links = new List { @@ -298,14 +298,14 @@ public void GetNavigationLinks_LinksFound_ReturnsListOfLinks() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetNavigationLinks().Result; + var result = await service.GetNavigationLinks(); result.Should().NotBeNull(); result.Should().BeSameAs(links); } [TestMethod] - public void GetAdvicePage_Null_LogsAndReturnsDefault() + public async Task GetAdvicePage_Null_LogsAndReturnsDefault() { _clientMock.Setup(client => client.GetEntriesByType( @@ -316,7 +316,7 @@ public void GetAdvicePage_Null_LogsAndReturnsDefault() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetAdvicePage("SomeId").Result; + var result = await service.GetAdvicePage("SomeId"); _logger.VerifyWarning("Advice page with SomeId could not be found"); @@ -324,7 +324,7 @@ public void GetAdvicePage_Null_LogsAndReturnsDefault() } [TestMethod] - public void GetAdvicePage_ReturnsContent_RendersHtmlAndReturns() + public async Task GetAdvicePage_ReturnsContent_RendersHtmlAndReturns() { var content = new ContentfulCollection { @@ -347,7 +347,7 @@ public void GetAdvicePage_ReturnsContent_RendersHtmlAndReturns() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetAdvicePage("SomeId").Result; + var result = await service.GetAdvicePage("SomeId"); result!.Heading.Should().Be("Test Heading"); result.Body.Should().Be(_testRichText); @@ -355,7 +355,161 @@ public void GetAdvicePage_ReturnsContent_RendersHtmlAndReturns() } [TestMethod] - public void GetDetailsPage_Null_LogsAndReturnsDefault() + public async Task GetRadioQuestionPage_ReturnsContent() + { + var content = new ContentfulCollection + { + Items = + [ + new RadioQuestionPage + { + Question = "Question", + AdditionalInformationHeader = "Additional info" + } + ] + }; + + _clientMock.Setup(client => + client.GetEntriesByType( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(content); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + var result = await service.GetRadioQuestionPage("SomeId"); + + result!.Question.Should().Be("Question"); + result.AdditionalInformationHeader.Should().Be("Additional info"); + } + + [TestMethod] + public async Task GetDateQuestionPage_ReturnsContent() + { + var content = new ContentfulCollection + { + Items = + [ + new DateQuestionPage + { + Question = "Question", + QuestionHint = "Question hint" + } + ] + }; + + _clientMock.Setup(client => + client.GetEntriesByType( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(content); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + var result = await service.GetDateQuestionPage("SomeId"); + + result!.Question.Should().Be("Question"); + result.QuestionHint.Should().Be("Question hint"); + } + + [TestMethod] + public async Task GetDropdownQuestionPage_ReturnsContent() + { + var content = new ContentfulCollection + { + Items = + [ + new DropdownQuestionPage + { + Question = "Question", + DropdownHeading = "Dropdown heading" + } + ] + }; + + _clientMock.Setup(client => + client.GetEntriesByType( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(content); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + var result = await service.GetDropdownQuestionPage("SomeId"); + + result!.Question.Should().Be("Question"); + result.DropdownHeading.Should().Be("Dropdown heading"); + } + + [TestMethod] + public async Task GetConfirmQualificationPage_ReturnsContent() + { + var content = new ContentfulCollection + { + Items = + [ + new ConfirmQualificationPage + { + Heading = "Heading", + RadioHeading = "Radio Heading", + AwardingOrganisationLabel = "AO" + } + ] + }; + + _clientMock.Setup(client => + client.GetEntriesByType( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(content); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + var result = await service.GetConfirmQualificationPage(); + + result!.Heading.Should().Be("Heading"); + result.RadioHeading.Should().Be("Radio Heading"); + result.AwardingOrganisationLabel.Should().Be("AO"); + } + + [TestMethod] + public async Task GetQualificationListPage_ReturnsContent() + { + var content = new ContentfulCollection + { + Items = + [ + new QualificationListPage + { + Header = "Header", + AwardingOrganisationHeading = "AO Heading", + MultipleQualificationsFoundText = "Multiple qualifications found" + } + ] + }; + + _clientMock.Setup(client => + client.GetEntriesByType( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(content); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + var result = await service.GetQualificationListPage(); + + result!.Header.Should().Be("Header"); + result.AwardingOrganisationHeading.Should().Be("AO Heading"); + result.MultipleQualificationsFoundText.Should().Be("Multiple qualifications found"); + } + + [TestMethod] + public async Task GetDetailsPage_Null_LogsAndReturnsDefault() { _clientMock.Setup(client => client.GetEntriesByType( @@ -366,7 +520,7 @@ public void GetDetailsPage_Null_LogsAndReturnsDefault() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetDetailsPage().Result; + var result = await service.GetDetailsPage(); _logger.VerifyWarning("No details page entry returned"); @@ -374,7 +528,7 @@ public void GetDetailsPage_Null_LogsAndReturnsDefault() } [TestMethod] - public void GetDetailsPage_NoContent_LogsAndReturnsDefault() + public async Task GetDetailsPage_NoContent_LogsAndReturnsDefault() { _clientMock.Setup(client => client.GetEntriesByType( @@ -385,7 +539,7 @@ public void GetDetailsPage_NoContent_LogsAndReturnsDefault() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetDetailsPage().Result; + var result = await service.GetDetailsPage(); _logger.VerifyWarning("No details page entry returned"); @@ -393,7 +547,7 @@ public void GetDetailsPage_NoContent_LogsAndReturnsDefault() } [TestMethod] - public void GetDetailsPage_Content_RendersHtmlAndReturns() + public async Task GetDetailsPage_Content_RendersHtmlAndReturns() { var content = new ContentfulCollection { @@ -426,7 +580,7 @@ public void GetDetailsPage_Content_RendersHtmlAndReturns() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetDetailsPage().Result; + var result = await service.GetDetailsPage(); result!.AwardingOrgLabel.Should().Be("Test Awarding Org Label"); result.BookmarkHeading.Should().Be("Test bookmark heading"); @@ -445,7 +599,7 @@ public void GetDetailsPage_Content_RendersHtmlAndReturns() } [TestMethod] - public void GetQualificationById_Null_LogsAndReturnsDefault() + public async Task GetQualificationById_Null_LogsAndReturnsDefault() { _clientMock.Setup(client => client.GetEntriesByType( @@ -456,7 +610,7 @@ public void GetQualificationById_Null_LogsAndReturnsDefault() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetQualificationById("SomeId").Result; + var result = await service.GetQualificationById("SomeId"); _logger.VerifyWarning("No qualifications returned for qualificationId: SomeId"); @@ -464,7 +618,7 @@ public void GetQualificationById_Null_LogsAndReturnsDefault() } [TestMethod] - public void GetQualificationById_NoContent_LogsAndReturnsDefault() + public async Task GetQualificationById_NoContent_LogsAndReturnsDefault() { _clientMock.Setup(client => client.GetEntriesByType( @@ -475,7 +629,7 @@ public void GetQualificationById_NoContent_LogsAndReturnsDefault() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetQualificationById("SomeId").Result; + var result = await service.GetQualificationById("SomeId"); _logger.VerifyWarning("No qualifications returned for qualificationId: SomeId"); @@ -483,7 +637,7 @@ public void GetQualificationById_NoContent_LogsAndReturnsDefault() } [TestMethod] - public void GetQualificationById_QualificationExists_Returns() + public async Task GetQualificationById_QualificationExists_Returns() { var qualification = new Qualification("SomeId", "Test qualification name", "Test awarding org", 123, "Test from which year", "Test to which year", "Test qualification number", @@ -499,14 +653,14 @@ public void GetQualificationById_QualificationExists_Returns() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetQualificationById("SomeId").Result; + var result = await service.GetQualificationById("SomeId"); result.Should().NotBeNull(); result.Should().Be(qualification); } [TestMethod] - public void GetPhaseBannerContent_Null_LogsAndReturnsDefault() + public async Task GetPhaseBannerContent_Null_LogsAndReturnsDefault() { _clientMock.Setup(client => client.GetEntriesByType( @@ -517,7 +671,7 @@ public void GetPhaseBannerContent_Null_LogsAndReturnsDefault() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetPhaseBannerContent().Result; + var result = await service.GetPhaseBannerContent(); _logger.VerifyWarning("No phase banner entry returned"); @@ -525,7 +679,7 @@ public void GetPhaseBannerContent_Null_LogsAndReturnsDefault() } [TestMethod] - public void GetPhaseBannerContent_NoContent_LogsAndReturnsDefault() + public async Task GetPhaseBannerContent_NoContent_LogsAndReturnsDefault() { _clientMock.Setup(client => client.GetEntriesByType( @@ -536,7 +690,7 @@ public void GetPhaseBannerContent_NoContent_LogsAndReturnsDefault() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetPhaseBannerContent().Result; + var result = await service.GetPhaseBannerContent(); _logger.VerifyWarning("No phase banner entry returned"); @@ -544,7 +698,7 @@ public void GetPhaseBannerContent_NoContent_LogsAndReturnsDefault() } [TestMethod] - public void GetPhaseBannerContent_PhaseBannerExists_Returns() + public async Task GetPhaseBannerContent_PhaseBannerExists_Returns() { var phaseBanner = new PhaseBanner { @@ -563,7 +717,7 @@ public void GetPhaseBannerContent_PhaseBannerExists_Returns() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetPhaseBannerContent().Result; + var result = await service.GetPhaseBannerContent(); result.Should().NotBeNull(); @@ -574,7 +728,7 @@ public void GetPhaseBannerContent_PhaseBannerExists_Returns() } [TestMethod] - public void GetCookiesBannerContent_Null_LogsAndReturnsDefault() + public async Task GetCookiesBannerContent_Null_LogsAndReturnsDefault() { _clientMock.Setup(client => client.GetEntriesByType( @@ -585,7 +739,7 @@ public void GetCookiesBannerContent_Null_LogsAndReturnsDefault() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetCookiesBannerContent().Result; + var result = await service.GetCookiesBannerContent(); _logger.VerifyWarning("No cookies banner entry returned"); @@ -593,7 +747,7 @@ public void GetCookiesBannerContent_Null_LogsAndReturnsDefault() } [TestMethod] - public void GetCookiesBannerContent_NoContent_LogsAndReturnsDefault() + public async Task GetCookiesBannerContent_NoContent_LogsAndReturnsDefault() { _clientMock.Setup(client => client.GetEntriesByType( @@ -604,7 +758,7 @@ public void GetCookiesBannerContent_NoContent_LogsAndReturnsDefault() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetCookiesBannerContent().Result; + var result = await service.GetCookiesBannerContent(); _logger.VerifyWarning("No cookies banner entry returned"); @@ -612,7 +766,7 @@ public void GetCookiesBannerContent_NoContent_LogsAndReturnsDefault() } [TestMethod] - public void GetCookiesBannerContent_CookiesBannerExists_Returns() + public async Task GetCookiesBannerContent_CookiesBannerExists_Returns() { var cookiesBanner = new CookiesBanner { @@ -636,7 +790,7 @@ public void GetCookiesBannerContent_CookiesBannerExists_Returns() var service = new ContentfulContentService(_clientMock.Object, _logger.Object); - var result = service.GetCookiesBannerContent().Result; + var result = await service.GetCookiesBannerContent(); result.Should().NotBeNull(); @@ -656,4 +810,57 @@ public void GetCookiesBannerContent_CookiesBannerExists_Returns() result.RejectedCookiesContent.Should().Be(cookiesBanner.RejectedCookiesContent); result.RejectedCookiesContent!.Content.Should().ContainSingle(x => ((Text)x).Value == "TEST"); } + + [TestMethod] + public async Task GetQualifications_ReturnsQualifications() + { + var qualification = new Qualification("Id", "Name", + "AO", 6, + "2014", "2020", + "number", "Rq"); + + _clientMock.Setup(c => + c.GetEntriesByType(It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new ContentfulCollection { Items = [qualification] }); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + var result = await service.GetQualifications(); + + result.Should().HaveCount(1).And.Contain(qualification); + } + + [TestMethod] + public async Task GetQualifications_ContentfulHasNoQualifications_ReturnsEmpty() + { + _clientMock.Setup(c => + c.GetEntriesByType(It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(new ContentfulCollection { Items = [] }); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + var result = await service.GetQualifications(); + + result.Should().BeEmpty(); + } + + [TestMethod] + public async Task GetPage_WhenContentfulGetEntriesByTypeThrows_LogsError() + { + _clientMock.Setup(c => + c.GetEntriesByType(It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Throws(); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + await service.GetStartPage(); + + _logger.VerifyError($"Exception trying to retrieve {nameof(StartPage)} from Contentful."); + } } \ No newline at end of file From 673cd10df21aeb1d131ab5b2217a04e2ecf4ead5 Mon Sep 17 00:00:00 2001 From: RobertGHippo Date: Wed, 10 Jul 2024 13:43:25 +0100 Subject: [PATCH 03/15] Couple more unit tests. --- .../Services/ContentfulContentServiceTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/ContentfulContentServiceTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/ContentfulContentServiceTests.cs index 7adf771c..99d7db3c 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/ContentfulContentServiceTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/ContentfulContentServiceTests.cs @@ -476,6 +476,25 @@ public async Task GetConfirmQualificationPage_ReturnsContent() result.AwardingOrganisationLabel.Should().Be("AO"); } + [TestMethod] + public async Task GetConfirmQualificationPage_NoData_ReturnsNull() + { + var content = new ContentfulCollection { Items = [] }; + + _clientMock.Setup(client => + client.GetEntriesByType( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(content); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + var result = await service.GetConfirmQualificationPage(); + + result.Should().BeNull(); + } + [TestMethod] public async Task GetQualificationListPage_ReturnsContent() { @@ -508,6 +527,25 @@ public async Task GetQualificationListPage_ReturnsContent() result.MultipleQualificationsFoundText.Should().Be("Multiple qualifications found"); } + [TestMethod] + public async Task GetQualificationListPage_NoData_ReturnsNull() + { + var content = new ContentfulCollection { Items = [] }; + + _clientMock.Setup(client => + client.GetEntriesByType( + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(content); + + var service = new ContentfulContentService(_clientMock.Object, _logger.Object); + + var result = await service.GetQualificationListPage(); + + result.Should().BeNull(); + } + [TestMethod] public async Task GetDetailsPage_Null_LogsAndReturnsDefault() { From ae0c319cd12322f9209e218d278b55d7b0ad81cc Mon Sep 17 00:00:00 2001 From: Daniel Clarke Date: Mon, 15 Jul 2024 16:50:07 +0100 Subject: [PATCH 04/15] Added new generic error page and new page not found error pages --- .../Controllers/ErrorController.cs | 16 ++++++++++++++-- .../Controllers/HomeController.cs | 2 +- .../Models/Error/ErrorModel.cs | 8 ++++++++ .../Models/Error/ErrorViewModel.cs | 8 ++++++++ .../Models/ErrorViewModel.cs | 11 ----------- src/Dfe.EarlyYearsQualification.Web/Program.cs | 10 +++++++++- .../Views/{Shared => Error}/Error.cshtml | 2 +- .../Views/Error/NotFound.cshtml | 14 ++++++++++++++ .../Views/Error/ProblemWithTheService.cshtml | 14 ++++++++++++++ .../Controllers/ErrorControllerTests.cs | 2 +- .../Models/ErrorViewModelTests.cs | 2 +- 11 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorModel.cs create mode 100644 src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorViewModel.cs delete mode 100644 src/Dfe.EarlyYearsQualification.Web/Models/ErrorViewModel.cs rename src/Dfe.EarlyYearsQualification.Web/Views/{Shared => Error}/Error.cshtml (89%) create mode 100644 src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml create mode 100644 src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs index 03bf3d42..4a169b97 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using Dfe.EarlyYearsQualification.Web.Models; +using Dfe.EarlyYearsQualification.Web.Models.Error; using Microsoft.AspNetCore.Mvc; namespace Dfe.EarlyYearsQualification.Web.Controllers; @@ -11,6 +11,18 @@ public class ErrorController : Controller [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Index() { - return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + return View("ProblemWithTheService"); + } + + [Route("{statusCode:int}")] + public IActionResult HttpStatusCodeHandler(int statusCode) + { + HttpContext.Response.StatusCode = statusCode; + + return statusCode switch + { + 404 => View("NotFound"), + _ => View("ProblemWithTheService") + }; } } \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/HomeController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/HomeController.cs index 878edef4..1f98a6ed 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/HomeController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/HomeController.cs @@ -29,7 +29,7 @@ public async Task Index() var model = await Map(startPageContent); userJourneyCookieService.ResetUserJourneyCookie(); - + return View(model); } diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorModel.cs b/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorModel.cs new file mode 100644 index 00000000..05ed158a --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorModel.cs @@ -0,0 +1,8 @@ +namespace Dfe.EarlyYearsQualification.Web.Models.Error; + +public class ErrorModel +{ + public string Heading { get; init; } = string.Empty; + + public string Content { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorViewModel.cs b/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorViewModel.cs new file mode 100644 index 00000000..dea87e1f --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorViewModel.cs @@ -0,0 +1,8 @@ +namespace Dfe.EarlyYearsQualification.Web.Models.Error; + +public class ErrorViewModel +{ + public string? RequestId { get; init; } + + public bool ShowRequestId => !string.IsNullOrWhiteSpace(RequestId); +} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/ErrorViewModel.cs b/src/Dfe.EarlyYearsQualification.Web/Models/ErrorViewModel.cs deleted file mode 100644 index 685433bf..00000000 --- a/src/Dfe.EarlyYearsQualification.Web/Models/ErrorViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Dfe.EarlyYearsQualification.Web.Models; - -public class ErrorViewModel -{ - public string? RequestId { get; init; } - - public bool ShowRequestId - { - get { return !string.IsNullOrWhiteSpace(RequestId); } - } -} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Program.cs b/src/Dfe.EarlyYearsQualification.Web/Program.cs index 5698e261..0a91afd0 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Program.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Program.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Azure.Identity; using Contentful.AspNetCore; @@ -10,6 +11,7 @@ using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; using GovUk.Frontend.AspNetCore; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; @@ -89,10 +91,16 @@ // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { - app.UseExceptionHandler("/Home/Error"); + app.UseExceptionHandler("/Error"); + app.UseStatusCodePagesWithReExecute("/Error/{0}"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } +else +{ + app.UseDeveloperExceptionPage(); +} app.UseHttpsRedirection(); app.UseStaticFiles(); diff --git a/src/Dfe.EarlyYearsQualification.Web/Views/Shared/Error.cshtml b/src/Dfe.EarlyYearsQualification.Web/Views/Error/Error.cshtml similarity index 89% rename from src/Dfe.EarlyYearsQualification.Web/Views/Shared/Error.cshtml rename to src/Dfe.EarlyYearsQualification.Web/Views/Error/Error.cshtml index 35169387..ab4aab65 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Views/Shared/Error.cshtml +++ b/src/Dfe.EarlyYearsQualification.Web/Views/Error/Error.cshtml @@ -1,4 +1,4 @@ -@model ErrorViewModel +@model Dfe.EarlyYearsQualification.Web.Models.Error.ErrorViewModel @{ ViewData["Title"] = "Error"; } diff --git a/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml b/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml new file mode 100644 index 00000000..2ff52383 --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml @@ -0,0 +1,14 @@ +@{ + ViewData["Title"] = "Page not found"; +} + +
+
+

Page not found

+
+

If you typed the web address, check it is correct.

+

If you pasted the web address, check you copied the entire address.

+

If the web address is correct or you selected a link or button, contact the check an early years qualification team to report a fault with the service.

+
+
+
\ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml b/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml new file mode 100644 index 00000000..92448ee6 --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml @@ -0,0 +1,14 @@ +@{ + ViewData["Title"] = "There is a problem with the service"; +} + +
+
+

Sorry, there is a problem with the service

+
+

Try again later.

+

We have not saved your answers. When the service is available, you will have to start again.

+

You can download the early years qualifications list (EYQL) spreadsheet.

+
+
+
\ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ErrorControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ErrorControllerTests.cs index f94a5c0d..023b3ccc 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ErrorControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ErrorControllerTests.cs @@ -1,6 +1,6 @@ using System.Diagnostics; using Dfe.EarlyYearsQualification.Web.Controllers; -using Dfe.EarlyYearsQualification.Web.Models; +using Dfe.EarlyYearsQualification.Web.Models.Error; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Models/ErrorViewModelTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Models/ErrorViewModelTests.cs index 24dc3e6b..d77661d9 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Models/ErrorViewModelTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Models/ErrorViewModelTests.cs @@ -1,4 +1,4 @@ -using Dfe.EarlyYearsQualification.Web.Models; +using Dfe.EarlyYearsQualification.Web.Models.Error; using FluentAssertions; namespace Dfe.EarlyYearsQualification.UnitTests.Models; From 04f211018818419be5f04da67955c1507b02d3d7 Mon Sep 17 00:00:00 2001 From: Daniel Clarke Date: Tue, 16 Jul 2024 11:52:50 +0100 Subject: [PATCH 05/15] moved response cache to encompass all error pages --- .../Controllers/ErrorController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs index 4a169b97..9ce64020 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs @@ -5,10 +5,10 @@ namespace Dfe.EarlyYearsQualification.Web.Controllers; [Route("/error")] +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public class ErrorController : Controller { [HttpGet] - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Index() { return View("ProblemWithTheService"); From e1473745b313324d6d243d250c452b66e2604727 Mon Sep 17 00:00:00 2001 From: RobertGHippo Date: Tue, 16 Jul 2024 13:33:26 +0100 Subject: [PATCH 06/15] Fixes bug where it would accept later month in current year. New service class responsible for validating DateQuestionModel. New DateTimeAdapter to allow unit testing of validator. --- .../Controllers/QuestionsController.cs | 16 +-- .../QuestionModels/DateQuestionModel.cs | 10 -- .../Validators/DateQuestionModelValidator.cs | 22 ++++ .../Validators/IDateQuestionModelValidator.cs | 6 + .../Program.cs | 4 + .../Services/DatesAndTimes/DateTimeAdapter.cs | 9 ++ .../DatesAndTimes/IDateTimeAdapter.cs | 6 + .../Controllers/QuestionsControllerTests.cs | 114 +++++++++++++----- .../DateQuestionModelValidatorTests.cs | 107 ++++++++++++++++ 9 files changed, 249 insertions(+), 45 deletions(-) create mode 100644 src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/Validators/DateQuestionModelValidator.cs create mode 100644 src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/Validators/IDateQuestionModelValidator.cs create mode 100644 src/Dfe.EarlyYearsQualification.Web/Services/DatesAndTimes/DateTimeAdapter.cs create mode 100644 src/Dfe.EarlyYearsQualification.Web/Services/DatesAndTimes/IDateTimeAdapter.cs create mode 100644 tests/Dfe.EarlyYearsQualification.UnitTests/Services/DateQuestionModelValidatorTests.cs diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs index 0ce6956f..baef0173 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs @@ -5,6 +5,7 @@ using Dfe.EarlyYearsQualification.Web.Constants; using Dfe.EarlyYearsQualification.Web.Controllers.Base; using Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels; +using Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels.Validators; using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; @@ -17,7 +18,8 @@ public class QuestionsController( IContentService contentService, IHtmlRenderer renderer, IUserJourneyCookieService userJourneyCookieService, - IContentFilterService contentFilterService) + IContentFilterService contentFilterService, + IDateQuestionModelValidator questionModelValidator) : ServiceController { private const string Questions = "Questions"; @@ -36,7 +38,7 @@ public async Task WhereWasTheQualificationAwarded(RadioQuestionMo if (!ModelState.IsValid) { var questionPage = await contentService.GetRadioQuestionPage(QuestionPages.WhereWasTheQualificationAwarded); - + // ReSharper disable once InvertIf if (questionPage is not null) { @@ -77,7 +79,7 @@ public async Task WhenWasTheQualificationStarted() [HttpPost("when-was-the-qualification-started")] public async Task WhenWasTheQualificationStarted(DateQuestionModel model) { - if (!ModelState.IsValid || !model.IsModelValid()) + if (!ModelState.IsValid || !questionModelValidator.IsValid(model)) { var questionPage = await contentService.GetDateQuestionPage(QuestionPages.WhenWasTheQualificationStarted); if (questionPage is not null) @@ -173,7 +175,7 @@ public async Task WhatIsTheAwardingOrganisation(DropdownQuestionM private bool WithinDateRange() { - (int? startDateMonth, int? startDateYear) = userJourneyCookieService.GetWhenWasQualificationAwarded(); + var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationAwarded(); if (startDateMonth is not null && startDateYear is not null) { var date = new DateOnly(startDateYear.Value, startDateMonth.Value, 1); @@ -182,11 +184,11 @@ private bool WithinDateRange() return false; } - + private async Task> GetFilteredQualifications() { - int? level = userJourneyCookieService.GetLevelOfQualification(); - (int? startDateMonth, int? startDateYear) = userJourneyCookieService.GetWhenWasQualificationAwarded(); + var level = userJourneyCookieService.GetLevelOfQualification(); + var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationAwarded(); return await contentFilterService.GetFilteredQualifications(level, startDateMonth, startDateYear, null, null); } diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/DateQuestionModel.cs b/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/DateQuestionModel.cs index d1e75db1..7a4c6932 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/DateQuestionModel.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/DateQuestionModel.cs @@ -13,14 +13,4 @@ public class DateQuestionModel : BaseQuestionModel [Required] public int SelectedMonth { get; init; } [Required] public int SelectedYear { get; init; } - - public bool IsModelValid() - { - if (SelectedMonth is < 1 or > 12) - { - return false; - } - - return SelectedYear > 1900 && SelectedYear <= DateTime.UtcNow.Year; - } } \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/Validators/DateQuestionModelValidator.cs b/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/Validators/DateQuestionModelValidator.cs new file mode 100644 index 00000000..2748e089 --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/Validators/DateQuestionModelValidator.cs @@ -0,0 +1,22 @@ +using Dfe.EarlyYearsQualification.Web.Services.DatesAndTimes; + +namespace Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels.Validators; + +public class DateQuestionModelValidator(IDateTimeAdapter dateTimeAdapter) : IDateQuestionModelValidator +{ + public bool IsValid(DateQuestionModel model) + { + if (model.SelectedYear < 1900 + || model.SelectedMonth < 1 + || model.SelectedMonth > 12) + { + return false; + } + + var selectedDate = new DateOnly(model.SelectedYear, model.SelectedMonth, 1); + + var now = dateTimeAdapter.Now(); + + return selectedDate <= DateOnly.FromDateTime(now); + } +} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/Validators/IDateQuestionModelValidator.cs b/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/Validators/IDateQuestionModelValidator.cs new file mode 100644 index 00000000..fd58a87c --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Models/Content/QuestionModels/Validators/IDateQuestionModelValidator.cs @@ -0,0 +1,6 @@ +namespace Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels.Validators; + +public interface IDateQuestionModelValidator +{ + bool IsValid(DateQuestionModel model); +} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Program.cs b/src/Dfe.EarlyYearsQualification.Web/Program.cs index 5698e261..92c3b83f 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Program.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Program.cs @@ -5,8 +5,10 @@ using Dfe.EarlyYearsQualification.Content.Services; using Dfe.EarlyYearsQualification.Mock.Extensions; using Dfe.EarlyYearsQualification.Web.Filters; +using Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels.Validators; using Dfe.EarlyYearsQualification.Web.Security; using Dfe.EarlyYearsQualification.Web.Services.CookiesPreferenceService; +using Dfe.EarlyYearsQualification.Web.Services.DatesAndTimes; using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; using GovUk.Frontend.AspNetCore; using Microsoft.AspNetCore.DataProtection; @@ -65,6 +67,8 @@ }); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); var accessIsChallenged = !builder.Configuration.GetValue("ServiceAccess:IsPublic"); // ...by default, challenge the user for the secret value unless that's explicitly turned off diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/DatesAndTimes/DateTimeAdapter.cs b/src/Dfe.EarlyYearsQualification.Web/Services/DatesAndTimes/DateTimeAdapter.cs new file mode 100644 index 00000000..5ab7cde7 --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Services/DatesAndTimes/DateTimeAdapter.cs @@ -0,0 +1,9 @@ +namespace Dfe.EarlyYearsQualification.Web.Services.DatesAndTimes; + +public class DateTimeAdapter : IDateTimeAdapter +{ + public DateTime Now() + { + return DateTime.Now; + } +} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/DatesAndTimes/IDateTimeAdapter.cs b/src/Dfe.EarlyYearsQualification.Web/Services/DatesAndTimes/IDateTimeAdapter.cs new file mode 100644 index 00000000..637c5073 --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Services/DatesAndTimes/IDateTimeAdapter.cs @@ -0,0 +1,6 @@ +namespace Dfe.EarlyYearsQualification.Web.Services.DatesAndTimes; + +public interface IDateTimeAdapter +{ + DateTime Now(); +} \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QuestionsControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QuestionsControllerTests.cs index b0b1ae31..0dfff89a 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QuestionsControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QuestionsControllerTests.cs @@ -9,6 +9,7 @@ using Dfe.EarlyYearsQualification.Web.Controllers; using Dfe.EarlyYearsQualification.Web.Models; using Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels; +using Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels.Validators; using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; using FluentAssertions; using Microsoft.AspNetCore.Mvc; @@ -28,12 +29,14 @@ public async Task WhereWasTheQualificationAwarded_ContentServiceReturnsNoQuestio var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); mockContentService.Setup(x => x.GetRadioQuestionPage(QuestionPages.WhereWasTheQualificationAwarded)) .ReturnsAsync((RadioQuestionPage?)default).Verifiable(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhereWasTheQualificationAwarded(); @@ -58,6 +61,7 @@ public async Task WhereWasTheQualificationAwarded_ContentServiceReturnsQuestionP var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var questionPage = new RadioQuestionPage { @@ -69,7 +73,8 @@ public async Task WhereWasTheQualificationAwarded_ContentServiceReturnsQuestionP .ReturnsAsync(questionPage); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhereWasTheQualificationAwarded(); @@ -97,9 +102,11 @@ public async Task Post_WhereWasTheQualificationAwarded_InvalidModel_ReturnsQuest var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); controller.ModelState.AddModelError("option", "test error"); var result = await controller.WhereWasTheQualificationAwarded(new RadioQuestionModel()); @@ -122,9 +129,11 @@ public async Task Post_WhereWasTheQualificationAwarded_PassInOutsideUk_Redirects var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhereWasTheQualificationAwarded(new RadioQuestionModel @@ -149,9 +158,11 @@ public async Task Post_WhereWasTheQualificationAwarded_PassInEngland_RedirectsTo var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhereWasTheQualificationAwarded(new RadioQuestionModel { Option = Options.England }); @@ -174,6 +185,7 @@ public async Task WhenWasTheQualificationStarted_ReturnsView() var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var questionPage = new DateQuestionPage { @@ -188,7 +200,8 @@ public async Task WhenWasTheQualificationStarted_ReturnsView() .ReturnsAsync(questionPage); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhenWasTheQualificationStarted(); @@ -219,12 +232,14 @@ public async Task WhenWasTheQualificationStarted_CantFindContentfulPage_ReturnsE var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); mockContentService.Setup(x => x.GetDateQuestionPage(QuestionPages.WhenWasTheQualificationStarted)) .ReturnsAsync((DateQuestionPage?)default).Verifiable(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhenWasTheQualificationStarted(); @@ -246,9 +261,11 @@ public async Task Post_WhenWasTheQualificationStarted_InvalidModel_ReturnsDateQu var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); controller.ModelState.AddModelError("option", "test error"); var result = await controller.WhenWasTheQualificationStarted(new DateQuestionModel()); @@ -276,6 +293,7 @@ public async Task Post_WhenWasTheQualificationStarted_PassedInvalidValues_Return var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var questionPage = new DateQuestionPage { @@ -290,7 +308,8 @@ public async Task Post_WhenWasTheQualificationStarted_PassedInvalidValues_Return .ReturnsAsync(questionPage); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhenWasTheQualificationStarted(new DateQuestionModel { @@ -323,6 +342,7 @@ public async Task Post_WhenWasTheQualificationStarted_YearProvidedIsNextYear_Ret var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var questionPage = new DateQuestionPage { @@ -337,7 +357,8 @@ public async Task Post_WhenWasTheQualificationStarted_YearProvidedIsNextYear_Ret .ReturnsAsync(questionPage); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhenWasTheQualificationStarted(new DateQuestionModel { @@ -370,14 +391,23 @@ public async Task Post_WhenWasTheQualificationStarted_ValidModel_ReturnsRedirect var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); + + mockQuestionModelValidator + .Setup(x => x.IsValid(It.IsAny())) + .Returns(true); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); + + const int selectedMonth = 12; + const int selectedYear = 2024; var result = await controller.WhenWasTheQualificationStarted(new DateQuestionModel { - SelectedMonth = 12, - SelectedYear = 2024 + SelectedMonth = selectedMonth, + SelectedYear = selectedYear }); result.Should().NotBeNull(); @@ -387,7 +417,9 @@ public async Task Post_WhenWasTheQualificationStarted_ValidModel_ReturnsRedirect resultType!.ActionName.Should().Be("WhatLevelIsTheQualification"); - mockUserJourneyCookieService.Verify(x => x.SetWhenWasQualificationAwarded("12/2024"), Times.Once); + mockUserJourneyCookieService + .Verify(x => x.SetWhenWasQualificationAwarded($"{selectedMonth}/{selectedYear}"), + Times.Once); } [TestMethod] @@ -398,12 +430,14 @@ public async Task WhatLevelIsTheQualification_ContentServiceReturnsNoQuestionPag var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); mockContentService.Setup(x => x.GetRadioQuestionPage(QuestionPages.WhatLevelIsTheQualification)) .ReturnsAsync((RadioQuestionPage?)default).Verifiable(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhatLevelIsTheQualification(); @@ -428,6 +462,7 @@ public async Task WhatLevelIsTheQualification_ContentServiceReturnsQuestionPage_ var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var questionPage = new RadioQuestionPage { @@ -443,7 +478,8 @@ public async Task WhatLevelIsTheQualification_ContentServiceReturnsQuestionPage_ mockRenderer.Setup(x => x.ToHtml(It.IsAny())).ReturnsAsync("Test html body"); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhatLevelIsTheQualification(); @@ -475,9 +511,11 @@ public async Task Post_WhatLevelIsTheQualification_InvalidModel_ReturnsQuestionP var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); controller.ModelState.AddModelError("option", "test error"); var result = await controller.WhatLevelIsTheQualification(new RadioQuestionModel()); @@ -500,9 +538,11 @@ public async Task Post_WhatLevelIsTheQualification_ReturnsRedirectResponse() var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhatLevelIsTheQualification(new RadioQuestionModel { @@ -527,11 +567,13 @@ public async Task Post_WhatLevelIsTheQualification_Level2WithInDate_ReturnsRedir var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationAwarded()) .Returns((6, 2015)); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhatLevelIsTheQualification(new RadioQuestionModel { @@ -555,12 +597,14 @@ public async Task WhatIsTheAwardingOrganisation_ContentServiceReturnsNoQuestionP var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); mockContentService.Setup(x => x.GetDropdownQuestionPage(QuestionPages.WhatIsTheAwardingOrganisation)) .ReturnsAsync((DropdownQuestionPage?)default).Verifiable(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhatIsTheAwardingOrganisation(); @@ -585,6 +629,7 @@ public async Task WhatIsTheAwardingOrganisation_ContentServiceReturnsQuestionPag var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var questionPage = new DropdownQuestionPage { @@ -605,12 +650,13 @@ public async Task WhatIsTheAwardingOrganisation_ContentServiceReturnsQuestionPag It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), + It.IsAny(), It.IsAny())) .ReturnsAsync([]); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhatIsTheAwardingOrganisation(); @@ -642,6 +688,7 @@ public async Task var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var questionPage = new DropdownQuestionPage { @@ -677,7 +724,8 @@ public async Task .ReturnsAsync(listOfQualifications); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhatIsTheAwardingOrganisation(); @@ -711,6 +759,7 @@ public async Task var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var questionPage = new DropdownQuestionPage { @@ -743,7 +792,8 @@ public async Task .ReturnsAsync(listOfQualifications); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhatIsTheAwardingOrganisation(); @@ -770,9 +820,11 @@ public async Task Post_WhatIsTheAwardingOrganisation_InvalidModel_ReturnsQuestio var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var questionPage = new DropdownQuestionPage { @@ -819,9 +871,11 @@ public async Task Post_WhatIsTheAwardingOrganisation_NoValueSelectedAndNotInList var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var result = await controller.WhatIsTheAwardingOrganisation(new DropdownQuestionModel { @@ -848,9 +902,11 @@ public async Task var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var questionPage = new DropdownQuestionPage { @@ -894,9 +950,11 @@ public async Task var mockRenderer = new Mock(); var mockUserJourneyCookieService = new Mock(); var mockContentFilterService = new Mock(); + var mockQuestionModelValidator = new Mock(); var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockRenderer.Object, - mockUserJourneyCookieService.Object, mockContentFilterService.Object); + mockUserJourneyCookieService.Object, mockContentFilterService.Object, + mockQuestionModelValidator.Object); var questionPage = new DropdownQuestionPage { diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/DateQuestionModelValidatorTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/DateQuestionModelValidatorTests.cs new file mode 100644 index 00000000..349ade57 --- /dev/null +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/DateQuestionModelValidatorTests.cs @@ -0,0 +1,107 @@ +using Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels; +using Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels.Validators; +using Dfe.EarlyYearsQualification.Web.Services.DatesAndTimes; +using FluentAssertions; +using Moq; + +namespace Dfe.EarlyYearsQualification.UnitTests.Services; + +[TestClass] +public class DateQuestionModelValidatorTests +{ + [TestMethod] + public void DateQuestionModelValidator_GivenDateInRecentPast_ValidatesTrue() + { + var dateTimeAdapter = new Mock(); + dateTimeAdapter.Setup(d => d.Now()).Returns(new DateTime(2024, 7, 16, 13, 1, 12, DateTimeKind.Local)); + + var validator = new DateQuestionModelValidator(dateTimeAdapter.Object); + + var model = new DateQuestionModel { SelectedMonth = 5, SelectedYear = 2023 }; + + validator.IsValid(model).Should().BeTrue(); + } + + [TestMethod] + public void DateQuestionModelValidator_GivenDateEarlierThisMonth_ValidatesTrue() + { + var thisYear = 2024; + var thisMonth = 7; + + var dateTimeAdapter = new Mock(); + dateTimeAdapter.Setup(d => d.Now()) + .Returns(new DateTime(thisYear, thisMonth, 16, 13, 1, 12, DateTimeKind.Local)); + + var validator = new DateQuestionModelValidator(dateTimeAdapter.Object); + + var model = new DateQuestionModel { SelectedMonth = thisMonth, SelectedYear = thisYear }; + + validator.IsValid(model).Should().BeTrue(); + } + + [TestMethod] + public void DateQuestionModelValidator_GivenDateThisMonthOnFirstOfMonth_ValidatesTrue() + { + var thisYear = 2024; + var thisMonth = 7; + + var dateTimeAdapter = new Mock(); + dateTimeAdapter.Setup(d => d.Now()) + .Returns(new DateTime(thisYear, thisMonth, 1, 0, 0, 1, DateTimeKind.Local)); + + var validator = new DateQuestionModelValidator(dateTimeAdapter.Object); + + var model = new DateQuestionModel { SelectedMonth = thisMonth, SelectedYear = thisYear }; + + validator.IsValid(model).Should().BeTrue(); + } + + [TestMethod] + public void DateQuestionModelValidator_GivenDateInFuture_ValidatesFalse() + { + var dateTimeAdapter = new Mock(); + dateTimeAdapter.Setup(d => d.Now()).Returns(new DateTime(2022, 10, 10, 15, 32, 12, DateTimeKind.Local)); + + var validator = new DateQuestionModelValidator(dateTimeAdapter.Object); + + var model = new DateQuestionModel { SelectedMonth = 5, SelectedYear = 2023 }; + + validator.IsValid(model).Should().BeFalse(); + } + + [TestMethod] + public void DateQuestionModelValidator_GivenDateBefore1900_ValidatesFalse() + { + var dateTimeAdapter = new Mock(); + + var validator = new DateQuestionModelValidator(dateTimeAdapter.Object); + + var model = new DateQuestionModel { SelectedMonth = 12, SelectedYear = 1899 }; + + validator.IsValid(model).Should().BeFalse(); + } + + [TestMethod] + public void DateQuestionModelValidator_GivenMonthBefore1_ValidatesFalse() + { + var dateTimeAdapter = new Mock(); + + var validator = new DateQuestionModelValidator(dateTimeAdapter.Object); + + var model = new DateQuestionModel { SelectedMonth = 0, SelectedYear = 2024 }; + + validator.IsValid(model).Should().BeFalse(); + } + + [TestMethod] + public void DateQuestionModelValidator_GivenMonthAfter12_ValidatesFalse() + { + var dateTimeAdapter = new Mock(); + + var validator = new DateQuestionModelValidator(dateTimeAdapter.Object); + + var model = new DateQuestionModel { SelectedMonth = 13, SelectedYear = 2024 }; + + validator.IsValid(model).Should().BeFalse(); + } +} \ No newline at end of file From 8be35e850a0361efaa7086ea424a784d30cdc56a Mon Sep 17 00:00:00 2001 From: RobertGHippo Date: Tue, 16 Jul 2024 13:42:07 +0100 Subject: [PATCH 07/15] Rename to clarify this isn't DateQuestionModel validation. --- .../Controllers/QuestionsController.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs index baef0173..e5234a43 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs @@ -121,7 +121,7 @@ public async Task WhatLevelIsTheQualification(RadioQuestionModel userJourneyCookieService.SetLevelOfQualification(model.Option!); - if (model.Option == "2" && WithinDateRange()) + if (model.Option == "2" && WasAwardedBetweenSeptember2014AndAugust2019()) { return RedirectToAction("QualificationsStartedBetweenSept2014AndAug2019", "Advice"); } @@ -173,7 +173,7 @@ public async Task WhatIsTheAwardingOrganisation(DropdownQuestionM return RedirectToAction("Get", "QualificationDetails"); } - private bool WithinDateRange() + private bool WasAwardedBetweenSeptember2014AndAugust2019() { var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationAwarded(); if (startDateMonth is not null && startDateYear is not null) @@ -243,11 +243,13 @@ private static DropdownQuestionModel MapDropdownModel(DropdownQuestionModel mode { var awardingOrganisationExclusions = new[] { AwardingOrganisations.AllHigherEducation, AwardingOrganisations.Various }; - var uniqueAwardingOrganisations = qualifications.Select(x => x.AwardingOrganisationTitle) - .Distinct() - .Where(x => !awardingOrganisationExclusions.Any(x.Contains)) - .Order() - .ToList(); + + var uniqueAwardingOrganisations = + qualifications.Select(x => x.AwardingOrganisationTitle) + .Distinct() + .Where(x => !Array.Exists(awardingOrganisationExclusions, x.Contains)) + .Order() + .ToList(); model.ActionName = actionName; model.ControllerName = controllerName; From 4c00c5cf2e0343363f63e5a52c2b669b5871a5d9 Mon Sep 17 00:00:00 2001 From: RobertGHippo Date: Tue, 16 Jul 2024 13:47:54 +0100 Subject: [PATCH 08/15] Test that "next month" isn't valid. --- .../Services/DateQuestionModelValidatorTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/DateQuestionModelValidatorTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/DateQuestionModelValidatorTests.cs index 349ade57..72282b27 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/DateQuestionModelValidatorTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/DateQuestionModelValidatorTests.cs @@ -56,6 +56,23 @@ public void DateQuestionModelValidator_GivenDateThisMonthOnFirstOfMonth_Validate validator.IsValid(model).Should().BeTrue(); } + [TestMethod] + public void DateQuestionModelValidator_GivenDateLaterThisYear_ValidatesFalse() + { + var thisYear = 2024; + var thisMonth = 7; + + var dateTimeAdapter = new Mock(); + dateTimeAdapter.Setup(d => d.Now()) + .Returns(new DateTime(thisYear, thisMonth, 1, 0, 0, 1, DateTimeKind.Local)); + + var validator = new DateQuestionModelValidator(dateTimeAdapter.Object); + + var model = new DateQuestionModel { SelectedMonth = thisMonth + 1, SelectedYear = thisYear }; + + validator.IsValid(model).Should().BeFalse(); + } + [TestMethod] public void DateQuestionModelValidator_GivenDateInFuture_ValidatesFalse() { From b2c294b77ecb06df3e82f95c1cf56081b82a211a Mon Sep 17 00:00:00 2001 From: Daniel Clarke Date: Tue, 16 Jul 2024 14:29:11 +0100 Subject: [PATCH 09/15] removed some old error page related files, tweaked some of the new error page layouts and added unit tests --- .../Controllers/ErrorController.cs | 2 - .../Models/Error/ErrorModel.cs | 8 --- .../Models/Error/ErrorViewModel.cs | 8 --- .../Views/Error/Error.cshtml | 25 -------- .../Views/Error/NotFound.cshtml | 12 ++-- .../Controllers/ErrorControllerTests.cs | 60 +++++++++---------- .../Models/ErrorViewModelTests.cs | 40 ------------- 7 files changed, 37 insertions(+), 118 deletions(-) delete mode 100644 src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorModel.cs delete mode 100644 src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorViewModel.cs delete mode 100644 src/Dfe.EarlyYearsQualification.Web/Views/Error/Error.cshtml delete mode 100644 tests/Dfe.EarlyYearsQualification.UnitTests/Models/ErrorViewModelTests.cs diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs index 9ce64020..bf36ad00 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/ErrorController.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; -using Dfe.EarlyYearsQualification.Web.Models.Error; using Microsoft.AspNetCore.Mvc; namespace Dfe.EarlyYearsQualification.Web.Controllers; diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorModel.cs b/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorModel.cs deleted file mode 100644 index 05ed158a..00000000 --- a/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Dfe.EarlyYearsQualification.Web.Models.Error; - -public class ErrorModel -{ - public string Heading { get; init; } = string.Empty; - - public string Content { get; init; } = string.Empty; -} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorViewModel.cs b/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorViewModel.cs deleted file mode 100644 index dea87e1f..00000000 --- a/src/Dfe.EarlyYearsQualification.Web/Models/Error/ErrorViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Dfe.EarlyYearsQualification.Web.Models.Error; - -public class ErrorViewModel -{ - public string? RequestId { get; init; } - - public bool ShowRequestId => !string.IsNullOrWhiteSpace(RequestId); -} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Views/Error/Error.cshtml b/src/Dfe.EarlyYearsQualification.Web/Views/Error/Error.cshtml deleted file mode 100644 index ab4aab65..00000000 --- a/src/Dfe.EarlyYearsQualification.Web/Views/Error/Error.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@model Dfe.EarlyYearsQualification.Web.Models.Error.ErrorViewModel -@{ - ViewData["Title"] = "Error"; -} - -

Error.

-

An error occurred while processing your request.

- -@if (Model.ShowRequestId) -{ -

- Request ID: @Model.RequestId -

-} - -

Development Mode

-

- Swapping to Development environment will display more detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

\ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml b/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml index 2ff52383..15043ca0 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml +++ b/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml @@ -4,11 +4,13 @@
-

Page not found

-
-

If you typed the web address, check it is correct.

-

If you pasted the web address, check you copied the entire address.

-

If the web address is correct or you selected a link or button, contact the check an early years qualification team to report a fault with the service.

+

Page not found

+
+

+ If you typed the web address, check it is correct.

+ If you pasted the web address, check you copied the entire address.

+ If the web address is correct or you selected a link or button, contact the check an early years qualification team to report a fault with the service. +

\ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ErrorControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ErrorControllerTests.cs index 023b3ccc..6f179e22 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ErrorControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ErrorControllerTests.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; using Dfe.EarlyYearsQualification.Web.Controllers; -using Dfe.EarlyYearsQualification.Web.Models.Error; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -11,47 +9,49 @@ namespace Dfe.EarlyYearsQualification.UnitTests.Controllers; public class ErrorControllerTests { [TestMethod] - public void GetError_ReturnsViewWithTraceId() + public void Index_ReturnsProblemWithServiceView() { var controller = new ErrorController(); - const string traceIdentifier = "Trace"; - - controller.ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext - { - TraceIdentifier = traceIdentifier - } - }; - var result = controller.Index(); result.Should().BeAssignableTo() - .Which.Model.Should().BeAssignableTo() - .Which.RequestId.Should().Be(traceIdentifier); + .Which.ViewName.Should().Be("ProblemWithTheService"); } [TestMethod] - public void GetError_ReturnsViewWithActivityId() + public void HttpStatusCodeHandler_404_ReturnsNotFoundView() { var controller = new ErrorController(); + + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; - const string operationName = "UnitTest"; - - var activity = new Activity(operationName).Start(); + var result = controller.HttpStatusCodeHandler(404); + + result.Should().BeAssignableTo() + .Which.ViewName.Should().Be("NotFound"); - try - { - var result = controller.Index(); + controller.ControllerContext.HttpContext.Response.StatusCode.Should().Be(404); + } + + [TestMethod] + public void HttpStatusCodeHandler_Not404_ReturnsProblemWithServiceView() + { + var controller = new ErrorController(); + + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + }; - result.Should().BeAssignableTo() - .Which.Model.Should().BeAssignableTo() - .Which.RequestId.Should().Be(activity.Id); - } - finally - { - activity.Stop(); - } + var result = controller.HttpStatusCodeHandler(500); + + result.Should().BeAssignableTo() + .Which.ViewName.Should().Be("ProblemWithTheService"); + + controller.ControllerContext.HttpContext.Response.StatusCode.Should().Be(500); } } \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Models/ErrorViewModelTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Models/ErrorViewModelTests.cs deleted file mode 100644 index d77661d9..00000000 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Models/ErrorViewModelTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Dfe.EarlyYearsQualification.Web.Models.Error; -using FluentAssertions; - -namespace Dfe.EarlyYearsQualification.UnitTests.Models; - -[TestClass] -public class ErrorViewModelTests -{ - [TestMethod] - public void RequestId_IsNotEmpty_ShowRequestId_IsTrue() - { - var model = new ErrorViewModel { RequestId = "xyz" }; - - model.ShowRequestId.Should().BeTrue(); - } - - [TestMethod] - public void RequestId_IsNull_ShowRequestId_IsFalse() - { - var model = new ErrorViewModel { RequestId = null }; - - model.ShowRequestId.Should().BeFalse(); - } - - [TestMethod] - public void RequestId_IsEmpty_ShowRequestId_IsFalse() - { - var model = new ErrorViewModel { RequestId = string.Empty }; - - model.ShowRequestId.Should().BeFalse(); - } - - [TestMethod] - public void RequestId_IsWhitespace_ShowRequestId_IsFalse() - { - var model = new ErrorViewModel { RequestId = " " }; - - model.ShowRequestId.Should().BeFalse(); - } -} \ No newline at end of file From c698c618a7cd2cdcc9643a447bbbe9797775a2f8 Mon Sep 17 00:00:00 2001 From: Daniel Clarke Date: Tue, 16 Jul 2024 14:29:23 +0100 Subject: [PATCH 10/15] tweaked problem with service page layout --- .../Views/Error/ProblemWithTheService.cshtml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml b/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml index 92448ee6..f7e03c25 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml +++ b/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml @@ -4,11 +4,13 @@
-

Sorry, there is a problem with the service

-
-

Try again later.

-

We have not saved your answers. When the service is available, you will have to start again.

-

You can download the early years qualifications list (EYQL) spreadsheet.

+

Sorry, there is a problem with the service

+
+

+ Try again later.

+ We have not saved your answers.

+ When the service is available, you will have to start again.You can download the early years qualifications list (EYQL) spreadsheet. +

\ No newline at end of file From dfd4361ba453e1d2eb1d697f141bde85556f26c3 Mon Sep 17 00:00:00 2001 From: Daniel Clarke Date: Tue, 16 Jul 2024 14:32:24 +0100 Subject: [PATCH 11/15] removed random line space in home controller --- .../Controllers/HomeController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/HomeController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/HomeController.cs index 1f98a6ed..878edef4 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/HomeController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/HomeController.cs @@ -29,7 +29,7 @@ public async Task Index() var model = await Map(startPageContent); userJourneyCookieService.ResetUserJourneyCookie(); - + return View(model); } From beb51fe8b5ff01522ee041aacf1090c49c73ff22 Mon Sep 17 00:00:00 2001 From: Daniel Clarke Date: Tue, 16 Jul 2024 16:11:08 +0100 Subject: [PATCH 12/15] removed unused using statements from program + added E2E tests --- .../Program.cs | 2 -- .../Views/Error/NotFound.cshtml | 2 +- .../Views/Error/ProblemWithTheService.cshtml | 2 +- .../e2e/pages/not-found-page-spec.cy.js | 32 +++++++++++++++++++ .../pages/problem-with-the-service-spec.cy.js | 15 +++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/not-found-page-spec.cy.js create mode 100644 tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/problem-with-the-service-spec.cy.js diff --git a/src/Dfe.EarlyYearsQualification.Web/Program.cs b/src/Dfe.EarlyYearsQualification.Web/Program.cs index 0a91afd0..92b89b23 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Program.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Program.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Azure.Identity; using Contentful.AspNetCore; @@ -11,7 +10,6 @@ using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; using GovUk.Frontend.AspNetCore; using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; diff --git a/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml b/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml index 15043ca0..12c84fae 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml +++ b/src/Dfe.EarlyYearsQualification.Web/Views/Error/NotFound.cshtml @@ -9,7 +9,7 @@

If you typed the web address, check it is correct.

If you pasted the web address, check you copied the entire address.

- If the web address is correct or you selected a link or button, contact the check an early years qualification team to report a fault with the service. + If the web address is correct or you selected a link or button, contact the check an early years qualification team to report a fault with the service.

diff --git a/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml b/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml index f7e03c25..9d74ad11 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml +++ b/src/Dfe.EarlyYearsQualification.Web/Views/Error/ProblemWithTheService.cshtml @@ -9,7 +9,7 @@

Try again later.

We have not saved your answers.

- When the service is available, you will have to start again.You can download the early years qualifications list (EYQL) spreadsheet. + When the service is available, you will have to start again.You can download the early years qualifications list (EYQL) spreadsheet.

diff --git a/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/not-found-page-spec.cy.js b/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/not-found-page-spec.cy.js new file mode 100644 index 00000000..ef3f4d47 --- /dev/null +++ b/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/not-found-page-spec.cy.js @@ -0,0 +1,32 @@ +describe("A spec used to test the not found page", () => { + + beforeEach(() => { + cy.setCookie('auth-secret', Cypress.env('auth_secret')); + }); + + it("Checks the page contains the relevant components", () => { + cy.visit({ + url: "/error/404", + failOnStatusCode: false, + }); + + cy.get("#page-not-found-heading").should("contain.text", "Page not found"); + cy.get("#page-not-found-statement-body").should("contain.text", "If you typed the web address, check it is correct."); + cy.get("#page-not-found-statement-body").should("contain.text", "If you pasted the web address, check you copied the entire address."); + cy.get("#page-not-found-statement-body").should("contain.text", "If the web address is correct or you selected a link or button, contact the check an early years qualification team to report a fault with the service."); + + cy.get("#page-not-found-link").should("have.attr", "href", "#"); + + }); + + it("Check that visiting a URL that doesn't exist shows this page without altering the URL", () => { + cy.visit({ + url: "/does-not-exist", + failOnStatusCode: false, + }); + + cy.url().should("include", "/does-not-exist"); + + cy.get("#page-not-found-heading").should("contain.text", "Page not found"); + }); +}); \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/problem-with-the-service-spec.cy.js b/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/problem-with-the-service-spec.cy.js new file mode 100644 index 00000000..a04f0b5e --- /dev/null +++ b/tests/Dfe.EarlyYearsQualification.E2ETests/cypress/e2e/pages/problem-with-the-service-spec.cy.js @@ -0,0 +1,15 @@ +describe("A spec used to test the not found page", () => { + + it("Checks the page contains the relevant components", () => { + cy.setCookie('auth-secret', Cypress.env('auth_secret')); + + cy.visit("/error"); + + cy.get("#problem-with-service-heading").should("contain.text", "Sorry, there is a problem with the service"); + cy.get("#problem-with-service-body").should("contain.text", "Try again later."); + cy.get("#problem-with-service-body").should("contain.text", "We have not saved your answers."); + cy.get("#problem-with-service-body").should("contain.text", "When the service is available, you will have to start again.You can download the early years qualifications list (EYQL) spreadsheet."); + + cy.get("#problem-with-service-link").should("have.attr", "href", "https://www.gov.uk/government/publications/early-years-qualifications-achieved-in-england"); + }); +}); \ No newline at end of file From a70ac136993d189440ca215359fa5602fc522bc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 08:17:24 +0100 Subject: [PATCH 13/15] Bump contentful.aspnetcore and contentful.csharp (#238) Bumps contentful.aspnetcore and contentful.csharp. These dependencies needed to be updated together. Updates `contentful.aspnetcore` from 7.6.0 to 7.6.1 Updates `contentful.csharp` from 7.6.0 to 7.6.1 --- updated-dependencies: - dependency-name: contentful.aspnetcore dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: contentful.csharp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Dfe.EarlyYearsQualification.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dfe.EarlyYearsQualification.Web/Dfe.EarlyYearsQualification.Web.csproj b/src/Dfe.EarlyYearsQualification.Web/Dfe.EarlyYearsQualification.Web.csproj index e83c84e7..3a1c3217 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Dfe.EarlyYearsQualification.Web.csproj +++ b/src/Dfe.EarlyYearsQualification.Web/Dfe.EarlyYearsQualification.Web.csproj @@ -12,7 +12,7 @@ - + From 1ac851ae9ae09e900f508a64c782c612117fc9de Mon Sep 17 00:00:00 2001 From: Sam C <156680559+sam-c-dfe@users.noreply.github.com> Date: Wed, 17 Jul 2024 08:38:28 +0100 Subject: [PATCH 14/15] =?UTF-8?q?POC=20for=20the=20upload=20of=20the=20add?= =?UTF-8?q?itional=20requirement=20questions=20&=20ratio=20re=E2=80=A6=20(?= =?UTF-8?q?#240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * POC for the upload of the additional requirement questions & ratio requirements * Added test file * Updated to set the editor interface for the new fields. Fixed issue with version mismatch issue we got whenever we ran the script on existing records * Renamed test file --- ...rlyYearsQualification.ContentUpload.csproj | 5 +- .../Program.cs | 127 ++++++++++++++---- .../QualificationUpload.cs | 30 +++++ .../csv/reference-upload-test.csv | 4 + 4 files changed, 138 insertions(+), 28 deletions(-) create mode 100644 content/Dfe.EarlyYearsQualification.ContentUpload/QualificationUpload.cs create mode 100644 content/Dfe.EarlyYearsQualification.ContentUpload/csv/reference-upload-test.csv diff --git a/content/Dfe.EarlyYearsQualification.ContentUpload/Dfe.EarlyYearsQualification.ContentUpload.csproj b/content/Dfe.EarlyYearsQualification.ContentUpload/Dfe.EarlyYearsQualification.ContentUpload.csproj index debfd425..f5047e54 100644 --- a/content/Dfe.EarlyYearsQualification.ContentUpload/Dfe.EarlyYearsQualification.ContentUpload.csproj +++ b/content/Dfe.EarlyYearsQualification.ContentUpload/Dfe.EarlyYearsQualification.ContentUpload.csproj @@ -8,7 +8,7 @@ - + @@ -23,6 +23,9 @@ Always + + Always + diff --git a/content/Dfe.EarlyYearsQualification.ContentUpload/Program.cs b/content/Dfe.EarlyYearsQualification.ContentUpload/Program.cs index 2e9deebb..59d2e9b6 100644 --- a/content/Dfe.EarlyYearsQualification.ContentUpload/Program.cs +++ b/content/Dfe.EarlyYearsQualification.ContentUpload/Program.cs @@ -4,7 +4,6 @@ using Contentful.Core.Models; using Contentful.Core.Models.Management; using Contentful.Core.Search; -using Dfe.EarlyYearsQualification.Content.Entities; using Microsoft.VisualBasic.FileIO; namespace Dfe.EarlyYearsQualification.ContentUpload; @@ -12,7 +11,7 @@ namespace Dfe.EarlyYearsQualification.ContentUpload; [ExcludeFromCodeCoverage] public static class Program { - private const string Locale = "en-US"; + private const string Locale = "en-GB"; private const string SpaceId = ""; private const string ManagementApiKey = ""; @@ -67,11 +66,9 @@ await client.PublishEntry(entryToPublish.SystemProperties.Id, private static async Task SetUpContentModels(ContentfulManagementClient client) { // Check current version of model - var currentModels = await client.GetContentTypes(); + var contentTypeModel = await client.GetContentType("Qualification"); - var currentModel = currentModels.FirstOrDefault(x => x.SystemProperties.Id == "Qualification"); - - var version = currentModel?.SystemProperties.Version ?? 1; + var version = contentTypeModel?.SystemProperties.Version ?? 1; var contentType = new ContentType { @@ -136,18 +133,48 @@ private static async Task SetUpContentModels(ContentfulManagementClient client) Name = "Additional Requirements", Id = "additionalRequirements", Type = "Text" + }, + new Field + { + Name = "Additional Requirement Questions", + Id = "additionalRequirementQuestions", + Type = "Array", + Items = new Schema + { + Type = "Link", + LinkType = "Entry", + Validations = [new LinkContentTypeValidator + { + ContentTypeIds = ["additionalRequirementQuestion"] + }] + } + }, + new Field + { + Name = "Ratio Requirements", + Id = "ratioRequirements", + Type = "Array", + Items = new Schema + { + Type = "Link", + LinkType = "Entry", + Validations = [new LinkContentTypeValidator + { + ContentTypeIds = ["ratioRequirement"] + }] + } } ] }; var typeToActivate = await client.CreateOrUpdateContentType(contentType, version: version); await client.ActivateContentType("Qualification", typeToActivate.SystemProperties.Version!.Value); - + Thread.Sleep(2000); // Allows the API time to activate the content type - await SetHelpText(client, typeToActivate); + await SetHelpText(client); } - private static async Task SetHelpText(ContentfulManagementClient client, ContentType typeToActivate) + private static async Task SetHelpText(ContentfulManagementClient client) { var editorInterface = await client.GetEditorInterface("Qualification"); SetHelpTextForField(editorInterface, "qualificationId", @@ -163,9 +190,10 @@ private static async Task SetHelpText(ContentfulManagementClient client, Content SetHelpTextForField(editorInterface, "qualificationNumber", "The number of the qualification"); SetHelpTextForField(editorInterface, "additionalRequirements", "The additional requirements for the qualification", SystemWidgetIds.MultipleLine); - - await client.UpdateEditorInterface(editorInterface, "Qualification", - typeToActivate.SystemProperties.Version!.Value); + SetHelpTextForField(editorInterface, "additionalRequirementQuestions", "The optional additional requirements questions", SystemWidgetIds.EntryMultipleLinksEditor); + SetHelpTextForField(editorInterface, "ratioRequirements", "The optional ratio requirements", SystemWidgetIds.EntryMultipleLinksEditor); + + await client.UpdateEditorInterface(editorInterface, "Qualification", editorInterface.SystemProperties.Version!.Value); } private static void SetHelpTextForField(EditorInterface editorInterface, string fieldId, string helpText, @@ -176,8 +204,27 @@ private static void SetHelpTextForField(EditorInterface editorInterface, string editorInterface.Controls.First(x => x.FieldId == fieldId).WidgetId = widgetId; } - private static Entry BuildEntryFromQualification(Qualification qualification) + private static Entry BuildEntryFromQualification(QualificationUpload qualification) { + var addtionalRequirementQuestions = new List(); + if (qualification.AdditionalRequirementQuestions is not null) + { + foreach (var questionId in qualification.AdditionalRequirementQuestions) + { + addtionalRequirementQuestions.Add(new Reference(SystemLinkTypes.Entry, questionId)); + } + } + + var ratioRequirements = new List(); + if (qualification.RatioRequirements is not null) + { + foreach (var ratioId in qualification.RatioRequirements) + { + ratioRequirements.Add(new Reference(SystemLinkTypes.Entry, ratioId)); + } + } + + var entry = new Entry { SystemProperties = new SystemProperties @@ -226,18 +273,28 @@ private static Entry BuildEntryFromQualification(Qualification qualific additionalRequirements = new Dictionary { { Locale, qualification.AdditionalRequirements ?? "" } - } + }, + + additionalRequirementQuestions = new Dictionary>() + { + { Locale, addtionalRequirementQuestions } + }, + + ratioRequirements = new Dictionary>() + { + { Locale, ratioRequirements } + } } }; return entry; } - private static List GetQualificationsToAddOrUpdate() + private static List GetQualificationsToAddOrUpdate() { - var lines = ReadCsvFile("./csv/ey-quals-full-2024-updated.csv"); + var lines = ReadCsvFile("./csv/reference-upload-test.csv"); - var listObjResult = new List(); + var listObjResult = new List(); foreach (var t in lines) { @@ -249,17 +306,33 @@ private static List GetQualificationsToAddOrUpdate() var toWhichYear = t[3]; var qualificationNumber = t[6]; var additionalRequirements = t[7]; + var additionalRequirementQuestionString = t[8]; + var ratioRequirementsString = t[9]; + + string[] additionalRequirementQuestionsArray = []; + if (!string.IsNullOrEmpty(additionalRequirementQuestionString)) + { + additionalRequirementQuestionsArray = additionalRequirementQuestionString.Split(':'); + } + + string[] ratioRequirementsArray = []; + if (!string.IsNullOrEmpty(ratioRequirementsString)) + { + ratioRequirementsArray = ratioRequirementsString.Split(':'); + } - listObjResult.Add(new Qualification( - qualificationId, - qualificationName, - awardingOrganisationTitle, - qualificationLevel, - fromWhichYear, - toWhichYear, - qualificationNumber, - additionalRequirements - )); + listObjResult.Add(new QualificationUpload( + qualificationId, + qualificationName, + awardingOrganisationTitle, + qualificationLevel, + fromWhichYear, + toWhichYear, + qualificationNumber, + additionalRequirements, + additionalRequirementQuestionsArray, + ratioRequirementsArray + )); } return listObjResult; diff --git a/content/Dfe.EarlyYearsQualification.ContentUpload/QualificationUpload.cs b/content/Dfe.EarlyYearsQualification.ContentUpload/QualificationUpload.cs new file mode 100644 index 00000000..0c84064d --- /dev/null +++ b/content/Dfe.EarlyYearsQualification.ContentUpload/QualificationUpload.cs @@ -0,0 +1,30 @@ +namespace Dfe.EarlyYearsQualification.ContentUpload; + +public class QualificationUpload( + string qualificationId, + string qualificationName, + string awardingOrganisationTitle, + int qualificationLevel, + string? fromWhichYear, + string? toWhichYear, + string? qualificationNumber, + string? additionalRequirements, + string[]? additionalRequirementQuestions, + string[]? ratioRequirements) +{ + // Required Fields + public string QualificationId { get; } = qualificationId; + public string QualificationName { get; } = qualificationName; + public string AwardingOrganisationTitle { get; } = awardingOrganisationTitle; + public int QualificationLevel { get; } = qualificationLevel; + + // Optional Fields + public string? FromWhichYear { get; } = fromWhichYear; + public string? ToWhichYear { get; } = toWhichYear; + public string? QualificationNumber { get; } = qualificationNumber; + public string? AdditionalRequirements { get; } = additionalRequirements; + + public string[]? AdditionalRequirementQuestions { get; } = additionalRequirementQuestions; + + public string[]? RatioRequirements { get; } = ratioRequirements; +} \ No newline at end of file diff --git a/content/Dfe.EarlyYearsQualification.ContentUpload/csv/reference-upload-test.csv b/content/Dfe.EarlyYearsQualification.ContentUpload/csv/reference-upload-test.csv new file mode 100644 index 00000000..c0005bd0 --- /dev/null +++ b/content/Dfe.EarlyYearsQualification.ContentUpload/csv/reference-upload-test.csv @@ -0,0 +1,4 @@ +Qualification Ref,Qualification Level,From which date,To which date,Qualification Name,Awarding Organisation Title,Qualification Number,Notes/Additional Requirements,AdditionalRequirementQuestions,RatioRequirements +EYQ-600-SAM,5,Sep-24,,NCFE CACHE Level 5 Diploma for the Early Years Senior Practitioner (HTQ),NCFE,610/4163/4,"This qualification will replace the predecessor version NCFE CACHE Level 5 Diploma for the Early Years Senior Practitioner 603/3907/X, listed on the post-Sept 2014 tab, which will be withdrawn on 31st August 2024 and this new version launched on 1 September 2024. ","4Mx3V72twImd3VY5uNHiTG:7ymGLoQg58CBfQt91G3ez5","5flybYVibSWDwCdo9LB90b" +EYQ-601-SAM,5,Sep-24,,NCFE CACHE Level 5 Diploma for the Early Years Senior Practitioner (HTQ),NCFE,610/4163/4,"This qualification will replace the predecessor version NCFE CACHE Level 5 Diploma for the Early Years Senior Practitioner 603/3907/X, listed on the post-Sept 2014 tab, which will be withdrawn on 31st August 2024 and this new version launched on 1 September 2024. ","4Mx3V72twImd3VY5uNHiTG", +EYQ-602-SAM,5,Sep-24,,NCFE CACHE Level 5 Diploma for the Early Years Senior Practitioner (HTQ),NCFE,610/4163/4,"This qualification will replace the predecessor version NCFE CACHE Level 5 Diploma for the Early Years Senior Practitioner 603/3907/X, listed on the post-Sept 2014 tab, which will be withdrawn on 31st August 2024 and this new version launched on 1 September 2024. ",, \ No newline at end of file From 6f6c7bcfac19a170d3bd7b2cb047af343715b9d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 08:53:40 +0100 Subject: [PATCH 15/15] Bump contentful.csharp from 7.6.0 to 7.6.1 (#239) Bumps contentful.csharp from 7.6.0 to 7.6.1. --- updated-dependencies: - dependency-name: contentful.csharp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../Dfe.EarlyYearsQualification.Content.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dfe.EarlyYearsQualification.Content/Dfe.EarlyYearsQualification.Content.csproj b/src/Dfe.EarlyYearsQualification.Content/Dfe.EarlyYearsQualification.Content.csproj index 12d63b77..0d809ac9 100644 --- a/src/Dfe.EarlyYearsQualification.Content/Dfe.EarlyYearsQualification.Content.csproj +++ b/src/Dfe.EarlyYearsQualification.Content/Dfe.EarlyYearsQualification.Content.csproj @@ -7,7 +7,7 @@ - +