From e51a6df90e742714b1a9a7c86bdb43451c76182a Mon Sep 17 00:00:00 2001 From: Tom Whittington Date: Tue, 3 Dec 2024 09:17:05 +0000 Subject: [PATCH 1/8] chore: split QualificationDetailsController.cs into a new QualificationSearchController.cs --- .../ConfirmQualificationController.cs | 2 +- .../QualificationDetailsController.cs | 154 +-------- .../QualificationSearchController.cs | 144 ++++++++ .../Controllers/QuestionsController.cs | 23 +- .../Get.cshtml | 4 +- .../ConfirmQualificationControllerTests.cs | 12 +- .../QualificationDetailsControllerTests.cs | 319 ++++-------------- .../QualificationSearchControllerTests.cs | 199 +++++++++++ .../Controllers/QuestionsControllerTests.cs | 20 +- 9 files changed, 460 insertions(+), 417 deletions(-) create mode 100644 src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationSearchController.cs rename src/Dfe.EarlyYearsQualification.Web/Views/{QualificationDetails => QualificationSearch}/Get.cshtml (92%) create mode 100644 tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationSearchControllerTests.cs diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/ConfirmQualificationController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/ConfirmQualificationController.cs index fe5870f4..4af06768 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/ConfirmQualificationController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/ConfirmQualificationController.cs @@ -95,7 +95,7 @@ public async Task Confirm(ConfirmQualificationPageModel model) "QualificationDetails", new { qualificationId = model.QualificationId }), - _ => RedirectToAction("Get", "QualificationDetails") + _ => RedirectToAction("Get", "QualificationSearch") }; } diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs index f9ce4de6..33ad725f 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs @@ -1,4 +1,3 @@ -using System.Globalization; using Contentful.Core.Models; using Dfe.EarlyYearsQualification.Content.Constants; using Dfe.EarlyYearsQualification.Content.Entities; @@ -23,34 +22,6 @@ public class QualificationDetailsController( IUserJourneyCookieService userJourneyCookieService) : ServiceController { - [HttpGet] - public async Task Get() - { - var listPageContent = await contentService.GetQualificationListPage(); - if (listPageContent is null) - { - logger.LogError("No content for the qualification list page"); - return RedirectToAction("Index", "Error"); - } - - var model = await MapList(listPageContent, await GetFilteredQualifications()); - - return View(model); - } - - [HttpPost] - public IActionResult Refine(string? refineSearch) - { - if (!ModelState.IsValid) - { - logger.LogWarning($"Invalid model state in {nameof(QualificationDetailsController)} POST"); - } - - userJourneyCookieService.SetQualificationNameSearchCriteria(refineSearch ?? string.Empty); - - return RedirectToAction("Get"); - } - [HttpGet("qualification-details/{qualificationId}")] public async Task Index(string qualificationId) { @@ -106,33 +77,33 @@ public async Task Index(string qualificationId) { // If the qualification has no additional requirements then skip all checks and return. if (model.AdditionalRequirementAnswers == null) return (true, null); - + // If qualification contains the QTS question, check the answers if (QualificationContainsQtsQuestion(qualification)) { var qtsQuestion = qualification.AdditionalRequirementQuestions!.First(x => x.Sys.Id == AdditionalRequirementQuestions .QtsQuestion); - + if (UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, model.AdditionalRequirementAnswers)) { // Remove the additional requirements that they didn't answer following the bypass. model.AdditionalRequirementAnswers.RemoveAll(x => x.Question != qtsQuestion.Question); return (true, null); } - + // Check remaining questions var answersToCheck = new List(); answersToCheck.AddRange(model.AdditionalRequirementAnswers); // As L6 / L7 can potentially work at L3/2/unqualified, remove the Qts question and check answers answersToCheck.RemoveAll(x => x.Question == qtsQuestion.Question); - + // As we know that they didn't answer the Qts question, we need to show the L6 requirements by default. // Adding it here covers scenarios where they are OK for L2/3/Unqualified and just Unqualified. model.RatioRequirements.ShowRequirementsForLevel6ByDefault = true; - + if (!AnswersIndicateNotFullAndRelevant(answersToCheck)) return (true, null); - + // Answers indicate not full and relevant model.RatioRequirements = MarkAsNotFullAndRelevant(model.RatioRequirements); // Set any content for L6 @@ -147,10 +118,10 @@ public async Task Index(string qualificationId) qualification); model.RatioRequirements.RequirementsForLevel6 = await contentParser.ToHtml(requirementsForLevel6); model.RatioRequirements.ShowRequirementsForLevel6ByDefault = true; - + return (false, View(model)); } - + // If there is a mismatch between the questions answered, then clear the answers and navigate back to the additional requirements check page if (model.AdditionalRequirementAnswers.Count == 0 || model.AdditionalRequirementAnswers.Exists(answer => string.IsNullOrEmpty(answer.Answer))) @@ -201,9 +172,9 @@ private async Task CheckRatioRequirements(bool qualificationStartedBeforeSeptemb var additionalRequirementDetailPropertyToCheck = $"RequirementForLevel{qualification.QualificationLevel}{beforeOrAfter}2014"; - - if (qualification.IsAutomaticallyApprovedAtLevel6 || - (QualificationContainsQtsQuestion(qualification) + + if (qualification.IsAutomaticallyApprovedAtLevel6 || + (QualificationContainsQtsQuestion(qualification) && UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, model.AdditionalRequirementAnswers))) { // Check user against QTS criteria and swap to Qts Criteria if matches @@ -292,12 +263,12 @@ private static RatioRequirementModel MarkAsNotFullAndRelevant(RatioRequirementMo return model; } - + private static bool QualificationContainsQtsQuestion(Qualification qualification) { return qualification.AdditionalRequirementQuestions != null - && qualification.AdditionalRequirementQuestions.Any(x => x.Sys.Id == AdditionalRequirementQuestions - .QtsQuestion); + && qualification.AdditionalRequirementQuestions.Exists(x => x.Sys.Id == AdditionalRequirementQuestions + .QtsQuestion); } private static bool UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(Qualification qualification, @@ -308,7 +279,7 @@ private static bool UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(Qualif { return false; } - + var qtsQuestion = qualification.AdditionalRequirementQuestions!.First(x => x.Sys.Id == AdditionalRequirementQuestions .QtsQuestion); @@ -317,99 +288,6 @@ private static bool UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(Qualif var answerAsBool = userAnsweredQuestion.Answer == "yes"; return qtsQuestion.AnswerToBeFullAndRelevant == answerAsBool; } - - private async Task> GetFilteredQualifications() - { - var level = userJourneyCookieService.GetLevelOfQualification(); - var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); - var awardingOrganisation = userJourneyCookieService.GetAwardingOrganisation(); - var searchCriteria = userJourneyCookieService.GetSearchCriteria(); - - return await qualificationsRepository.Get(level, startDateMonth, startDateYear, - awardingOrganisation, searchCriteria); - } - - private async Task MapList(QualificationListPage content, - List? qualifications) - { - var basicQualificationsModels = GetBasicQualificationsModels(qualifications); - - var filterModel = GetFilterModel(content); - - return new QualificationListModel - { - BackButton = NavigationLinkMapper.Map(content.BackButton), - Filters = filterModel, - Header = content.Header, - SingleQualificationFoundText = content.SingleQualificationFoundText, - MultipleQualificationsFoundText = content.MultipleQualificationsFoundText, - PreSearchBoxContent = await contentParser.ToHtml(content.PreSearchBoxContent), - SearchButtonText = content.SearchButtonText, - LevelHeading = content.LevelHeading, - AwardingOrganisationHeading = content.AwardingOrganisationHeading, - PostSearchCriteriaContent = await contentParser.ToHtml(content.PostSearchCriteriaContent), - PostQualificationListContent = await contentParser.ToHtml(content.PostQualificationListContent), - SearchCriteriaHeading = content.SearchCriteriaHeading, - SearchCriteria = userJourneyCookieService.GetSearchCriteria(), - Qualifications = basicQualificationsModels.OrderBy(x => x.QualificationName).ToList(), - NoResultText = await contentParser.ToHtml(content.NoResultsText), - ClearSearchText = content.ClearSearchText, - NoQualificationsFoundText = content.NoQualificationsFoundText - }; - } - - private FilterModel GetFilterModel(QualificationListPage content) - { - var filterModel = new FilterModel - { - Country = userJourneyCookieService.GetWhereWasQualificationAwarded()!, - Level = content.AnyLevelHeading, - AwardingOrganisation = content.AnyAwardingOrganisationHeading - }; - - var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); - if (startDateMonth is not null && startDateYear is not null) - { - var date = new DateOnly(startDateYear.Value, startDateMonth.Value, 1); - filterModel.StartDate = $"{date.ToString("MMMM", CultureInfo.InvariantCulture)} {startDateYear.Value}"; - } - - var level = userJourneyCookieService.GetLevelOfQualification(); - if (level > 0) - { - filterModel.Level = $"Level {level}"; - } - - var awardingOrganisation = userJourneyCookieService.GetAwardingOrganisation(); - if (!string.IsNullOrEmpty(awardingOrganisation)) - { - filterModel.AwardingOrganisation = awardingOrganisation; - } - - return filterModel; - } - - private static List GetBasicQualificationsModels(List? qualifications) - { - var basicQualificationsModels = new List(); - - // ReSharper disable once InvertIf - if (qualifications is not null && qualifications.Count > 0) - { - foreach (var qualification in qualifications) - { - basicQualificationsModels.Add(new BasicQualificationModel - { - QualificationId = qualification.QualificationId, - QualificationLevel = qualification.QualificationLevel, - QualificationName = qualification.QualificationName, - AwardingOrganisationTitle = qualification.AwardingOrganisationTitle - }); - } - } - - return basicQualificationsModels; - } private async Task MapDetails( bool qualificationStartedBeforeSeptember2014, @@ -428,7 +306,7 @@ private async Task MapDetails( var dateOnly = new DateOnly(startYear.Value, startMonth.Value, 1); dateStarted = dateOnly.ToString("MMMM yyyy"); } - + var checkAnotherQualificationText = await contentParser.ToHtml(content.CheckAnotherQualificationText); var furtherInfoText = await contentParser.ToHtml(content.FurtherInfoText); var requirementsText = await contentParser.ToHtml(content.RequirementsText); diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationSearchController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationSearchController.cs new file mode 100644 index 00000000..7dd740da --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationSearchController.cs @@ -0,0 +1,144 @@ +using System.Globalization; +using Dfe.EarlyYearsQualification.Content.Entities; +using Dfe.EarlyYearsQualification.Content.RichTextParsing; +using Dfe.EarlyYearsQualification.Content.Services.Interfaces; +using Dfe.EarlyYearsQualification.Web.Attributes; +using Dfe.EarlyYearsQualification.Web.Controllers.Base; +using Dfe.EarlyYearsQualification.Web.Mappers; +using Dfe.EarlyYearsQualification.Web.Models.Content; +using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; +using Microsoft.AspNetCore.Mvc; + +namespace Dfe.EarlyYearsQualification.Web.Controllers; + +[Route("/qualifications")] +[RedirectIfDateMissing] +public class QualificationSearchController( + ILogger logger, + IQualificationsRepository qualificationsRepository, + IContentService contentService, + IGovUkContentParser contentParser, + IUserJourneyCookieService userJourneyCookieService) + : ServiceController +{ + [HttpGet] + public async Task Get() + { + var listPageContent = await contentService.GetQualificationListPage(); + if (listPageContent is null) + { + logger.LogError("No content for the qualification list page"); + return RedirectToAction("Index", "Error"); + } + + var model = await MapList(listPageContent, await GetFilteredQualifications()); + + return View(model); + } + + [HttpPost] + public IActionResult Refine(string? refineSearch) + { + if (!ModelState.IsValid) + { + logger.LogWarning($"Invalid model state in {nameof(QualificationSearchController)} POST"); + } + + userJourneyCookieService.SetQualificationNameSearchCriteria(refineSearch ?? string.Empty); + + return RedirectToAction("Get"); + } + + private async Task> GetFilteredQualifications() + { + var level = userJourneyCookieService.GetLevelOfQualification(); + var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); + var awardingOrganisation = userJourneyCookieService.GetAwardingOrganisation(); + var searchCriteria = userJourneyCookieService.GetSearchCriteria(); + + return await qualificationsRepository.Get(level, startDateMonth, startDateYear, + awardingOrganisation, searchCriteria); + } + + private async Task MapList(QualificationListPage content, + List? qualifications) + { + var basicQualificationsModels = GetBasicQualificationsModels(qualifications); + + var filterModel = GetFilterModel(content); + + return new QualificationListModel + { + BackButton = NavigationLinkMapper.Map(content.BackButton), + Filters = filterModel, + Header = content.Header, + SingleQualificationFoundText = content.SingleQualificationFoundText, + MultipleQualificationsFoundText = content.MultipleQualificationsFoundText, + PreSearchBoxContent = await contentParser.ToHtml(content.PreSearchBoxContent), + SearchButtonText = content.SearchButtonText, + LevelHeading = content.LevelHeading, + AwardingOrganisationHeading = content.AwardingOrganisationHeading, + PostSearchCriteriaContent = await contentParser.ToHtml(content.PostSearchCriteriaContent), + PostQualificationListContent = await contentParser.ToHtml(content.PostQualificationListContent), + SearchCriteriaHeading = content.SearchCriteriaHeading, + SearchCriteria = userJourneyCookieService.GetSearchCriteria(), + Qualifications = basicQualificationsModels.OrderBy(x => x.QualificationName).ToList(), + NoResultText = await contentParser.ToHtml(content.NoResultsText), + ClearSearchText = content.ClearSearchText, + NoQualificationsFoundText = content.NoQualificationsFoundText + }; + } + + private FilterModel GetFilterModel(QualificationListPage content) + { + var filterModel = new FilterModel + { + Country = userJourneyCookieService.GetWhereWasQualificationAwarded()!, + Level = content.AnyLevelHeading, + AwardingOrganisation = content.AnyAwardingOrganisationHeading + }; + + var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); + if (startDateMonth is not null && startDateYear is not null) + { + var date = new DateOnly(startDateYear.Value, startDateMonth.Value, 1); + filterModel.StartDate = $"{date.ToString("MMMM", CultureInfo.InvariantCulture)} {startDateYear.Value}"; + } + + var level = userJourneyCookieService.GetLevelOfQualification(); + if (level > 0) + { + filterModel.Level = $"Level {level}"; + } + + var awardingOrganisation = userJourneyCookieService.GetAwardingOrganisation(); + if (!string.IsNullOrEmpty(awardingOrganisation)) + { + filterModel.AwardingOrganisation = awardingOrganisation; + } + + return filterModel; + } + + private static List GetBasicQualificationsModels(List? qualifications) + { + var basicQualificationsModels = new List(); + + // ReSharper disable once InvertIf + if (qualifications is not null && qualifications.Count > 0) + { + foreach (var qualification in qualifications) + { + basicQualificationsModels.Add(new BasicQualificationModel + { + QualificationId = qualification.QualificationId, + QualificationLevel = qualification.QualificationLevel, + QualificationName = qualification.QualificationName, + AwardingOrganisationTitle = qualification.AwardingOrganisationTitle + }); + } + } + + return basicQualificationsModels; + } +} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs index 95c47e62..5f5f0f2b 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/QuestionsController.cs @@ -11,7 +11,6 @@ using Dfe.EarlyYearsQualification.Web.Models.Content.QuestionModels.Validators; using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; namespace Dfe.EarlyYearsQualification.Web.Controllers; @@ -218,7 +217,7 @@ public async Task WhatIsTheAwardingOrganisation(DropdownQuestionM userJourneyCookieService.SetAwardingOrganisation(model.NotInTheList ? string.Empty : model.SelectedValue!); userJourneyCookieService.SetAwardingOrganisationNotOnList(model.NotInTheList); - return RedirectToAction("Get", "QualificationDetails"); + return RedirectToAction("Get", "QualificationSearch"); } private async Task> GetFilteredQualifications() @@ -251,7 +250,7 @@ private async Task MapRadioModel(RadioQuestionModel model, R { var additionalInformationBody = await contentParser.ToHtml(question.AdditionalInformationBody); return RadioQuestionMapper.Map(model, question, actionName, controllerName, additionalInformationBody, - selectedAnswer); + selectedAnswer); } private async Task MapDateModel(DateQuestionModel model, DateQuestionPage question, @@ -264,21 +263,21 @@ private async Task MapDateModel(DateQuestionModel model, Date var bannerErrorText = validationResult is { BannerErrorMessages.Count: > 0 } ? string.Join("
", validationResult!.BannerErrorMessages) : null; - + var errorMessageText = validationResult is { ErrorMessages.Count: > 0 } - ? string.Join("
", validationResult!.ErrorMessages) - : null; - + ? string.Join("
", validationResult!.ErrorMessages) + : null; + var errorBannerLinkText = placeholderUpdater.Replace(bannerErrorText ?? question.ErrorBannerLinkText); - + var errorMessage = placeholderUpdater.Replace(errorMessageText ?? question.ErrorMessage); - + var additionalInformationBody = await contentParser.ToHtml(question.AdditionalInformationBody); return DateQuestionMapper.Map(model, question, actionName, controllerName, errorBannerLinkText, - errorMessage, additionalInformationBody, validationResult, selectedMonth, - selectedYear); + errorMessage, additionalInformationBody, validationResult, selectedMonth, + selectedYear); } private async Task MapDropdownModel(DropdownQuestionModel model, @@ -296,7 +295,7 @@ var uniqueAwardingOrganisations .Distinct() .Where(x => !Array.Exists(awardingOrganisationExclusions, x.Contains)) .Order(); - + var additionalInformationBodyHtml = await contentParser.ToHtml(question.AdditionalInformationBody); return DropdownQuestionMapper.Map(model, question, actionName, controllerName, uniqueAwardingOrganisations, diff --git a/src/Dfe.EarlyYearsQualification.Web/Views/QualificationDetails/Get.cshtml b/src/Dfe.EarlyYearsQualification.Web/Views/QualificationSearch/Get.cshtml similarity index 92% rename from src/Dfe.EarlyYearsQualification.Web/Views/QualificationDetails/Get.cshtml rename to src/Dfe.EarlyYearsQualification.Web/Views/QualificationSearch/Get.cshtml index f5e112a8..56e300d5 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Views/QualificationDetails/Get.cshtml +++ b/src/Dfe.EarlyYearsQualification.Web/Views/QualificationSearch/Get.cshtml @@ -53,14 +53,14 @@
- @using (Html.BeginForm("Refine", "QualificationDetails", FormMethod.Post, new { id="refine-search-form", @class = "float-left", style = "width:80%" })) + @using (Html.BeginForm("Refine", "QualificationSearch", FormMethod.Post, new { id="refine-search-form", @class = "float-left", style = "width:80%" })) { } - @using (Html.BeginForm("Refine", "QualificationDetails", FormMethod.Post, new { id="clear-search-form", @class = "float-left", style = "width:20%" })) + @using (Html.BeginForm("Refine", "QualificationSearch", FormMethod.Post, new { id="clear-search-form", @class = "float-left", style = "width:20%" })) { diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ConfirmQualificationControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ConfirmQualificationControllerTests.cs index b2bb6195..9f48ac46 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ConfirmQualificationControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/ConfirmQualificationControllerTests.cs @@ -116,7 +116,7 @@ public async Task Index_PageDetailsAndQualificationFound_MapsModelAndReturnsView var mockContentService = new Mock(); var confirmQualificationPageContent = GetConfirmQualificationPageContent(); - + mockContentService.Setup(x => x.GetConfirmQualificationPage()).ReturnsAsync(confirmQualificationPageContent); var qualification = new Qualification("Some ID", @@ -189,7 +189,7 @@ public async Task Index_PageDetailsAndQualificationFound_MapsModelAndReturnsView model.QualificationAwardingOrganisation.Should().Be(AwardingOrganisations.Ncfe); model.QualificationDateAdded.Should().Be("2014"); } - + [TestMethod] public async Task Post_InvalidModel_CantGetPageContent_LogsAndReturnsError() { @@ -311,7 +311,7 @@ public async Task Post_InvalidModel_BuildsModelWithHasErrorsAndReturns() var mockContentService = new Mock(); var confirmQualificationPageContent = GetConfirmQualificationPageContent(); - + mockContentService.Setup(x => x.GetConfirmQualificationPage()).ReturnsAsync(confirmQualificationPageContent); var qualification = new Qualification("Some ID", @@ -439,7 +439,7 @@ public async Task actionResult.RouteValues.Should().Contain("qualificationId", "TEST-123"); actionResult.RouteValues.Should().Contain("questionIndex", 1); } - + [TestMethod] public async Task Post_ValidModel_QualificationHasAdditionalRequirementsButAutomaticallyApprovedAtL6IsTrue_RedirectsToCheckAdditionalRequirements() @@ -578,7 +578,7 @@ public async Task Post_ValidModel_PassedAnythingButYes_RedirectsBackToTheQualifi var actionResult = (RedirectToActionResult)result; actionResult.ActionName.Should().Be("Get"); - actionResult.ControllerName.Should().Be("QualificationDetails"); + actionResult.ControllerName.Should().Be("QualificationSearch"); } [TestMethod] @@ -620,7 +620,7 @@ await controller.Confirm(new ConfirmQualificationPageModel mockUserJourneyService.Verify(x => x.ClearAdditionalQuestionsAnswers(), Times.Once); } - + private static ConfirmQualificationPage GetConfirmQualificationPageContent() { return new ConfirmQualificationPage diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs index 121b59b1..6521d036 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs @@ -692,26 +692,26 @@ public async Task Index_QualificationContainsQts_UserAnswerDoesntMatch_NotApprov const string requirementsForLevel = "Test"; var additionalRequirementQuestions = new List - { - new() - { - Sys = new SystemProperties - { - Id = AdditionalRequirementQuestions.QtsQuestion - }, - Question = "This is the Qts Question", - AnswerToBeFullAndRelevant = true - }, - new() - { - Sys = new SystemProperties - { - Id = "Some other Id" - }, - Question = "Have they got pediatric first aid?", - AnswerToBeFullAndRelevant = true - } - }; + { + new() + { + Sys = new SystemProperties + { + Id = AdditionalRequirementQuestions.QtsQuestion + }, + Question = "This is the Qts Question", + AnswerToBeFullAndRelevant = true + }, + new() + { + Sys = new SystemProperties + { + Id = "Some other Id" + }, + Question = "Have they got pediatric first aid?", + AnswerToBeFullAndRelevant = true + } + }; var ratioRequirements = new List { @@ -740,7 +740,7 @@ public async Task Index_QualificationContainsQts_UserAnswerDoesntMatch_NotApprov RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) } }; - + var listOfAdditionalReqsAnswered = new Dictionary { { "This is the Qts Question", "no" }, @@ -822,7 +822,7 @@ public async Task Index_QualificationContainsQts_UserAnswerDoesntMatch_NotApprov model.RatioRequirements.RequirementsForLevel6.Should().Be(requirementsForLevel); model.RatioRequirements.ShowRequirementsForLevel6ByDefault.Should().BeTrue(); } - + [TestMethod] public async Task Index_QualificationContainsQts_UserAnswerMatches_ApprovedAtL6() { @@ -838,26 +838,26 @@ public async Task Index_QualificationContainsQts_UserAnswerMatches_ApprovedAtL6( const string requirementsForLevel = "Test"; var additionalRequirementQuestions = new List - { - new() - { - Sys = new SystemProperties - { - Id = AdditionalRequirementQuestions.QtsQuestion - }, - Question = "This is the Qts Question", - AnswerToBeFullAndRelevant = true - }, - new() - { - Sys = new SystemProperties - { - Id = "Some other Id" - }, - Question = "Have they got pediatric first aid?", - AnswerToBeFullAndRelevant = true - } - }; + { + new() + { + Sys = new SystemProperties + { + Id = AdditionalRequirementQuestions.QtsQuestion + }, + Question = "This is the Qts Question", + AnswerToBeFullAndRelevant = true + }, + new() + { + Sys = new SystemProperties + { + Id = "Some other Id" + }, + Question = "Have they got pediatric first aid?", + AnswerToBeFullAndRelevant = true + } + }; var ratioRequirements = new List { @@ -886,7 +886,7 @@ public async Task Index_QualificationContainsQts_UserAnswerMatches_ApprovedAtL6( RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) } }; - + var listOfAdditionalReqsAnswered = new Dictionary { { "This is the Qts Question", "yes" }, @@ -968,7 +968,7 @@ public async Task Index_QualificationContainsQts_UserAnswerMatches_ApprovedAtL6( model.AdditionalRequirementAnswers.Should().NotBeNull(); model.AdditionalRequirementAnswers!.Count.Should().Be(1); } - + [TestMethod] public async Task Index_QualificationIsAutomaticallyApprovedAtL6_ApprovedAtL6() { @@ -982,7 +982,7 @@ public async Task Index_QualificationIsAutomaticallyApprovedAtL6_ApprovedAtL6() const int level = 6; const int startDateYear = 2022; const string requirementsForLevel = "Test"; - + var ratioRequirements = new List { new() @@ -1010,7 +1010,7 @@ public async Task Index_QualificationIsAutomaticallyApprovedAtL6_ApprovedAtL6() RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) } }; - + var listOfAdditionalReqsAnswered = new Dictionary { { "This is the Qts Question", "yes" }, @@ -1089,7 +1089,7 @@ public async Task Index_QualificationIsAutomaticallyApprovedAtL6_ApprovedAtL6() model.RatioRequirements.ApprovedForLevel6.Should().BeTrue(); model.RatioRequirements.ApprovedForUnqualified.Should().BeTrue(); } - + [TestMethod] public async Task Index_QualificationContainsQts_UserAnswerDoesntMatch_OnlyApprovedAtUnqualified() { @@ -1105,26 +1105,26 @@ public async Task Index_QualificationContainsQts_UserAnswerDoesntMatch_OnlyAppro const string requirementsForLevel = "Test"; var additionalRequirementQuestions = new List - { - new() - { - Sys = new SystemProperties - { - Id = AdditionalRequirementQuestions.QtsQuestion - }, - Question = "This is the Qts Question", - AnswerToBeFullAndRelevant = true - }, - new() - { - Sys = new SystemProperties - { - Id = "Some other Id" - }, - Question = "Have they got pediatric first aid?", - AnswerToBeFullAndRelevant = true - } - }; + { + new() + { + Sys = new SystemProperties + { + Id = AdditionalRequirementQuestions.QtsQuestion + }, + Question = "This is the Qts Question", + AnswerToBeFullAndRelevant = true + }, + new() + { + Sys = new SystemProperties + { + Id = "Some other Id" + }, + Question = "Have they got pediatric first aid?", + AnswerToBeFullAndRelevant = true + } + }; var ratioRequirements = new List { @@ -1153,7 +1153,7 @@ public async Task Index_QualificationContainsQts_UserAnswerDoesntMatch_OnlyAppro RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) } }; - + var listOfAdditionalReqsAnswered = new Dictionary { { "This is the Qts Question", "no" }, @@ -1294,9 +1294,9 @@ public async Task Index_BackToAdditionalQuestionsLinkIncludesQualificationId() var detailsPage = new DetailsPage { BackToConfirmAnswers = new NavigationLink - { - Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" - } + { + Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" + } }; mockRepository.Setup(x => x.GetById(qualificationId)) @@ -1333,181 +1333,4 @@ public async Task Index_BackToAdditionalQuestionsLinkIncludesQualificationId() model!.BackButton!.Href.Should().Be("/qualifications/check-additional-questions/eyq-145/confirm-answers"); } - - [TestMethod] - public async Task Get_ReturnsView() - { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - mockRepository - .Setup(x => - x.Get(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync([]); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - mockContentService.Setup(x => x.GetQualificationListPage()).ReturnsAsync(new QualificationListPage - { - BackButton = new NavigationLink - { - DisplayText = "TEST", - Href = "/", - OpenInNewTab = false - }, - Header = "TEST" - }); - - var result = await controller.Get(); - - result.Should().NotBeNull(); - result.Should().BeOfType(); - } - - [TestMethod] - public async Task Get_NoContent_LogsAndRedirectsToError() - { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - mockContentService.Setup(x => x.GetQualificationListPage()).ReturnsAsync(default(QualificationListPage)); - - var result = await controller.Get(); - - result.Should().BeOfType(); - - var actionResult = (RedirectToActionResult)result; - - actionResult.ActionName.Should().Be("Index"); - actionResult.ControllerName.Should().Be("Error"); - - mockLogger.VerifyError("No content for the qualification list page"); - } - - [TestMethod] - public void Refine_SaveQualificationName_RedirectsToGet() - { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = controller.Refine("Test"); - - result.Should().BeOfType(); - - var actionResult = (RedirectToActionResult)result; - - actionResult.ActionName.Should().Be("Get"); - mockUserJourneyCookieService.Verify(x => x.SetQualificationNameSearchCriteria("Test"), Times.Once); - } - - [TestMethod] - public void Refine_NullParam_RedirectsToGet() - { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = controller.Refine(null); - - result.Should().BeOfType(); - - var actionResult = (RedirectToActionResult)result; - - actionResult.ActionName.Should().Be("Get"); - mockUserJourneyCookieService.Verify(x => x.SetQualificationNameSearchCriteria(string.Empty), Times.Once); - } - - [TestMethod] - public void Refine_InvalidModel_LogsWarning() - { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - controller.ModelState.AddModelError("Key", "Error message"); - - controller.Refine(null); - - mockLogger.VerifyWarning($"Invalid model state in {nameof(QualificationDetailsController)} POST"); - } } \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationSearchControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationSearchControllerTests.cs new file mode 100644 index 00000000..655bfa18 --- /dev/null +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationSearchControllerTests.cs @@ -0,0 +1,199 @@ +using Dfe.EarlyYearsQualification.Content.Entities; +using Dfe.EarlyYearsQualification.Content.RichTextParsing; +using Dfe.EarlyYearsQualification.Content.Services.Interfaces; +using Dfe.EarlyYearsQualification.UnitTests.Extensions; +using Dfe.EarlyYearsQualification.Web.Controllers; +using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Dfe.EarlyYearsQualification.UnitTests.Controllers; + +[TestClass] +public class QualificationSearchControllerTests +{ + [TestMethod] + public async Task Get_ReturnsView() + { + var mockLogger = new Mock>(); + var mockRepository = new Mock(); + var mockContentService = new Mock(); + var mockContentParser = new Mock(); + var mockUserJourneyCookieService = new Mock(); + + mockRepository + .Setup(x => + x.Get(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync([]); + + var controller = + new QualificationSearchController(mockLogger.Object, + mockRepository.Object, + mockContentService.Object, + mockContentParser.Object, + mockUserJourneyCookieService.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + mockContentService.Setup(x => x.GetQualificationListPage()) + .ReturnsAsync(new QualificationListPage + { + BackButton = new NavigationLink + { + DisplayText = "TEST", + Href = "/", + OpenInNewTab = false + }, + Header = "TEST" + }); + + var result = await controller.Get(); + + result.Should().NotBeNull(); + result.Should().BeOfType(); + } + + [TestMethod] + public async Task Get_NoContent_LogsAndRedirectsToError() + { + var mockLogger = new Mock>(); + var mockRepository = new Mock(); + var mockContentService = new Mock(); + var mockContentParser = new Mock(); + var mockUserJourneyCookieService = new Mock(); + + var controller = + new QualificationSearchController(mockLogger.Object, + mockRepository.Object, + mockContentService.Object, + mockContentParser.Object, + mockUserJourneyCookieService.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + mockContentService.Setup(x => x.GetQualificationListPage()) + .ReturnsAsync(default(QualificationListPage)); + + var result = await controller.Get(); + + result.Should().BeOfType(); + + var actionResult = (RedirectToActionResult)result; + + actionResult.ActionName.Should().Be("Index"); + actionResult.ControllerName.Should().Be("Error"); + + mockLogger.VerifyError("No content for the qualification list page"); + } + + [TestMethod] + public void Refine_SaveQualificationName_RedirectsToGet() + { + var mockLogger = new Mock>(); + var mockRepository = new Mock(); + var mockContentService = new Mock(); + var mockContentParser = new Mock(); + var mockUserJourneyCookieService = new Mock(); + + var controller = + new QualificationSearchController(mockLogger.Object, + mockRepository.Object, + mockContentService.Object, + mockContentParser.Object, + mockUserJourneyCookieService.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + var result = controller.Refine("Test"); + + result.Should().BeOfType(); + + var actionResult = (RedirectToActionResult)result; + + actionResult.ActionName.Should().Be("Get"); + mockUserJourneyCookieService.Verify(x => x.SetQualificationNameSearchCriteria("Test"), + Times.Once); + } + + [TestMethod] + public void Refine_NullParam_RedirectsToGet() + { + var mockLogger = new Mock>(); + var mockRepository = new Mock(); + var mockContentService = new Mock(); + var mockContentParser = new Mock(); + var mockUserJourneyCookieService = new Mock(); + + var controller = + new QualificationSearchController(mockLogger.Object, + mockRepository.Object, + mockContentService.Object, + mockContentParser.Object, + mockUserJourneyCookieService.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + var result = controller.Refine(null); + + result.Should().BeOfType(); + + var actionResult = (RedirectToActionResult)result; + + actionResult.ActionName.Should().Be("Get"); + mockUserJourneyCookieService.Verify(x => x.SetQualificationNameSearchCriteria(string.Empty), + Times.Once); + } + + [TestMethod] + public void Refine_InvalidModel_LogsWarning() + { + var mockLogger = new Mock>(); + var mockRepository = new Mock(); + var mockContentService = new Mock(); + var mockContentParser = new Mock(); + var mockUserJourneyCookieService = new Mock(); + + var controller = + new QualificationSearchController(mockLogger.Object, + mockRepository.Object, + mockContentService.Object, + mockContentParser.Object, + mockUserJourneyCookieService.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + controller.ModelState.AddModelError("Key", "Error message"); + + controller.Refine(null); + + mockLogger + .VerifyWarning($"Invalid model state in {nameof(QualificationSearchController)} POST"); + } +} \ 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 1f5a9211..7dbb7fb0 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QuestionsControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QuestionsControllerTests.cs @@ -681,7 +681,7 @@ public async Task Post_WhatLevelIsTheQualification_Level2StartedBetween2014And20 resultType!.ActionName.Should().Be("QualificationsStartedBetweenSept2014AndAug2019"); resultType.ControllerName.Should().Be("Advice"); } - + [TestMethod] public async Task Post_WhatLevelIsTheQualification_Level7Post2014_ReturnsRedirectResponse() { @@ -692,28 +692,28 @@ public async Task Post_WhatLevelIsTheQualification_Level7Post2014_ReturnsRedirec var mockRepository = new Mock(); var mockQuestionModelValidator = new Mock(); var mockPlaceholderUpdater = new Mock(); - + mockUserJourneyCookieService.Setup(x => x.WasStartedOnOrAfterSeptember2014()) .Returns(true); - + var controller = new QuestionsController(mockLogger.Object, mockContentService.Object, mockContentParser.Object, mockUserJourneyCookieService.Object, mockRepository.Object, mockQuestionModelValidator.Object, mockPlaceholderUpdater.Object); - + var result = await controller.WhatLevelIsTheQualification(new RadioQuestionModel { Option = "7" }); - + result.Should().NotBeNull(); - + var resultType = result as RedirectToActionResult; resultType.Should().NotBeNull(); - + resultType!.ActionName.Should().Be("Level7QualificationPost2014"); resultType.ControllerName.Should().Be("Advice"); } - + [TestMethod] public async Task WhatIsTheAwardingOrganisation_ContentServiceReturnsNoQuestionPage_RedirectsToErrorPage() { @@ -1053,7 +1053,7 @@ public async Task resultType.Should().NotBeNull(); resultType!.ActionName.Should().Be("Get"); - resultType.ControllerName.Should().Be("QualificationDetails"); + resultType.ControllerName.Should().Be("QualificationSearch"); mockUserJourneyCookieService .Verify(x => x.SetAwardingOrganisation("Some Awarding Organisation"), Times.Once); @@ -1101,7 +1101,7 @@ public async Task resultType.Should().NotBeNull(); resultType!.ActionName.Should().Be("Get"); - resultType.ControllerName.Should().Be("QualificationDetails"); + resultType.ControllerName.Should().Be("QualificationSearch"); mockUserJourneyCookieService .Verify(x => x.SetAwardingOrganisation(string.Empty), Times.Once); From 4d96c51b6bc2074b810242e849423a3c15d7a8ef Mon Sep 17 00:00:00 2001 From: Tom Whittington Date: Tue, 3 Dec 2024 11:34:24 +0000 Subject: [PATCH 2/8] chore: refactor of QualificationSearchController.cs into QualificationSearchService.cs --- .../QualificationSearchController.cs | 116 +------- .../IQualificationSearchService.cs | 13 + .../QualificationSearchService.cs | 131 +++++++++ .../QualificationSearchControllerTests.cs | 205 ++++++-------- .../QualificationSearchServiceTests.cs | 259 ++++++++++++++++++ 5 files changed, 486 insertions(+), 238 deletions(-) create mode 100644 src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/IQualificationSearchService.cs create mode 100644 src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs create mode 100644 tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationSearchServiceTests.cs diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationSearchController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationSearchController.cs index 7dd740da..16b91fd5 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationSearchController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationSearchController.cs @@ -1,12 +1,6 @@ -using System.Globalization; -using Dfe.EarlyYearsQualification.Content.Entities; -using Dfe.EarlyYearsQualification.Content.RichTextParsing; -using Dfe.EarlyYearsQualification.Content.Services.Interfaces; using Dfe.EarlyYearsQualification.Web.Attributes; using Dfe.EarlyYearsQualification.Web.Controllers.Base; -using Dfe.EarlyYearsQualification.Web.Mappers; -using Dfe.EarlyYearsQualification.Web.Models.Content; -using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; +using Dfe.EarlyYearsQualification.Web.Services.QualificationSearch; using Microsoft.AspNetCore.Mvc; namespace Dfe.EarlyYearsQualification.Web.Controllers; @@ -15,25 +9,20 @@ namespace Dfe.EarlyYearsQualification.Web.Controllers; [RedirectIfDateMissing] public class QualificationSearchController( ILogger logger, - IQualificationsRepository qualificationsRepository, - IContentService contentService, - IGovUkContentParser contentParser, - IUserJourneyCookieService userJourneyCookieService) + IQualificationSearchService qualificationSearchService) : ServiceController { [HttpGet] public async Task Get() { - var listPageContent = await contentService.GetQualificationListPage(); - if (listPageContent is null) + var qualifications = await qualificationSearchService.GetQualifications(); + if (qualifications is null) { logger.LogError("No content for the qualification list page"); return RedirectToAction("Index", "Error"); } - var model = await MapList(listPageContent, await GetFilteredQualifications()); - - return View(model); + return View(qualifications); } [HttpPost] @@ -44,101 +33,8 @@ public IActionResult Refine(string? refineSearch) logger.LogWarning($"Invalid model state in {nameof(QualificationSearchController)} POST"); } - userJourneyCookieService.SetQualificationNameSearchCriteria(refineSearch ?? string.Empty); + qualificationSearchService.Refine(refineSearch ?? string.Empty); return RedirectToAction("Get"); } - - private async Task> GetFilteredQualifications() - { - var level = userJourneyCookieService.GetLevelOfQualification(); - var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); - var awardingOrganisation = userJourneyCookieService.GetAwardingOrganisation(); - var searchCriteria = userJourneyCookieService.GetSearchCriteria(); - - return await qualificationsRepository.Get(level, startDateMonth, startDateYear, - awardingOrganisation, searchCriteria); - } - - private async Task MapList(QualificationListPage content, - List? qualifications) - { - var basicQualificationsModels = GetBasicQualificationsModels(qualifications); - - var filterModel = GetFilterModel(content); - - return new QualificationListModel - { - BackButton = NavigationLinkMapper.Map(content.BackButton), - Filters = filterModel, - Header = content.Header, - SingleQualificationFoundText = content.SingleQualificationFoundText, - MultipleQualificationsFoundText = content.MultipleQualificationsFoundText, - PreSearchBoxContent = await contentParser.ToHtml(content.PreSearchBoxContent), - SearchButtonText = content.SearchButtonText, - LevelHeading = content.LevelHeading, - AwardingOrganisationHeading = content.AwardingOrganisationHeading, - PostSearchCriteriaContent = await contentParser.ToHtml(content.PostSearchCriteriaContent), - PostQualificationListContent = await contentParser.ToHtml(content.PostQualificationListContent), - SearchCriteriaHeading = content.SearchCriteriaHeading, - SearchCriteria = userJourneyCookieService.GetSearchCriteria(), - Qualifications = basicQualificationsModels.OrderBy(x => x.QualificationName).ToList(), - NoResultText = await contentParser.ToHtml(content.NoResultsText), - ClearSearchText = content.ClearSearchText, - NoQualificationsFoundText = content.NoQualificationsFoundText - }; - } - - private FilterModel GetFilterModel(QualificationListPage content) - { - var filterModel = new FilterModel - { - Country = userJourneyCookieService.GetWhereWasQualificationAwarded()!, - Level = content.AnyLevelHeading, - AwardingOrganisation = content.AnyAwardingOrganisationHeading - }; - - var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); - if (startDateMonth is not null && startDateYear is not null) - { - var date = new DateOnly(startDateYear.Value, startDateMonth.Value, 1); - filterModel.StartDate = $"{date.ToString("MMMM", CultureInfo.InvariantCulture)} {startDateYear.Value}"; - } - - var level = userJourneyCookieService.GetLevelOfQualification(); - if (level > 0) - { - filterModel.Level = $"Level {level}"; - } - - var awardingOrganisation = userJourneyCookieService.GetAwardingOrganisation(); - if (!string.IsNullOrEmpty(awardingOrganisation)) - { - filterModel.AwardingOrganisation = awardingOrganisation; - } - - return filterModel; - } - - private static List GetBasicQualificationsModels(List? qualifications) - { - var basicQualificationsModels = new List(); - - // ReSharper disable once InvertIf - if (qualifications is not null && qualifications.Count > 0) - { - foreach (var qualification in qualifications) - { - basicQualificationsModels.Add(new BasicQualificationModel - { - QualificationId = qualification.QualificationId, - QualificationLevel = qualification.QualificationLevel, - QualificationName = qualification.QualificationName, - AwardingOrganisationTitle = qualification.AwardingOrganisationTitle - }); - } - } - - return basicQualificationsModels; - } } \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/IQualificationSearchService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/IQualificationSearchService.cs new file mode 100644 index 00000000..5ef38a1d --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/IQualificationSearchService.cs @@ -0,0 +1,13 @@ +using Dfe.EarlyYearsQualification.Content.Entities; +using Dfe.EarlyYearsQualification.Web.Models.Content; + +namespace Dfe.EarlyYearsQualification.Web.Services.QualificationSearch; + +public interface IQualificationSearchService +{ + void Refine(string refineSearch); + Task GetQualifications(); + Task> GetFilteredQualifications(); + FilterModel GetFilterModel(QualificationListPage content); + List GetBasicQualificationsModels(List qualifications); +} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs new file mode 100644 index 00000000..65079fb7 --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs @@ -0,0 +1,131 @@ +using System.Globalization; +using Dfe.EarlyYearsQualification.Content.Entities; +using Dfe.EarlyYearsQualification.Content.RichTextParsing; +using Dfe.EarlyYearsQualification.Content.Services.Interfaces; +using Dfe.EarlyYearsQualification.Web.Mappers; +using Dfe.EarlyYearsQualification.Web.Models.Content; +using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; + +namespace Dfe.EarlyYearsQualification.Web.Services.QualificationSearch; + +public class QualificationSearchService( + IQualificationsRepository qualificationsRepository, + IContentService contentService, + IGovUkContentParser contentParser, + IUserJourneyCookieService userJourneyCookieService +) : IQualificationSearchService +{ + public void Refine(string refineSearch) + { + userJourneyCookieService.SetQualificationNameSearchCriteria(refineSearch); + } + + public async Task GetQualifications() + { + var qualificationListPage = await contentService.GetQualificationListPage(); + if (qualificationListPage is null) return null; + + var filteredQualifications = await GetFilteredQualifications(); + var model = await MapList(qualificationListPage, filteredQualifications); + return model; + } + + public async Task> GetFilteredQualifications() + { + var level = userJourneyCookieService.GetLevelOfQualification(); + var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); + var awardingOrganisation = userJourneyCookieService.GetAwardingOrganisation(); + var searchCriteria = userJourneyCookieService.GetSearchCriteria(); + + return await qualificationsRepository.Get( + level, + startDateMonth, + startDateYear, + awardingOrganisation, + searchCriteria + ); + } + + public async Task MapList(QualificationListPage content, List? qualifications) + { + var basicQualificationsModels = qualifications == null ? [] : GetBasicQualificationsModels(qualifications); + + var filterModel = GetFilterModel(content); + + return new QualificationListModel + { + BackButton = NavigationLinkMapper.Map(content.BackButton), + Filters = filterModel, + Header = content.Header, + SingleQualificationFoundText = content.SingleQualificationFoundText, + MultipleQualificationsFoundText = content.MultipleQualificationsFoundText, + PreSearchBoxContent = await contentParser.ToHtml(content.PreSearchBoxContent), + SearchButtonText = content.SearchButtonText, + LevelHeading = content.LevelHeading, + AwardingOrganisationHeading = content.AwardingOrganisationHeading, + PostSearchCriteriaContent = await contentParser.ToHtml(content.PostSearchCriteriaContent), + PostQualificationListContent = await contentParser.ToHtml(content.PostQualificationListContent), + SearchCriteriaHeading = content.SearchCriteriaHeading, + SearchCriteria = userJourneyCookieService.GetSearchCriteria(), + Qualifications = basicQualificationsModels.OrderBy(x => x.QualificationName).ToList(), + NoResultText = await contentParser.ToHtml(content.NoResultsText), + ClearSearchText = content.ClearSearchText, + NoQualificationsFoundText = content.NoQualificationsFoundText + }; + } + + public FilterModel GetFilterModel(QualificationListPage content) + { + var countryAwarded = userJourneyCookieService.GetWhereWasQualificationAwarded()!; + var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); + var level = userJourneyCookieService.GetLevelOfQualification(); + var awardingOrganisation = userJourneyCookieService.GetAwardingOrganisation(); + + var filterModel = new FilterModel + { + Country = countryAwarded, + Level = content.AnyLevelHeading, + AwardingOrganisation = content.AnyAwardingOrganisationHeading + }; + + if (startDateMonth is not null && startDateYear is not null) + { + var date = new DateOnly(startDateYear.Value, startDateMonth.Value, 1); + filterModel.StartDate = $"{date.ToString("MMMM", CultureInfo.InvariantCulture)} {startDateYear.Value}"; + } + + if (level > 0) + { + filterModel.Level = $"Level {level}"; + } + + if (!string.IsNullOrEmpty(awardingOrganisation)) + { + filterModel.AwardingOrganisation = awardingOrganisation; + } + + return filterModel; + } + + public List GetBasicQualificationsModels(List qualifications) + { + var basicQualificationsModels = new List(); + + if (qualifications is not null && qualifications.Count > 0) + { + basicQualificationsModels.AddRange( + qualifications.Select( + qualification => new BasicQualificationModel + { + QualificationId = qualification.QualificationId, + QualificationLevel = qualification.QualificationLevel, + QualificationName = qualification.QualificationName, + AwardingOrganisationTitle = qualification.AwardingOrganisationTitle + } + ) + ); + } + + return basicQualificationsModels; + } +} \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationSearchControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationSearchControllerTests.cs index 655bfa18..93d0ee11 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationSearchControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationSearchControllerTests.cs @@ -1,9 +1,7 @@ -using Dfe.EarlyYearsQualification.Content.Entities; -using Dfe.EarlyYearsQualification.Content.RichTextParsing; -using Dfe.EarlyYearsQualification.Content.Services.Interfaces; using Dfe.EarlyYearsQualification.UnitTests.Extensions; using Dfe.EarlyYearsQualification.Web.Controllers; -using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; +using Dfe.EarlyYearsQualification.Web.Models.Content; +using Dfe.EarlyYearsQualification.Web.Services.QualificationSearch; using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -15,48 +13,30 @@ namespace Dfe.EarlyYearsQualification.UnitTests.Controllers; [TestClass] public class QualificationSearchControllerTests { + private Mock> _mockLogger = new(); + private Mock _mockQualificationSearchService = new(); + + private QualificationSearchController GetSut() => new(_mockLogger.Object, + _mockQualificationSearchService.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + + [TestInitialize] + public void Initialize() + { + _mockLogger = new Mock>(); + _mockQualificationSearchService = new Mock(); + } + [TestMethod] public async Task Get_ReturnsView() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - mockRepository - .Setup(x => - x.Get(It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync([]); - - var controller = - new QualificationSearchController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - mockContentService.Setup(x => x.GetQualificationListPage()) - .ReturnsAsync(new QualificationListPage - { - BackButton = new NavigationLink - { - DisplayText = "TEST", - Href = "/", - OpenInNewTab = false - }, - Header = "TEST" - }); + _mockQualificationSearchService.Setup(o => o.GetQualifications()).ReturnsAsync(new QualificationListModel()); + var controller = GetSut(); var result = await controller.Get(); @@ -67,27 +47,7 @@ public async Task Get_ReturnsView() [TestMethod] public async Task Get_NoContent_LogsAndRedirectsToError() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationSearchController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - mockContentService.Setup(x => x.GetQualificationListPage()) - .ReturnsAsync(default(QualificationListPage)); + var controller = GetSut(); var result = await controller.Get(); @@ -98,102 +58,91 @@ public async Task Get_NoContent_LogsAndRedirectsToError() actionResult.ActionName.Should().Be("Index"); actionResult.ControllerName.Should().Be("Error"); - mockLogger.VerifyError("No content for the qualification list page"); + _mockLogger.VerifyError("No content for the qualification list page"); } [TestMethod] - public void Refine_SaveQualificationName_RedirectsToGet() + public async Task Get_Calls_Service_GetQualifications() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationSearchController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; + var controller = GetSut(); + await controller.Get(); - var result = controller.Refine("Test"); + _mockQualificationSearchService.Verify(x => x.GetQualifications(), Times.Once); + } + + [TestMethod] + public async Task Get_NullQualifications_LogsAndRedirectsToError() + { + _mockQualificationSearchService.Setup(o => o.GetQualifications()).ReturnsAsync((QualificationListModel)null!); + + var controller = GetSut(); + var result = await controller.Get(); result.Should().BeOfType(); var actionResult = (RedirectToActionResult)result; - actionResult.ActionName.Should().Be("Get"); - mockUserJourneyCookieService.Verify(x => x.SetQualificationNameSearchCriteria("Test"), - Times.Once); + actionResult.ActionName.Should().Be("Index"); + actionResult.ControllerName.Should().Be("Error"); + + _mockLogger.VerifyError("No content for the qualification list page"); + } + + [TestMethod] + public void Refine_WithSearch_CallsService_WithSearch() + { + const string search = "Test"; + var controller = GetSut(); + + controller.Refine(search); + + _mockQualificationSearchService.Verify(x => x.Refine(search), Times.Once); + } + + [TestMethod] + public void Refine_NullParam_CallsService_WithEmptyString() + { + var controller = GetSut(); + + controller.Refine(null); + + _mockQualificationSearchService.Verify(x => x.Refine(string.Empty), Times.Once); } [TestMethod] public void Refine_NullParam_RedirectsToGet() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationSearchController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; + var controller = GetSut(); var result = controller.Refine(null); result.Should().BeOfType(); - var actionResult = (RedirectToActionResult)result; + actionResult.ActionName.Should().Be("Get"); + } + + [TestMethod] + public void Refine_WithSearch_RedirectsToGet() + { + var controller = GetSut(); + var result = controller.Refine("Test"); + + result.Should().BeOfType(); + var actionResult = (RedirectToActionResult)result; actionResult.ActionName.Should().Be("Get"); - mockUserJourneyCookieService.Verify(x => x.SetQualificationNameSearchCriteria(string.Empty), - Times.Once); } [TestMethod] public void Refine_InvalidModel_LogsWarning() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationSearchController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; + var controller = GetSut(); controller.ModelState.AddModelError("Key", "Error message"); controller.Refine(null); - mockLogger + _mockLogger .VerifyWarning($"Invalid model state in {nameof(QualificationSearchController)} POST"); } } \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationSearchServiceTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationSearchServiceTests.cs new file mode 100644 index 00000000..26a260c3 --- /dev/null +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationSearchServiceTests.cs @@ -0,0 +1,259 @@ +using System.Globalization; +using Dfe.EarlyYearsQualification.Content.Entities; +using Dfe.EarlyYearsQualification.Content.RichTextParsing; +using Dfe.EarlyYearsQualification.Content.Services.Interfaces; +using Dfe.EarlyYearsQualification.Web.Services.QualificationSearch; +using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; +using FluentAssertions; +using Moq; + +namespace Dfe.EarlyYearsQualification.UnitTests.Services; + +[TestClass] +public class QualificationSearchServiceTests +{ + private Mock _mockRepository = new(); + private Mock _mockContentService = new(); + private Mock _mockContentParser = new(); + private Mock _mockUserJourneyCookieService = new(); + + private IQualificationSearchService GetSut() => new QualificationSearchService( + _mockRepository.Object, + _mockContentService.Object, + _mockContentParser.Object, + _mockUserJourneyCookieService.Object + ); + + [TestInitialize] + public void Initialize() + { + _mockRepository = new Mock(); + _mockContentService = new Mock(); + _mockContentParser = new Mock(); + _mockUserJourneyCookieService = new Mock(); + } + + [TestMethod] + public void Refine_Calls_CookieService_With_RefineSearch() + { + const string refineSearch = "Test"; + var sut = GetSut(); + + sut.Refine(refineSearch); + + _mockUserJourneyCookieService.Verify(o => o.SetQualificationNameSearchCriteria(refineSearch)); + } + + [TestMethod] + public async Task Get_Calls_ContentService_GetQualificationListPage() + { + var sut = GetSut(); + + await sut.GetQualifications(); + + _mockContentService.Verify(o => o.GetQualificationListPage(), Times.Once); + } + + [TestMethod] + public async Task Get_NullListPage_Returns_Null() + { + _mockContentService.Setup(o => o.GetQualificationListPage()).ReturnsAsync((QualificationListPage)null!); + var sut = GetSut(); + + var result = await sut.GetQualifications(); + + result.Should().BeNull(); + } + + [TestMethod] + public async Task GetQualifications_GotList_Calls_Repository_Get() + { + _mockContentService.Setup(o => o.GetQualificationListPage()).ReturnsAsync(new QualificationListPage()); + var sut = GetSut(); + await sut.GetQualifications(); + + _mockRepository.Verify(o => o.Get( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Once); + } + + [TestMethod] + public async Task GetFilteredQualifications_GetsDetails_From_CookieService() + { + var sut = GetSut(); + await sut.GetFilteredQualifications(); + + _mockUserJourneyCookieService.Verify(o => o.GetLevelOfQualification(), Times.Once); + _mockUserJourneyCookieService.Verify(o => o.GetWhenWasQualificationStarted(), Times.Once); + _mockUserJourneyCookieService.Verify(o => o.GetAwardingOrganisation(), Times.Once); + _mockUserJourneyCookieService.Verify(o => o.GetSearchCriteria(), Times.Once); + } + + [TestMethod] + public async Task GetFilteredQualifications_Calls_Repository_Get_WithCorrectParams() + { + const int levelOfQualification = 123; + const int startDateMonth = 3; + const int startDateYear = 2016; + const string awardingOrganisation = "awarding organisation"; + const string qualificationName = "qualification name"; + + _mockUserJourneyCookieService.Setup(o => o.GetLevelOfQualification()).Returns(levelOfQualification); + _mockUserJourneyCookieService.Setup(o => o.GetWhenWasQualificationStarted()).Returns((startDateMonth, startDateYear)); + _mockUserJourneyCookieService.Setup(o => o.GetAwardingOrganisation()).Returns(awardingOrganisation); + _mockUserJourneyCookieService.Setup(o => o.GetSearchCriteria()).Returns(qualificationName); + + var sut = GetSut(); + await sut.GetFilteredQualifications(); + + _mockRepository.Verify(o => o.Get( + levelOfQualification, + startDateMonth, + startDateYear, + awardingOrganisation, + qualificationName + ), Times.Once); + } + + [TestMethod] + public void GetBasicQualificationModels_Returns_Correct_Models() + { + var qualifications = new List + { + new("qual-1", "qual-name-1", "org-1", 1), + new("qual-2", "qual-name-2", "org-2", 2), + new("qual-3", "qual-name-3", "org-2", 3), + }; + + var sut = GetSut(); + + var result = sut.GetBasicQualificationsModels(qualifications); + + result.Count.Should().Be(qualifications.Count); + + for (int i = 0; i < result.Count; i++) + { + var thisResult = result[i]; + var expectedResult = qualifications[i]; + + thisResult.QualificationId.Should().Be(expectedResult.QualificationId); + thisResult.QualificationName.Should().Be(expectedResult.QualificationName); + thisResult.AwardingOrganisationTitle.Should().Be(expectedResult.AwardingOrganisationTitle); + thisResult.QualificationLevel.Should().Be(expectedResult.QualificationLevel); + } + } + + [TestMethod] + public void GetFilterModel_Calls_CookieService() + { + var qualificationListPage = new QualificationListPage(); + var sut = GetSut(); + sut.GetFilterModel(qualificationListPage); + + _mockUserJourneyCookieService.Verify(o => o.GetWhereWasQualificationAwarded(), Times.Once); + _mockUserJourneyCookieService.Verify(o => o.GetWhenWasQualificationStarted(), Times.Once); + _mockUserJourneyCookieService.Verify(o => o.GetLevelOfQualification(), Times.Once); + _mockUserJourneyCookieService.Verify(o => o.GetAwardingOrganisation(), Times.Once); + } + + [TestMethod] + public void GetFilterModel_BasicModel_IsCorrect() + { + const string country = "where awarded"; + const string anyLevelHeading = "any heading"; + const string anyAwardingOrganisation = "any awarding organisation"; + + _mockUserJourneyCookieService.Setup(o => o.GetWhereWasQualificationAwarded()).Returns(country); + + var qualificationListPage = new QualificationListPage + { + AnyLevelHeading = anyLevelHeading, + AnyAwardingOrganisationHeading = anyAwardingOrganisation, + }; + var sut = GetSut(); + var result = sut.GetFilterModel(qualificationListPage); + + result.Country.Should().Be(country); + result.Level.Should().Be(anyLevelHeading); + result.AwardingOrganisation.Should().Be(anyAwardingOrganisation); + } + + [TestMethod] + public void GetFilterModel_GotStartDates_Sets_StartDate() + { + const int startDateMonth = 3; + const int startDateYear = 2016; + var qualificationListPage = new QualificationListPage(); + _mockUserJourneyCookieService.Setup(o => o.GetWhenWasQualificationStarted()).Returns((startDateMonth, startDateYear)); + var sut = GetSut(); + var result = sut.GetFilterModel(qualificationListPage); + + var expectedDt = new DateOnly(startDateYear, startDateMonth, 1); + var expectedStartDate = $"{expectedDt.ToString("MMMM", CultureInfo.InvariantCulture)} {startDateYear}"; + + result.StartDate.Should().Be(expectedStartDate); + } + + [TestMethod] + public void GetFilterModel_NotGotStartDates_Ignores_StartDate() + { + var qualificationListPage = new QualificationListPage(); + _mockUserJourneyCookieService.Setup(o => o.GetWhenWasQualificationStarted()).Returns((null, null)); + var sut = GetSut(); + var result = sut.GetFilterModel(qualificationListPage); + + result.StartDate.Should().Be(string.Empty); + } + + [TestMethod] + public void GetFilterModel_GotLevel_Sets_Level() + { + const int level = 3; + var qualificationListPage = new QualificationListPage(); + _mockUserJourneyCookieService.Setup(o => o.GetLevelOfQualification()).Returns(level); + var sut = GetSut(); + var result = sut.GetFilterModel(qualificationListPage); + + var expectedLevel = $"Level {level}"; + + result.Level.Should().Be(expectedLevel); + } + + [TestMethod] + public void GetFilterModel_NotGotLevel_Ignores_Level() + { + var qualificationListPage = new QualificationListPage(); + _mockUserJourneyCookieService.Setup(o => o.GetLevelOfQualification()).Returns((int?)null); + var sut = GetSut(); + var result = sut.GetFilterModel(qualificationListPage); + + result.Level.Should().Be(string.Empty); + } + + [TestMethod] + public void GetFilterModel_GotAwardingOrganisation_Sets_AwardingOrganisation() + { + const string awardingOrganisation = "awarding organisation"; + var qualificationListPage = new QualificationListPage(); + _mockUserJourneyCookieService.Setup(o => o.GetAwardingOrganisation()).Returns(awardingOrganisation); + var sut = GetSut(); + var result = sut.GetFilterModel(qualificationListPage); + + result.AwardingOrganisation.Should().Be(awardingOrganisation); + } + + [TestMethod] + public void GetFilterModel_NotGotAwardingOrganisation_Ignores_AwardingOrganisation() + { + var qualificationListPage = new QualificationListPage(); + _mockUserJourneyCookieService.Setup(o => o.GetAwardingOrganisation()).Returns((string?)null); + var sut = GetSut(); + var result = sut.GetFilterModel(qualificationListPage); + + result.AwardingOrganisation.Should().Be(string.Empty); + } +} \ No newline at end of file From a73bceca100ae70c83d3a7c4d259404edc1bc9d5 Mon Sep 17 00:00:00 2001 From: Tom Whittington Date: Tue, 3 Dec 2024 15:25:58 +0000 Subject: [PATCH 3/8] chore: cleanup of BasicQualificationModel.cs --- .../Models/Content/BasicQualificationModel.cs | 20 +++++++++++++--- .../Program.cs | 2 ++ .../IQualificationSearchService.cs | 2 +- .../QualificationSearchService.cs | 24 ++++--------------- .../QualificationSearchServiceTests.cs | 21 ++++++++-------- 5 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/Dfe.EarlyYearsQualification.Web/Models/Content/BasicQualificationModel.cs b/src/Dfe.EarlyYearsQualification.Web/Models/Content/BasicQualificationModel.cs index b4f91ba2..ebb7e812 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Models/Content/BasicQualificationModel.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Models/Content/BasicQualificationModel.cs @@ -1,12 +1,26 @@ +using Dfe.EarlyYearsQualification.Content.Entities; + namespace Dfe.EarlyYearsQualification.Web.Models.Content; public class BasicQualificationModel { + protected BasicQualificationModel() + { + } + + public BasicQualificationModel(Qualification qualification) + { + QualificationId = qualification.QualificationId; + QualificationLevel = qualification.QualificationLevel; + QualificationName = qualification.QualificationName; + AwardingOrganisationTitle = qualification.AwardingOrganisationTitle; + } + public string QualificationId { get; init; } = string.Empty; - + public string QualificationName { get; init; } = string.Empty; - + public string AwardingOrganisationTitle { get; init; } = string.Empty; - + public int QualificationLevel { get; init; } } \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Program.cs b/src/Dfe.EarlyYearsQualification.Web/Program.cs index 1b098064..58a91941 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Program.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Program.cs @@ -12,6 +12,7 @@ using Dfe.EarlyYearsQualification.Web.Services.Cookies; using Dfe.EarlyYearsQualification.Web.Services.CookiesPreferenceService; using Dfe.EarlyYearsQualification.Web.Services.DatesAndTimes; +using Dfe.EarlyYearsQualification.Web.Services.QualificationSearch; using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; using GovUk.Frontend.AspNetCore; using Microsoft.ApplicationInsights.AspNetCore.Extensions; @@ -99,6 +100,7 @@ builder.Services.AddTransient(); } +builder.Services.AddTransient(); builder.Services.AddModelRenderers(); builder.Services.AddSingleton(); builder.Services.AddScoped(); diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/IQualificationSearchService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/IQualificationSearchService.cs index 5ef38a1d..1740c707 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/IQualificationSearchService.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/IQualificationSearchService.cs @@ -7,7 +7,7 @@ public interface IQualificationSearchService { void Refine(string refineSearch); Task GetQualifications(); + Task MapList(QualificationListPage content, List? qualifications); Task> GetFilteredQualifications(); FilterModel GetFilterModel(QualificationListPage content); - List GetBasicQualificationsModels(List qualifications); } \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs index 65079fb7..62447385 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs @@ -67,7 +67,7 @@ public async Task MapList(QualificationListPage content, PostQualificationListContent = await contentParser.ToHtml(content.PostQualificationListContent), SearchCriteriaHeading = content.SearchCriteriaHeading, SearchCriteria = userJourneyCookieService.GetSearchCriteria(), - Qualifications = basicQualificationsModels.OrderBy(x => x.QualificationName).ToList(), + Qualifications = basicQualificationsModels, NoResultText = await contentParser.ToHtml(content.NoResultsText), ClearSearchText = content.ClearSearchText, NoQualificationsFoundText = content.NoQualificationsFoundText @@ -109,23 +109,9 @@ public FilterModel GetFilterModel(QualificationListPage content) public List GetBasicQualificationsModels(List qualifications) { - var basicQualificationsModels = new List(); - - if (qualifications is not null && qualifications.Count > 0) - { - basicQualificationsModels.AddRange( - qualifications.Select( - qualification => new BasicQualificationModel - { - QualificationId = qualification.QualificationId, - QualificationLevel = qualification.QualificationLevel, - QualificationName = qualification.QualificationName, - AwardingOrganisationTitle = qualification.AwardingOrganisationTitle - } - ) - ); - } - - return basicQualificationsModels; + if (qualifications is null) return []; + return qualifications.Select(qualification => new BasicQualificationModel(qualification)) + .OrderBy(qualification => qualification.QualificationName) + .ToList(); } } \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationSearchServiceTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationSearchServiceTests.cs index 26a260c3..211f002d 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationSearchServiceTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationSearchServiceTests.cs @@ -18,11 +18,11 @@ public class QualificationSearchServiceTests private Mock _mockUserJourneyCookieService = new(); private IQualificationSearchService GetSut() => new QualificationSearchService( - _mockRepository.Object, - _mockContentService.Object, - _mockContentParser.Object, - _mockUserJourneyCookieService.Object - ); + _mockRepository.Object, + _mockContentService.Object, + _mockContentParser.Object, + _mockUserJourneyCookieService.Object + ); [TestInitialize] public void Initialize() @@ -120,7 +120,7 @@ public async Task GetFilteredQualifications_Calls_Repository_Get_WithCorrectPara } [TestMethod] - public void GetBasicQualificationModels_Returns_Correct_Models() + public async Task MapList_Qualifications_Returns_Correct_List() { var qualifications = new List { @@ -131,13 +131,14 @@ public void GetBasicQualificationModels_Returns_Correct_Models() var sut = GetSut(); - var result = sut.GetBasicQualificationsModels(qualifications); + var result = await sut.MapList(new QualificationListPage(), qualifications); - result.Count.Should().Be(qualifications.Count); + var quals = result.Qualifications; + quals.Count.Should().Be(qualifications.Count); - for (int i = 0; i < result.Count; i++) + for (int i = 0; i < quals.Count; i++) { - var thisResult = result[i]; + var thisResult = quals[i]; var expectedResult = qualifications[i]; thisResult.QualificationId.Should().Be(expectedResult.QualificationId); From 915b319890b382dcc760ab5d4172523d60933b09 Mon Sep 17 00:00:00 2001 From: Tom Whittington Date: Tue, 3 Dec 2024 15:56:56 +0000 Subject: [PATCH 4/8] chore: removed duplicate method --- .../QualificationDetailsController.cs | 2 +- .../IUserJourneyCookieService.cs | 1 - .../UserJourneyCookieService.cs | 14 -------------- .../QualificationDetailsControllerTests.cs | 16 +++++++--------- 4 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs index 5939eede..c370220a 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs @@ -80,7 +80,7 @@ public async Task Index(string qualificationId) private async Task QualificationLevel3OrAboveMightBeRelevantAtLevel2(QualificationDetailsModel model, Qualification qualification) { // Check if the qualification is not full and relevant and was started between Sept 2014 and Aug 2019 and is above a level 2 qualification - if (model.RatioRequirements.IsNotFullAndRelevant && userJourneyCookieService.WasStartedBetweenSept2014AndAug2019() && qualification.QualificationLevel > 2) + if (model.RatioRequirements.IsNotFullAndRelevant && userJourneyCookieService.WasStartedBetweenSeptember2014AndAugust2019() && qualification.QualificationLevel > 2) { await QualIsNotLevel2NotApprovedAndStartedBetweenSept2014AndAug2019(model, qualification); } diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/UserJourneyCookieService/IUserJourneyCookieService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/UserJourneyCookieService/IUserJourneyCookieService.cs index e1c79b78..85c31cdf 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Services/UserJourneyCookieService/IUserJourneyCookieService.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Services/UserJourneyCookieService/IUserJourneyCookieService.cs @@ -24,5 +24,4 @@ public interface IUserJourneyCookieService Dictionary? GetAdditionalQuestionsAnswers(); bool UserHasAnsweredAdditionalQuestions(); YesOrNo GetQualificationWasSelectedFromList(); - bool WasStartedBetweenSept2014AndAug2019(); } \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/UserJourneyCookieService/UserJourneyCookieService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/UserJourneyCookieService/UserJourneyCookieService.cs index f5da5b64..f5e0623f 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Services/UserJourneyCookieService/UserJourneyCookieService.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Services/UserJourneyCookieService/UserJourneyCookieService.cs @@ -210,20 +210,6 @@ public bool WasStartedOnOrAfterSeptember2014() return date >= new DateOnly(2014, 9, 1); } - public bool WasStartedBetweenSept2014AndAug2019() - { - var (startDateMonth, startDateYear) = GetWhenWasQualificationStarted(); - - if (startDateMonth is null || startDateYear is null) - { - throw new - InvalidOperationException("Unable to determine whether qualification was started on or after 09-2014"); - } - - var date = new DateOnly(startDateYear.Value, startDateMonth.Value, 1); - return date >= new DateOnly(2014, 9, 1) && date <= new DateOnly(2019, 8,31); - } - public int? GetLevelOfQualification() { lock (_lockObject) diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs index 5b87d193..6abeff13 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs @@ -569,7 +569,7 @@ public async Task Index_BuildUpRatioRequirements_CantFindLevel6_LogsAndThrows() await Assert.ThrowsExceptionAsync(() => controller.Index(qualificationId)); mockLogger.VerifyError($"Could not find property: FullAndRelevantForLevel{level}After2014 within Level 6 Ratio Requirements for qualification: {qualificationId}"); } - + [TestMethod] [DataRow(9, 2014, 3)] [DataRow(8, 2019, 3)] @@ -664,8 +664,7 @@ public async Task Index_BuildUpRatioRequirements_NotRelevantButLevel3OrAboveStar }); mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((startMonth, startYear)); - mockUserJourneyCookieService.Setup(x => x.WasStartedBetweenSept2014AndAug2019()) - .Returns(true); + mockUserJourneyCookieService.Setup(x => x.WasStartedBetweenSeptember2014AndAugust2019()).Returns(true); var controller = new QualificationDetailsController(mockLogger.Object, @@ -717,7 +716,7 @@ public async Task const int level = 6; const int startMonth = 6; const int startYear = 2016; - + var additionalRequirementQuestions = new List { new() @@ -730,13 +729,13 @@ public async Task AnswerToBeFullAndRelevant = true } }; - + //Answer here makes this qual not full and relevant var listOfAdditionalReqsAnswered = new Dictionary { { "Have they got pediatric first aid?", "no" } }; - + var ratioRequirements = new List { new() @@ -791,9 +790,8 @@ public async Task }); mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((startMonth, startYear)); - mockUserJourneyCookieService.Setup(x => x.WasStartedBetweenSept2014AndAug2019()) - .Returns(true); - + mockUserJourneyCookieService.Setup(x => x.WasStartedBetweenSeptember2014AndAugust2019()).Returns(true); + mockUserJourneyCookieService.Setup(x => x.GetAdditionalQuestionsAnswers()) .Returns(listOfAdditionalReqsAnswered); From 09ff0d0a2a7dbcd9919b1e52d2c72d6a32eb3104 Mon Sep 17 00:00:00 2001 From: Tom Whittington Date: Mon, 9 Dec 2024 17:55:06 +0000 Subject: [PATCH 5/8] chore: rejigged all tests to fit new service structure --- .../QualificationDetailsController.cs | 404 +--- .../Program.cs | 2 + .../IQualificationDetailsService.cs | 24 + .../QualificationDetailsService.cs | 322 +++ .../Controllers/HomeControllerTests.cs | 5 - .../QualificationDetailsControllerTests.cs | 1732 +++-------------- .../Extensions/ActionResultExtensions.cs | 13 + .../GlobalUsings.cs | 7 +- .../QualificationDetailsServiceTests.cs | 765 ++++++++ 9 files changed, 1427 insertions(+), 1847 deletions(-) create mode 100644 src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/IQualificationDetailsService.cs create mode 100644 src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/QualificationDetailsService.cs create mode 100644 tests/Dfe.EarlyYearsQualification.UnitTests/Extensions/ActionResultExtensions.cs create mode 100644 tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationDetailsServiceTests.cs diff --git a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs index c370220a..f50a0c12 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Controllers/QualificationDetailsController.cs @@ -1,14 +1,9 @@ -using Contentful.Core.Models; using Dfe.EarlyYearsQualification.Content.Constants; using Dfe.EarlyYearsQualification.Content.Entities; -using Dfe.EarlyYearsQualification.Content.RichTextParsing; -using Dfe.EarlyYearsQualification.Content.Services.Interfaces; using Dfe.EarlyYearsQualification.Web.Attributes; using Dfe.EarlyYearsQualification.Web.Controllers.Base; -using Dfe.EarlyYearsQualification.Web.Mappers; -using Dfe.EarlyYearsQualification.Web.Models; using Dfe.EarlyYearsQualification.Web.Models.Content; -using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; +using Dfe.EarlyYearsQualification.Web.Services.QualificationDetails; using Microsoft.AspNetCore.Mvc; namespace Dfe.EarlyYearsQualification.Web.Controllers; @@ -17,420 +12,93 @@ namespace Dfe.EarlyYearsQualification.Web.Controllers; [RedirectIfDateMissing] public class QualificationDetailsController( ILogger logger, - IQualificationsRepository qualificationsRepository, - IContentService contentService, - IGovUkContentParser contentParser, - IUserJourneyCookieService userJourneyCookieService) + IQualificationDetailsService qualificationDetailsService +) : ServiceController { [HttpGet("qualification-details/{qualificationId}")] public async Task Index(string qualificationId) { - if (!ModelState.IsValid || string.IsNullOrEmpty(qualificationId)) - { - return BadRequest(); - } + if (!ModelState.IsValid || string.IsNullOrEmpty(qualificationId)) return BadRequest(); + if (!qualificationDetailsService.HasStartDate()) return RedirectToAction("Index", "Home"); - var detailsPageContent = await contentService.GetDetailsPage(); + var detailsPageContent = await qualificationDetailsService.GetDetailsPage(); if (detailsPageContent is null) { logger.LogError("No content for the qualification details page"); return RedirectToAction("Index", "Error"); } - var qualification = await qualificationsRepository.GetById(qualificationId); + var qualification = await qualificationDetailsService.GetQualification(qualificationId); if (qualification is null) { - var loggedQualificationId = qualificationId.Replace(Environment.NewLine, ""); - logger.LogError("Could not find details for qualification with ID: {QualificationId}", - loggedQualificationId); - + logger.LogError("Could not find details for qualification with ID: {QualificationId}", qualificationId.Replace(Environment.NewLine, "")); return RedirectToAction("Index", "Error"); } - // Grab the start date of the qualification - var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); - - // Check that the user has chosen a start date, if not then redirect them back to the start of the journey - if (startDateYear == null || startDateMonth == null) - { - return RedirectToAction("Index", "Home"); - } - - var qualificationStartedBeforeSeptember2014 = userJourneyCookieService.WasStartedBeforeSeptember2014(); - - var model = await MapDetails(qualificationStartedBeforeSeptember2014, qualification, detailsPageContent); + var model = await qualificationDetailsService.MapDetails(qualification, detailsPageContent); var validateAdditionalRequirementQuestions = await ValidateAdditionalQuestions(model, qualification); if (!validateAdditionalRequirementQuestions.isValid) { - await QualificationLevel3OrAboveMightBeRelevantAtLevel2(model, qualification); + await qualificationDetailsService.QualificationLevel3OrAboveMightBeRelevantAtLevel2(model, qualification); return validateAdditionalRequirementQuestions.actionResult!; } - // If all the additional requirement checks pass, then we can go to check each level individually - await CheckRatioRequirements(qualificationStartedBeforeSeptember2014, qualification, model); - - await QualificationLevel3OrAboveMightBeRelevantAtLevel2(model, qualification); + await qualificationDetailsService.CheckRatioRequirements(qualification, model); + await qualificationDetailsService.QualificationLevel3OrAboveMightBeRelevantAtLevel2(model, qualification); return View(model); } - private async Task QualificationLevel3OrAboveMightBeRelevantAtLevel2(QualificationDetailsModel model, Qualification qualification) - { - // Check if the qualification is not full and relevant and was started between Sept 2014 and Aug 2019 and is above a level 2 qualification - if (model.RatioRequirements.IsNotFullAndRelevant && userJourneyCookieService.WasStartedBetweenSeptember2014AndAugust2019() && qualification.QualificationLevel > 2) - { - await QualIsNotLevel2NotApprovedAndStartedBetweenSept2014AndAug2019(model, qualification); - } - } - - private async Task<(bool isValid, IActionResult? actionResult)> ValidateAdditionalQuestions(QualificationDetailsModel model, Qualification qualification) + private async Task<(bool isValid, IActionResult? actionResult)> ValidateAdditionalQuestions(QualificationDetailsModel details, Qualification qualification) { // If the qualification has no additional requirements then skip all checks and return. - if (model.AdditionalRequirementAnswers == null) return (true, null); + if (details.AdditionalRequirementAnswers == null) return (true, null); // If qualification contains the QTS question, check the answers - if (QualificationContainsQtsQuestion(qualification)) - { - var qtsQuestion = - qualification.AdditionalRequirementQuestions!.First(x => x.Sys.Id == AdditionalRequirementQuestions - .QtsQuestion); - - if (UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, model.AdditionalRequirementAnswers)) - { - // Remove the additional requirements that they didn't answer following the bypass. - model.AdditionalRequirementAnswers.RemoveAll(x => x.Question != qtsQuestion.Question); - return (true, null); - } - - // Check remaining questions - var answersToCheck = new List(); - answersToCheck.AddRange(model.AdditionalRequirementAnswers); - // As L6 / L7 can potentially work at L3/2/unqualified, remove the Qts question and check answers - answersToCheck.RemoveAll(x => x.Question == qtsQuestion.Question); - - // As we know that they didn't answer the Qts question, we need to show the L6 requirements by default. - // Adding it here covers scenarios where they are OK for L2/3/Unqualified and just Unqualified. - model.RatioRequirements.ShowRequirementsForLevel6ByDefault = true; - - if (!AnswersIndicateNotFullAndRelevant(answersToCheck)) return (true, null); - - // Answers indicate not full and relevant - model.RatioRequirements = MarkAsNotFullAndRelevant(model.RatioRequirements); - // Set any content for L6 - var qualificationStartedBefore2014 = userJourneyCookieService.WasStartedBeforeSeptember2014(); - var beforeOrAfter = - qualificationStartedBefore2014 - ? "Before" - : "After"; - var additionalRequirementDetailPropertyToCheck = $"RequirementForLevel{qualification.QualificationLevel}{beforeOrAfter}2014"; - var requirementsForLevel6 = GetRatioProperty(additionalRequirementDetailPropertyToCheck, - RatioRequirements.Level6RatioRequirementName, - qualification); - model.RatioRequirements.RequirementsForLevel6 = await contentParser.ToHtml(requirementsForLevel6); - model.RatioRequirements.ShowRequirementsForLevel6ByDefault = true; - - return (false, View(model)); - } + if (qualificationDetailsService.QualificationContainsQtsQuestion(qualification)) return await CheckAnswersWhereQtsAnswered(details, qualification); // If there is a mismatch between the questions answered, then clear the answers and navigate back to the additional requirements check page - if (model.AdditionalRequirementAnswers.Count == 0 || - model.AdditionalRequirementAnswers.Exists(answer => string.IsNullOrEmpty(answer.Answer))) + if (qualificationDetailsService.DoAdditionalAnswersMatchQuestions(details)) { return (false, RedirectToAction("Index", "CheckAdditionalRequirements", - new { model.QualificationId, questionIndex = 1 })); + new + { + details.QualificationId, + questionIndex = 1 + } + ) + ); } // If there are not any answers to the questions that are not full and relevant we can continue back to check the ratios. - if (!AnswersIndicateNotFullAndRelevant(model.AdditionalRequirementAnswers)) return (true, null); + if (!qualificationDetailsService.AnswersIndicateNotFullAndRelevant(details.AdditionalRequirementAnswers)) return (true, null); // At this point, there will be at least one question answered in a non full and relevant way. // we mark the ratios as not full and relevant and return. - model.RatioRequirements = MarkAsNotFullAndRelevant(model.RatioRequirements); - return (false, View(model)); - } - - /// - /// A function to take in the additional requirement questions and answers, match them up and check to see if the - /// user has answered any in a non full and relevant way. - /// - /// This should come from the pre mapped questions and answers - /// True if we find any question answered in a non full and relevant way, false if none are found - private static bool AnswersIndicateNotFullAndRelevant( - List additionalRequirementsAnswers) - { - return additionalRequirementsAnswers - .Exists(answer => - answer is - { AnswerToBeFullAndRelevant: true, Answer: "no" } - or - { AnswerToBeFullAndRelevant: false, Answer: "yes" }); - } - - /// If the qualification is above a level 2 qualification, is not full and relevant and is started between Sept 2014 and Aug 2019 - /// then it will have special requirements for level 2. - private async Task QualIsNotLevel2NotApprovedAndStartedBetweenSept2014AndAug2019(QualificationDetailsModel model, Qualification qualification) - { - model.RatioRequirements.ApprovedForLevel2 = QualificationApprovalStatus.FurtherActionRequired; - var requirementsForLevel2 = GetRatioProperty("RequirementForLevel2BetweenSept14AndAug19", - RatioRequirements.Level2RatioRequirementName, - qualification); - model.RatioRequirements.RequirementsForLevel2 = await contentParser.ToHtml(requirementsForLevel2); - model.RatioRequirements.ShowRequirementsForLevel2ByDefault = true; + details.RatioRequirements = qualificationDetailsService.MarkAsNotFullAndRelevant(details.RatioRequirements); + return (false, View(details)); } - private async Task CheckRatioRequirements(bool qualificationStartedBeforeSeptember2014, - Qualification qualification, - QualificationDetailsModel model) + private async Task<(bool isValid, IActionResult? actionResult)> CheckAnswersWhereQtsAnswered(QualificationDetailsModel details, Qualification qualification) { - // Build up property name to check for each level - var beforeOrAfter = - qualificationStartedBeforeSeptember2014 - ? "Before" - : "After"; + var qtsQuestion = qualification.AdditionalRequirementQuestions!.First(x => x.Sys.Id == AdditionalRequirementQuestions.QtsQuestion); - var fullAndRelevantPropertyToCheck = - $"FullAndRelevantForLevel{qualification.QualificationLevel}{beforeOrAfter}2014"; - - var additionalRequirementDetailPropertyToCheck = - $"RequirementForLevel{qualification.QualificationLevel}{beforeOrAfter}2014"; - - if (qualification.IsAutomaticallyApprovedAtLevel6 || - (QualificationContainsQtsQuestion(qualification) - && UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, model.AdditionalRequirementAnswers))) + if (qualificationDetailsService.UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, details.AdditionalRequirementAnswers)) { - // Check user against QTS criteria and swap to Qts Criteria if matches - fullAndRelevantPropertyToCheck = $"FullAndRelevantForQtsEtc{beforeOrAfter}2014"; - additionalRequirementDetailPropertyToCheck = $"RequirementForQtsEtc{beforeOrAfter}2014"; + // Remove the additional requirements that they didn't answer following the bypass. + details.AdditionalRequirementAnswers!.RemoveAll(x => x.Question != qtsQuestion.Question); + return (true, null); } - const string additionalRequirementHeading = "RequirementHeading"; - - var approvedForLevel2 = GetRatioProperty(fullAndRelevantPropertyToCheck, RatioRequirements.Level2RatioRequirementName, - qualification); - - model.RatioRequirements.ApprovedForLevel2 = approvedForLevel2 - ? QualificationApprovalStatus.Approved - : QualificationApprovalStatus.NotApproved; - - - var requirementsForLevel2 = GetRatioProperty(additionalRequirementDetailPropertyToCheck, - RatioRequirements.Level2RatioRequirementName, - qualification); - model.RatioRequirements.RequirementsForLevel2 = await contentParser.ToHtml(requirementsForLevel2); - - model.RatioRequirements.RequirementsHeadingForLevel2 = - GetRatioProperty(additionalRequirementHeading, RatioRequirements.Level2RatioRequirementName, - qualification); - - var approvedForLevel3 = GetRatioProperty(fullAndRelevantPropertyToCheck, RatioRequirements.Level3RatioRequirementName, - qualification); - - model.RatioRequirements.ApprovedForLevel3 = approvedForLevel3 - ? QualificationApprovalStatus.Approved - : QualificationApprovalStatus.NotApproved; - - - var requirementsForLevel3 = GetRatioProperty(additionalRequirementDetailPropertyToCheck, - RatioRequirements.Level3RatioRequirementName, - qualification); - model.RatioRequirements.RequirementsForLevel3 = await contentParser.ToHtml(requirementsForLevel3); - - model.RatioRequirements.RequirementsHeadingForLevel3 = - GetRatioProperty(additionalRequirementHeading, RatioRequirements.Level3RatioRequirementName, - qualification); - - var approvedForLevel6 = GetRatioProperty(fullAndRelevantPropertyToCheck, RatioRequirements.Level6RatioRequirementName, - qualification); - - model.RatioRequirements.ApprovedForLevel6 = approvedForLevel6 - ? QualificationApprovalStatus.Approved - : QualificationApprovalStatus.NotApproved; - - var requirementsForLevel6 = GetRatioProperty(additionalRequirementDetailPropertyToCheck, - RatioRequirements.Level6RatioRequirementName, - qualification); - model.RatioRequirements.RequirementsForLevel6 = await contentParser.ToHtml(requirementsForLevel6); - - model.RatioRequirements.RequirementsHeadingForLevel6 = - GetRatioProperty(additionalRequirementHeading, RatioRequirements.Level6RatioRequirementName, - qualification); - - var approvedForUnqualified = GetRatioProperty(fullAndRelevantPropertyToCheck, RatioRequirements.UnqualifiedRatioRequirementName, qualification); - - model.RatioRequirements.ApprovedForUnqualified = approvedForUnqualified - ? QualificationApprovalStatus.Approved - : QualificationApprovalStatus.NotApproved; - + var remainingAnswersIndicateFullAndRelevant = qualificationDetailsService.RemainingAnswersIndicateFullAndRelevant(details, qtsQuestion); + if (remainingAnswersIndicateFullAndRelevant.isFullAndRelevant) return (true, null); - var requirementsForUnqualified = GetRatioProperty(additionalRequirementDetailPropertyToCheck, - RatioRequirements.UnqualifiedRatioRequirementName, - qualification); - model.RatioRequirements.RequirementsForUnqualified = await contentParser.ToHtml(requirementsForUnqualified); - - model.RatioRequirements.RequirementsHeadingForUnqualified = - GetRatioProperty(additionalRequirementHeading, RatioRequirements.UnqualifiedRatioRequirementName, qualification); - } - - private T GetRatioProperty(string propertyToCheck, string ratioName, Qualification qualification) - { - try - { - var requirement = qualification.RatioRequirements!.Find(x => x.RatioRequirementName == ratioName); - - return (T)requirement!.GetType().GetProperty(propertyToCheck)!.GetValue(requirement, null)!; - } - catch (Exception ex) - { - logger.LogError(ex, - "Could not find property: {PropertyToCheck} within {RatioName} for qualification: {QualificationId}", - propertyToCheck, ratioName, qualification.QualificationId); - throw; - } - } - - private static RatioRequirementModel MarkAsNotFullAndRelevant(RatioRequirementModel model) - { - model.ApprovedForLevel2 = QualificationApprovalStatus.NotApproved; - model.ApprovedForLevel3 = QualificationApprovalStatus.NotApproved; - model.ApprovedForLevel6 = QualificationApprovalStatus.NotApproved; - model.ApprovedForUnqualified = QualificationApprovalStatus.Approved; - - return model; - } - - private static bool QualificationContainsQtsQuestion(Qualification qualification) - { - return qualification.AdditionalRequirementQuestions != null - && qualification.AdditionalRequirementQuestions.Exists(x => x.Sys.Id == AdditionalRequirementQuestions - .QtsQuestion); - } - - private static bool UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(Qualification qualification, - List? - additionalRequirementAnswerModels) - { - if (additionalRequirementAnswerModels is null) - { - return false; - } - - var qtsQuestion = - qualification.AdditionalRequirementQuestions!.First(x => x.Sys.Id == AdditionalRequirementQuestions - .QtsQuestion); - - var userAnsweredQuestion = additionalRequirementAnswerModels.First(x => x.Question == qtsQuestion.Question); - var answerAsBool = userAnsweredQuestion.Answer == "yes"; - return qtsQuestion.AnswerToBeFullAndRelevant == answerAsBool; - } - - private async Task MapDetails( - bool qualificationStartedBeforeSeptember2014, - Qualification qualification, - DetailsPage content) - { - var backNavLink = CalculateBackButton(qualificationStartedBeforeSeptember2014, - content, qualification.QualificationId); - - var dateStarted = string.Empty; - var (startMonth, startYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); - - // ReSharper disable once InvertIf - if (startYear is not null && startMonth is not null) - { - var dateOnly = new DateOnly(startYear.Value, startMonth.Value, 1); - dateStarted = dateOnly.ToString("MMMM yyyy"); - } - - var checkAnotherQualificationText = await contentParser.ToHtml(content.CheckAnotherQualificationText); - var furtherInfoText = await contentParser.ToHtml(content.FurtherInfoText); - var requirementsText = await contentParser.ToHtml(content.RequirementsText); - var ratiosText = await contentParser.ToHtml(content.RatiosText); - var ratiosTextNotFullAndRelevant = - await contentParser.ToHtml(content.RatiosTextNotFullAndRelevant); - var feedbackBodyHtml = await GetFeedbackBannerBodyToHtml(content.FeedbackBanner, contentParser); - - return QualificationDetailsMapper.Map(qualification, content, backNavLink, - MapAdditionalRequirementAnswers(qualification - .AdditionalRequirementQuestions), - dateStarted, checkAnotherQualificationText, furtherInfoText, - requirementsText, ratiosText, ratiosTextNotFullAndRelevant, - feedbackBodyHtml); - } - - private NavigationLink? CalculateBackButton( - bool qualificationStartedBeforeSeptember2014, - DetailsPage content, - string qualificationId) - { - if (userJourneyCookieService.UserHasAnsweredAdditionalQuestions()) - { - return ContentBackButtonLinkForAdditionalQuestions(content, qualificationId); - } - - var level = userJourneyCookieService.GetLevelOfQualification(); - - NavigationLink? backButton = null; - - if (userJourneyCookieService.GetQualificationWasSelectedFromList() != YesOrNo.Yes - && level == 6) - { - // Advice is different for qualifications started before September 2014 - backButton = qualificationStartedBeforeSeptember2014 - ? content.BackToLevelSixAdviceBefore2014 - : content.BackToLevelSixAdvice; - } - - return backButton ?? content.BackButton; - } - - private static NavigationLink? ContentBackButtonLinkForAdditionalQuestions( - DetailsPage content, string qualificationId) - { - const string placeholder = "$[qualification-id]$"; - var link = content.BackToConfirmAnswers; - - if (link == null) - { - return content.BackButton; - } - - link.Href = link.Href.Replace(placeholder, qualificationId); - - return link; - } - - private List? MapAdditionalRequirementAnswers( - List? additionalRequirementQuestions) - { - if (additionalRequirementQuestions is null) return null; - - var additionalRequirementsAnswers = userJourneyCookieService.GetAdditionalQuestionsAnswers(); - - var results = new List(); - - if (additionalRequirementsAnswers is null) return results; - - foreach (var additionalRequirementQuestion in additionalRequirementQuestions) - { - var answerToAdd = new AdditionalRequirementAnswerModel - { - Question = additionalRequirementQuestion.Question, - AnswerToBeFullAndRelevant = additionalRequirementQuestion.AnswerToBeFullAndRelevant, - ConfirmationStatement = additionalRequirementQuestion.ConfirmationStatement - }; - - if (additionalRequirementsAnswers.TryGetValue(additionalRequirementQuestion.Question, out var answer)) - { - answerToAdd.Answer = answer; - } - - results.Add(answerToAdd); - } + details = await qualificationDetailsService.CheckLevel6Requirements(qualification, details); - return results; + return (false, View(details)); } } \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Program.cs b/src/Dfe.EarlyYearsQualification.Web/Program.cs index 58a91941..bf3d882f 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Program.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Program.cs @@ -12,6 +12,7 @@ using Dfe.EarlyYearsQualification.Web.Services.Cookies; using Dfe.EarlyYearsQualification.Web.Services.CookiesPreferenceService; using Dfe.EarlyYearsQualification.Web.Services.DatesAndTimes; +using Dfe.EarlyYearsQualification.Web.Services.QualificationDetails; using Dfe.EarlyYearsQualification.Web.Services.QualificationSearch; using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; using GovUk.Frontend.AspNetCore; @@ -100,6 +101,7 @@ builder.Services.AddTransient(); } +builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddModelRenderers(); builder.Services.AddSingleton(); diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/IQualificationDetailsService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/IQualificationDetailsService.cs new file mode 100644 index 00000000..31bf673b --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/IQualificationDetailsService.cs @@ -0,0 +1,24 @@ +using Dfe.EarlyYearsQualification.Content.Entities; +using Dfe.EarlyYearsQualification.Web.Models.Content; + +namespace Dfe.EarlyYearsQualification.Web.Services.QualificationDetails; + +public interface IQualificationDetailsService +{ + Task GetQualification(string qualificationId); + Task GetDetailsPage(); + bool HasStartDate(); + Task GetFeedbackBannerBodyToHtml(FeedbackBanner? feedbackBanner); + bool QualificationContainsQtsQuestion(Qualification qualification); + bool UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(Qualification qualification, List? additionalRequirementAnswerModels); + bool AnswersIndicateNotFullAndRelevant(List additionalRequirementsAnswers); + RatioRequirementModel MarkAsNotFullAndRelevant(RatioRequirementModel model); + Task QualificationLevel3OrAboveMightBeRelevantAtLevel2(QualificationDetailsModel model, Qualification qualification); + Task CheckRatioRequirements(Qualification qualification, QualificationDetailsModel model); + (bool isFullAndRelevant, QualificationDetailsModel details) RemainingAnswersIndicateFullAndRelevant(QualificationDetailsModel details, AdditionalRequirementQuestion qtsQuestion); + Task CheckLevel6Requirements(Qualification qualification, QualificationDetailsModel details); + bool DoAdditionalAnswersMatchQuestions(QualificationDetailsModel details); + NavigationLink? CalculateBackButton(DetailsPage content, string qualificationId); + List? MapAdditionalRequirementAnswers(List? additionalRequirementQuestions); + Task MapDetails(Qualification qualification, DetailsPage content); +} \ No newline at end of file diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/QualificationDetailsService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/QualificationDetailsService.cs new file mode 100644 index 00000000..df27ec25 --- /dev/null +++ b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/QualificationDetailsService.cs @@ -0,0 +1,322 @@ +using Contentful.Core.Models; +using Dfe.EarlyYearsQualification.Content.Constants; +using Dfe.EarlyYearsQualification.Content.Entities; +using Dfe.EarlyYearsQualification.Content.RichTextParsing; +using Dfe.EarlyYearsQualification.Content.Services.Interfaces; +using Dfe.EarlyYearsQualification.Web.Mappers; +using Dfe.EarlyYearsQualification.Web.Models; +using Dfe.EarlyYearsQualification.Web.Models.Content; +using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; + +namespace Dfe.EarlyYearsQualification.Web.Services.QualificationDetails; + +public class QualificationDetailsService( + ILogger logger, + IQualificationsRepository qualificationsRepository, + IContentService contentService, + IGovUkContentParser contentParser, + IUserJourneyCookieService userJourneyCookieService +) : IQualificationDetailsService +{ + public async Task GetQualification(string qualificationId) + { + return await qualificationsRepository.GetById(qualificationId); + } + + public async Task GetDetailsPage() + { + return await contentService.GetDetailsPage(); + } + + public bool HasStartDate() + { + var (startDateMonth, startDateYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); + return startDateMonth is not null && startDateYear is not null; + } + + public async Task GetFeedbackBannerBodyToHtml(FeedbackBanner? feedbackBanner) + { + return feedbackBanner is not null + ? await contentParser.ToHtml(feedbackBanner.Body) + : null; + } + + public List? MapAdditionalRequirementAnswers(List? additionalRequirementQuestions) + { + if (additionalRequirementQuestions is null) return null; + + var additionalRequirementsAnswers = userJourneyCookieService.GetAdditionalQuestionsAnswers(); + + var results = new List(); + + if (additionalRequirementsAnswers is null) return results; + + foreach (var additionalRequirementQuestion in additionalRequirementQuestions) + { + var answerToAdd = new AdditionalRequirementAnswerModel + { + Question = additionalRequirementQuestion.Question, + AnswerToBeFullAndRelevant = additionalRequirementQuestion.AnswerToBeFullAndRelevant, + ConfirmationStatement = additionalRequirementQuestion.ConfirmationStatement + }; + + if (additionalRequirementsAnswers.TryGetValue(additionalRequirementQuestion.Question, out var answer)) + { + answerToAdd.Answer = answer; + } + + results.Add(answerToAdd); + } + + return results; + } + + public (bool isFullAndRelevant, QualificationDetailsModel details) RemainingAnswersIndicateFullAndRelevant(QualificationDetailsModel details, AdditionalRequirementQuestion qtsQuestion) + { + // Check remaining questions + var answersToCheck = new List(); + answersToCheck.AddRange(details.AdditionalRequirementAnswers!); + // As L6 / L7 can potentially work at L3/2/unqualified, remove the Qts question and check answers + answersToCheck.RemoveAll(x => x.Question == qtsQuestion.Question); + + // As we know that they didn't answer the Qts question, we need to show the L6 requirements by default. + // Adding it here covers scenarios where they are OK for L2/3/Unqualified and just Unqualified. + details.RatioRequirements.ShowRequirementsForLevel6ByDefault = true; + + if (!AnswersIndicateNotFullAndRelevant(answersToCheck)) return (true, details); + return (false, details); + } + + public bool QualificationContainsQtsQuestion(Qualification qualification) + { + return qualification.AdditionalRequirementQuestions != null + && qualification.AdditionalRequirementQuestions.Exists(x => x.Sys.Id == AdditionalRequirementQuestions.QtsQuestion); + } + + public RatioRequirementModel MarkAsNotFullAndRelevant(RatioRequirementModel model) + { + model.ApprovedForLevel2 = QualificationApprovalStatus.NotApproved; + model.ApprovedForLevel3 = QualificationApprovalStatus.NotApproved; + model.ApprovedForLevel6 = QualificationApprovalStatus.NotApproved; + model.ApprovedForUnqualified = QualificationApprovalStatus.Approved; + + return model; + } + + public bool DoAdditionalAnswersMatchQuestions(QualificationDetailsModel details) + { + return details.AdditionalRequirementAnswers!.Count == 0 || + details.AdditionalRequirementAnswers.Exists(answer => string.IsNullOrEmpty(answer.Answer)); + } + + public async Task CheckLevel6Requirements(Qualification qualification, QualificationDetailsModel details) + { + // Answers indicate not full and relevant + details.RatioRequirements = MarkAsNotFullAndRelevant(details.RatioRequirements); + // Set any content for L6 + var beforeOrAfter = userJourneyCookieService.WasStartedBeforeSeptember2014() ? "Before" : "After"; + var additionalRequirementDetailPropertyToCheck = $"RequirementForLevel{qualification.QualificationLevel}{beforeOrAfter}2014"; + var requirementsForLevel6 = GetRatioProperty(additionalRequirementDetailPropertyToCheck, RatioRequirements.Level6RatioRequirementName, qualification); + details.RatioRequirements.RequirementsForLevel6 = await contentParser.ToHtml(requirementsForLevel6); + details.RatioRequirements.ShowRequirementsForLevel6ByDefault = true; + return details; + } + + /// + /// A function to take in the additional requirement questions and answers, match them up and check to see if the + /// user has answered any in a non full and relevant way. + /// + /// This should come from the pre mapped questions and answers + /// True if we find any question answered in a non full and relevant way, false if none are found + public bool AnswersIndicateNotFullAndRelevant(List additionalRequirementsAnswers) + { + return additionalRequirementsAnswers + .Exists(answer => + answer is + { AnswerToBeFullAndRelevant: true, Answer: "no" } + or + { AnswerToBeFullAndRelevant: false, Answer: "yes" } + ); + } + + public bool UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(Qualification qualification, List? additionalRequirementAnswerModels) + { + if (additionalRequirementAnswerModels is null) + { + return false; + } + + var qtsQuestion = + qualification.AdditionalRequirementQuestions!.First(x => x.Sys.Id == AdditionalRequirementQuestions + .QtsQuestion); + + var userAnsweredQuestion = additionalRequirementAnswerModels.First(x => x.Question == qtsQuestion.Question); + var answerAsBool = userAnsweredQuestion.Answer == "yes"; + return qtsQuestion.AnswerToBeFullAndRelevant == answerAsBool; + } + + public async Task QualificationLevel3OrAboveMightBeRelevantAtLevel2(QualificationDetailsModel model, Qualification qualification) + { + // Check if the qualification is not full and relevant and was started between Sept 2014 and Aug 2019 and is above a level 2 qualification + if (model.RatioRequirements.IsNotFullAndRelevant && userJourneyCookieService.WasStartedBetweenSeptember2014AndAugust2019() && qualification.QualificationLevel > 2) + { + // If the qualification is above a level 2 qualification, is not full and relevant and is started between Sept 2014 and Aug 2019 + // then it will have special requirements for level 2. + model.RatioRequirements.ApprovedForLevel2 = QualificationApprovalStatus.FurtherActionRequired; + var requirementsForLevel2 = GetRatioProperty("RequirementForLevel2BetweenSept14AndAug19", RatioRequirements.Level2RatioRequirementName, qualification); + model.RatioRequirements.RequirementsForLevel2 = await contentParser.ToHtml(requirementsForLevel2); + model.RatioRequirements.ShowRequirementsForLevel2ByDefault = true; + } + } + + public T GetRatioProperty(string propertyToCheck, string ratioName, Qualification qualification) + { + try + { + var requirement = qualification.RatioRequirements!.Find(x => x.RatioRequirementName == ratioName); + + return (T)requirement!.GetType().GetProperty(propertyToCheck)!.GetValue(requirement, null)!; + } + catch (Exception ex) + { + logger.LogError(ex, "Could not find property: {PropertyToCheck} within {RatioName} for qualification: {QualificationId}", propertyToCheck, ratioName, qualification.QualificationId); + throw; + } + } + + public NavigationLink? CalculateBackButton(DetailsPage content, string qualificationId) + { + if (userJourneyCookieService.UserHasAnsweredAdditionalQuestions()) + { + var link = content.BackToConfirmAnswers; + if (link == null) return content.BackButton; + link.Href = link.Href.Replace("$[qualification-id]$", qualificationId); + return link; + } + + var level = userJourneyCookieService.GetLevelOfQualification(); + + NavigationLink? backButton = null; + + if (userJourneyCookieService.GetQualificationWasSelectedFromList() != YesOrNo.Yes + && level == 6) + { + // Advice is different for qualifications started before September 2014 + backButton = userJourneyCookieService.WasStartedBeforeSeptember2014() + ? content.BackToLevelSixAdviceBefore2014 + : content.BackToLevelSixAdvice; + } + + return backButton ?? content.BackButton; + } + + public async Task CheckRatioRequirements(Qualification qualification, QualificationDetailsModel model) + { + // Build up property name to check for each level + var beforeOrAfter = userJourneyCookieService.WasStartedBeforeSeptember2014() ? "Before" : "After"; + + var fullAndRelevantPropertyToCheck = $"FullAndRelevantForLevel{qualification.QualificationLevel}{beforeOrAfter}2014"; + + var additionalRequirementDetailPropertyToCheck = $"RequirementForLevel{qualification.QualificationLevel}{beforeOrAfter}2014"; + + if (qualification.IsAutomaticallyApprovedAtLevel6 || (QualificationContainsQtsQuestion(qualification) && + UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, model.AdditionalRequirementAnswers))) + { + // Check user against QTS criteria and swap to Qts Criteria if matches + fullAndRelevantPropertyToCheck = $"FullAndRelevantForQtsEtc{beforeOrAfter}2014"; + additionalRequirementDetailPropertyToCheck = $"RequirementForQtsEtc{beforeOrAfter}2014"; + } + + const string additionalRequirementHeading = "RequirementHeading"; + + var approvedForLevel2 = GetRatioProperty(fullAndRelevantPropertyToCheck, RatioRequirements.Level2RatioRequirementName, + qualification); + + model.RatioRequirements.ApprovedForLevel2 = approvedForLevel2 + ? QualificationApprovalStatus.Approved + : QualificationApprovalStatus.NotApproved; + + var requirementsForLevel2 = GetRatioProperty(additionalRequirementDetailPropertyToCheck, + RatioRequirements.Level2RatioRequirementName, + qualification); + model.RatioRequirements.RequirementsForLevel2 = await contentParser.ToHtml(requirementsForLevel2); + + model.RatioRequirements.RequirementsHeadingForLevel2 = + GetRatioProperty(additionalRequirementHeading, RatioRequirements.Level2RatioRequirementName, + qualification); + + var approvedForLevel3 = GetRatioProperty(fullAndRelevantPropertyToCheck, RatioRequirements.Level3RatioRequirementName, + qualification); + + model.RatioRequirements.ApprovedForLevel3 = approvedForLevel3 + ? QualificationApprovalStatus.Approved + : QualificationApprovalStatus.NotApproved; + + var requirementsForLevel3 = GetRatioProperty(additionalRequirementDetailPropertyToCheck, + RatioRequirements.Level3RatioRequirementName, + qualification); + model.RatioRequirements.RequirementsForLevel3 = await contentParser.ToHtml(requirementsForLevel3); + + model.RatioRequirements.RequirementsHeadingForLevel3 = + GetRatioProperty(additionalRequirementHeading, RatioRequirements.Level3RatioRequirementName, + qualification); + + var approvedForLevel6 = GetRatioProperty(fullAndRelevantPropertyToCheck, RatioRequirements.Level6RatioRequirementName, + qualification); + + model.RatioRequirements.ApprovedForLevel6 = approvedForLevel6 + ? QualificationApprovalStatus.Approved + : QualificationApprovalStatus.NotApproved; + + var requirementsForLevel6 = GetRatioProperty(additionalRequirementDetailPropertyToCheck, + RatioRequirements.Level6RatioRequirementName, + qualification); + model.RatioRequirements.RequirementsForLevel6 = await contentParser.ToHtml(requirementsForLevel6); + + model.RatioRequirements.RequirementsHeadingForLevel6 = + GetRatioProperty(additionalRequirementHeading, RatioRequirements.Level6RatioRequirementName, + qualification); + + var approvedForUnqualified = GetRatioProperty(fullAndRelevantPropertyToCheck, RatioRequirements.UnqualifiedRatioRequirementName, qualification); + + model.RatioRequirements.ApprovedForUnqualified = approvedForUnqualified + ? QualificationApprovalStatus.Approved + : QualificationApprovalStatus.NotApproved; + + var requirementsForUnqualified = GetRatioProperty(additionalRequirementDetailPropertyToCheck, + RatioRequirements.UnqualifiedRatioRequirementName, + qualification); + model.RatioRequirements.RequirementsForUnqualified = await contentParser.ToHtml(requirementsForUnqualified); + + model.RatioRequirements.RequirementsHeadingForUnqualified = + GetRatioProperty(additionalRequirementHeading, RatioRequirements.UnqualifiedRatioRequirementName, qualification); + } + + public async Task MapDetails(Qualification qualification, DetailsPage content) + { + var backNavLink = CalculateBackButton(content, qualification.QualificationId); + + var dateStarted = string.Empty; + var (startMonth, startYear) = userJourneyCookieService.GetWhenWasQualificationStarted(); + + // ReSharper disable once InvertIf + if (startYear is not null && startMonth is not null) + { + var dateOnly = new DateOnly(startYear.Value, startMonth.Value, 1); + dateStarted = dateOnly.ToString("MMMM yyyy"); + } + + var checkAnotherQualificationText = await contentParser.ToHtml(content.CheckAnotherQualificationText); + var furtherInfoText = await contentParser.ToHtml(content.FurtherInfoText); + var requirementsText = await contentParser.ToHtml(content.RequirementsText); + var ratiosText = await contentParser.ToHtml(content.RatiosText); + var ratiosTextNotFullAndRelevant = await contentParser.ToHtml(content.RatiosTextNotFullAndRelevant); + var feedbackBodyHtml = await GetFeedbackBannerBodyToHtml(content.FeedbackBanner); + + return QualificationDetailsMapper.Map(qualification, content, backNavLink, + MapAdditionalRequirementAnswers(qualification.AdditionalRequirementQuestions), + dateStarted, checkAnotherQualificationText, furtherInfoText, + requirementsText, ratiosText, ratiosTextNotFullAndRelevant, + feedbackBodyHtml); + } +} \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/HomeControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/HomeControllerTests.cs index 9358c6da..0b689952 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/HomeControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/HomeControllerTests.cs @@ -2,14 +2,9 @@ using Dfe.EarlyYearsQualification.Content.RichTextParsing; using Dfe.EarlyYearsQualification.Content.Services.Interfaces; using Dfe.EarlyYearsQualification.Mock.Helpers; -using Dfe.EarlyYearsQualification.UnitTests.Extensions; using Dfe.EarlyYearsQualification.Web.Controllers; using Dfe.EarlyYearsQualification.Web.Models.Content; using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; -using FluentAssertions; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; namespace Dfe.EarlyYearsQualification.UnitTests.Controllers; diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs index 6abeff13..8bdf3dbe 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Controllers/QualificationDetailsControllerTests.cs @@ -1,1598 +1,384 @@ using Contentful.Core.Models; using Dfe.EarlyYearsQualification.Content.Constants; using Dfe.EarlyYearsQualification.Content.Entities; -using Dfe.EarlyYearsQualification.Content.RichTextParsing; -using Dfe.EarlyYearsQualification.Content.Services.Interfaces; -using Dfe.EarlyYearsQualification.Mock.Helpers; -using Dfe.EarlyYearsQualification.UnitTests.Extensions; using Dfe.EarlyYearsQualification.Web.Controllers; -using Dfe.EarlyYearsQualification.Web.Models; using Dfe.EarlyYearsQualification.Web.Models.Content; -using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; -using FluentAssertions; +using Dfe.EarlyYearsQualification.Web.Services.QualificationDetails; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Moq; namespace Dfe.EarlyYearsQualification.UnitTests.Controllers; [TestClass] public class QualificationDetailsControllerTests { + private static Qualification DummyQualification => new(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ); + + private static DetailsPage DummyDetailsPage => new(); + + private static QualificationDetailsModel DummyDetails => new(); + + private readonly Mock> _mockLogger = new(); + private readonly Mock _mockQualificationDetailsService = new(); + + private QualificationDetailsController GetSut() => new(_mockLogger.Object, + _mockQualificationDetailsService.Object) + { + ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext() + } + }; + [TestMethod] - public async Task Index_PassInNullQualificationId_ReturnsBadRequest() + public async Task Index_NullId_Returns400BadRequest() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(string.Empty); + const string qualificationId = null!; + var sut = GetSut(); - result.Should().NotBeNull(); + var result = await sut.Index(qualificationId!); + result.Should().BeOfType(); var resultType = result as BadRequestResult; resultType.Should().NotBeNull(); resultType!.StatusCode.Should().Be(400); } [TestMethod] - public async Task Index_ContentServiceReturnsNullDetailsPage_RedirectsToHomeError() + public async Task Index_EmptyId_Returns400BadRequest() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - mockContentService.Setup(s => s.GetDetailsPage()) - .ReturnsAsync((DetailsPage?)null); - - var result = await controller.Index("X"); - - result.Should().BeOfType(); - - var actionResult = (RedirectToActionResult)result; - - actionResult.ActionName.Should().Be("Index"); - actionResult.ControllerName.Should().Be("Error"); - - mockLogger.VerifyError("No content for the qualification details page"); + const string qualificationId = ""; + var sut = GetSut(); + + var result = await sut.Index(qualificationId); + + result.Should().BeOfType(); + var resultType = result as BadRequestResult; + resultType.Should().NotBeNull(); + resultType!.StatusCode.Should().Be(400); } [TestMethod] - public async Task Index_ContentServiceReturnsNoQualification_RedirectsToErrorPage() + public async Task Index_Calls_QualificationDetailsService_HasStartDate() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - const string qualificationId = "eyq-145"; - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync((Qualification?)default); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(new DetailsPage()); - - var result = await controller.Index(qualificationId); + const string qualificationId = "qualificationId"; - result.Should().NotBeNull(); + var sut = GetSut(); - var resultType = result as RedirectToActionResult; - resultType.Should().NotBeNull(); - resultType!.ActionName.Should().Be("Index"); - resultType.ControllerName.Should().Be("Error"); + _ = await sut.Index(qualificationId); - mockLogger.VerifyError("Could not find details for qualification with ID: eyq-145"); + _mockQualificationDetailsService.Verify(o => o.HasStartDate(), Times.Once); } [TestMethod] - public async Task Index_NoDateOfQualificationSelectedPriorInTheJourney_RedirectToHome() + public async Task Index_MissingStartDate_RedirectsToHome() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - 2) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements" - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(new DetailsPage()); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((null, null)); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); + const string qualificationId = "qualificationId"; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(false); - var resultType = result as RedirectToActionResult; - resultType.Should().NotBeNull(); - resultType!.ActionName.Should().Be("Index"); - resultType.ControllerName.Should().Be("Home"); - } + var sut = GetSut(); - [TestMethod] - public async Task Index_NoDateMonthOfQualificationSelectedPriorInTheJourney_RedirectToHome() - { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - 2) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements" - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(new DetailsPage()); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((null, 2012)); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); + var result = await sut.Index(qualificationId); - var resultType = result as RedirectToActionResult; - resultType.Should().NotBeNull(); - resultType!.ActionName.Should().Be("Index"); - resultType.ControllerName.Should().Be("Home"); + result.VerifyRedirect("Index", "Home"); } [TestMethod] - public async Task Index_QualificationHasAdditionalQuestionsButNoneAnswered_RedirectTotTheAdditionalQuestionsPage() + public async Task Index_Calls_QualificationDetailsService_GetDetailsPage() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - - var listOfAdditionalReqs = new List - { - new() - { - Question = "Some Question" - } - }; - - // Mismatch between the two lists here to simulate questions not being answered - var listOfAdditionalReqsAnswered = new Dictionary(); - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - 2) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - AdditionalRequirementQuestions = listOfAdditionalReqs - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(new DetailsPage()); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((9, 2022)); - mockUserJourneyCookieService.Setup(x => x.GetAdditionalQuestionsAnswers()) - .Returns(listOfAdditionalReqsAnswered); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); + const string qualificationId = "qualificationId"; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); - var resultType = result as RedirectToActionResult; - resultType.Should().NotBeNull(); - resultType!.ActionName.Should().Be("Index"); - resultType.ControllerName.Should().Be("CheckAdditionalRequirements"); - resultType.RouteValues.Should().Contain("qualificationId", qualificationId); - resultType.RouteValues.Should().Contain("questionIndex", 1); + var sut = GetSut(); + + _ = await sut.Index(qualificationId); + + _mockQualificationDetailsService.Verify(o => o.GetDetailsPage(), Times.Once); } [TestMethod] - [DataRow("no", "no")] - [DataRow("yes", "yes")] - [DataRow("no", "yes")] - public async Task - Index_Index_QualificationHasAdditionalQuestionsButAnswersAreNotCorrect_MarkAsNotRelevantAndReturn( - string answer1, string answer2) + public async Task Index_DetailsPage_IsNull_RedirectsToError() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - - var listOfAdditionalReqs = new List - { - new() - { - Question = "Some Question", - AnswerToBeFullAndRelevant = true - }, - new() - { - Question = "Another Question", - AnswerToBeFullAndRelevant = false - } - }; - - // Question has been answered, but the answer is not what we want for the qualification to be full and relevant - var listOfAdditionalReqsAnswered = new Dictionary - { - { "Some Question", answer1 }, - { "Another Question", answer2 } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - 2) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - AdditionalRequirementQuestions = listOfAdditionalReqs - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(new DetailsPage()); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()) - .Returns((9, 2022)); - - mockUserJourneyCookieService.Setup(x => x.GetAdditionalQuestionsAnswers()) - .Returns(listOfAdditionalReqsAnswered); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); - - result.Should().NotBeNull(); - - var resultType = result as ViewResult; - resultType.Should().NotBeNull(); - - var model = resultType!.Model as QualificationDetailsModel; - model.Should().NotBeNull(); + const string qualificationId = "qualificationId"; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(o => o.GetDetailsPage()).ReturnsAsync((DetailsPage)null!); - model!.QualificationId.Should().Be(qualificationResult.QualificationId); - model.QualificationName.Should().Be(qualificationResult.QualificationName); - model.AwardingOrganisationTitle.Should().Be(qualificationResult.AwardingOrganisationTitle); - model.QualificationLevel.Should().Be(qualificationResult.QualificationLevel); - model.FromWhichYear.Should().Be(qualificationResult.FromWhichYear); - model.QualificationNumber.Should().Be(qualificationResult.QualificationNumber); + var sut = GetSut(); - model.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.NotApproved); + var result = await sut.Index(qualificationId); - //Note unqualified ratios will always be approved no matter the answers - model.RatioRequirements.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); + _mockLogger.VerifyError("No content for the qualification details page"); + result.VerifyRedirect("Index", "Error"); } [TestMethod] - public async Task Index_BuildUpRatioRequirements_CantFindLevel2_LogsAndThrows() + public async Task Index_Calls_QualificationDetailsService_GetQualification() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - const int level = 2; - const int startDateYear = 2022; - - var ratioRequirements = new List(); - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - RatioRequirements = ratioRequirements - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(new DetailsPage()); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((9, startDateYear)); - mockUserJourneyCookieService.Setup(x => x.WasStartedBeforeSeptember2014()) - .Returns(false); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - await Assert.ThrowsExceptionAsync(() => controller.Index(qualificationId)); - mockLogger.VerifyError($"Could not find property: FullAndRelevantForLevel{level}After2014 within Level 2 Ratio Requirements for qualification: {qualificationId}"); + const string qualificationId = "qualificationId"; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(o => o.GetDetailsPage()).ReturnsAsync(DummyDetailsPage); + + var sut = GetSut(); + + _ = await sut.Index(qualificationId); + + _mockQualificationDetailsService.Verify(o => o.GetQualification(qualificationId), Times.Once); } [TestMethod] - public async Task Index_BuildUpRatioRequirements_CantFindLevel3_LogsAndThrows() + public async Task Index_Qualification_IsNull_RedirectsToError() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - const int level = 2; - const int startDateYear = 2022; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForLevel2After2014 = true - } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - RatioRequirements = ratioRequirements - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(new DetailsPage()); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()) - .Returns((9, startDateYear)); - - mockUserJourneyCookieService.Setup(x => x.WasStartedBeforeSeptember2014()) - .Returns(false); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - await Assert.ThrowsExceptionAsync(() => controller.Index(qualificationId)); - mockLogger.VerifyError($"Could not find property: FullAndRelevantForLevel{level}After2014 within Level 3 Ratio Requirements for qualification: {qualificationId}"); + const string qualificationId = "qualificationId"; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(o => o.GetDetailsPage()).ReturnsAsync(DummyDetailsPage); + _mockQualificationDetailsService.Setup(o => o.GetQualification(qualificationId)).ReturnsAsync((Qualification)null!); + + var sut = GetSut(); + + var result = await sut.Index(qualificationId); + + _mockLogger.VerifyError($"Could not find details for qualification with ID: qualificationId"); + result.VerifyRedirect("Index", "Error"); } [TestMethod] - public async Task Index_BuildUpRatioRequirements_CantFindLevel6_LogsAndThrows() + public async Task Index_Calls_QualificationDetailsService_MapDetails() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - const int level = 2; - const int startDateYear = 2022; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForLevel2After2014 = true - }, - new() - { - RatioRequirementName = "Level 3 Ratio Requirements", - FullAndRelevantForLevel2After2014 = true - } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - RatioRequirements = ratioRequirements - }; - - mockRepository.Setup(x => x.GetById(qualificationId)).ReturnsAsync(qualificationResult); - mockContentService.Setup(x => x.GetDetailsPage()).ReturnsAsync(new DetailsPage()); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((9, startDateYear)); - mockUserJourneyCookieService.Setup(x => x.WasStartedBeforeSeptember2014()) - .Returns(false); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - await Assert.ThrowsExceptionAsync(() => controller.Index(qualificationId)); - mockLogger.VerifyError($"Could not find property: FullAndRelevantForLevel{level}After2014 within Level 6 Ratio Requirements for qualification: {qualificationId}"); + const string qualificationId = "qualificationId"; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(o => o.GetDetailsPage()).ReturnsAsync(DummyDetailsPage); + _mockQualificationDetailsService.Setup(o => o.GetQualification(qualificationId)).ReturnsAsync(DummyQualification); + + var sut = GetSut(); + try + { + _ = await sut.Index(qualificationId); + } + catch + { + //ignored + } + finally + { + _mockQualificationDetailsService.Verify(o => o.MapDetails(It.IsAny(), It.IsAny()), Times.Once); + } } [TestMethod] - [DataRow(9, 2014, 3)] - [DataRow(8, 2019, 3)] - [DataRow(9, 2014, 4)] - [DataRow(8, 2019, 4)] - [DataRow(9, 2014, 5)] - [DataRow(8, 2019, 5)] - [DataRow(9, 2014, 6)] - [DataRow(8, 2019, 6)] - [DataRow(9, 2014, 7)] - [DataRow(8, 2019, 7)] - public async Task Index_BuildUpRatioRequirements_NotRelevantButLevel3OrAboveStartedBetweenSept2014AndAug2019_MarksLevel2AsFurtherActionRequired(int startMonth, int startYear, int level) + public async Task Index_ValidateAdditionalQuestions_Valid_Calls_QualificationDetailsService_CheckRatioRequirements() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForLevel2After2014 = false, - FullAndRelevantForLevel3After2014 = false, - FullAndRelevantForLevel4After2014 = false, - FullAndRelevantForLevel5After2014 = false, - FullAndRelevantForLevel6After2014 = false, - FullAndRelevantForLevel7After2014 = false, - RequirementForLevel2BetweenSept14AndAug19 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Level 3 Ratio Requirements", - FullAndRelevantForLevel2After2014 = false, - FullAndRelevantForLevel3After2014 = false, - FullAndRelevantForLevel4After2014 = false, - FullAndRelevantForLevel5After2014 = false, - FullAndRelevantForLevel6After2014 = false, - FullAndRelevantForLevel7After2014 = false, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Level 6 Ratio Requirements", - FullAndRelevantForLevel2After2014 = false, - FullAndRelevantForLevel3After2014 = false, - FullAndRelevantForLevel4After2014 = false, - FullAndRelevantForLevel5After2014 = false, - FullAndRelevantForLevel6After2014 = false, - FullAndRelevantForLevel7After2014 = false, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - // Note that every qualification will be marked as relevant for unqualified work, this still counts as not F and R. - new() - { - RatioRequirementName = "Unqualified Ratio Requirements", - FullAndRelevantForLevel2After2014 = true, - FullAndRelevantForLevel3After2014 = true, - FullAndRelevantForLevel4After2014 = true, - FullAndRelevantForLevel5After2014 = true, - FullAndRelevantForLevel6After2014 = true, - FullAndRelevantForLevel7After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - RatioRequirements = ratioRequirements - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(new DetailsPage - { - BackToConfirmAnswers = new NavigationLink - { - Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" - } - }); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((startMonth, startYear)); - mockUserJourneyCookieService.Setup(x => x.WasStartedBetweenSeptember2014AndAugust2019()).Returns(true); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); - - result.Should().NotBeNull(); - - var resultType = result as ViewResult; - resultType.Should().NotBeNull(); + const string qualificationId = "qualificationId"; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(o => o.GetDetailsPage()).ReturnsAsync(DummyDetailsPage); + _mockQualificationDetailsService.Setup(o => o.GetQualification(qualificationId)).ReturnsAsync(DummyQualification); + _mockQualificationDetailsService.Setup(o => o.MapDetails(It.IsAny(), It.IsAny())).ReturnsAsync(DummyDetails); - var model = resultType!.Model as QualificationDetailsModel; - model.Should().NotBeNull(); + var sut = GetSut(); + _ = await sut.Index(qualificationId); - model!.QualificationId.Should().Be(qualificationResult.QualificationId); - model.QualificationName.Should().Be(qualificationResult.QualificationName); - model.AwardingOrganisationTitle.Should().Be(qualificationResult.AwardingOrganisationTitle); - model.QualificationLevel.Should().Be(qualificationResult.QualificationLevel); - model.FromWhichYear.Should().Be(qualificationResult.FromWhichYear); - model.QualificationNumber.Should().Be(qualificationResult.QualificationNumber); - - model.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.FurtherActionRequired); - model.RatioRequirements.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); + _mockQualificationDetailsService.Verify(o => o.CheckRatioRequirements(It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] - public async Task - Index_QualIsRelevantButAdditionalAnswersMarkItAsNotRelevantAndLevel3OrAboveStartedBetweenSept2014AndAug2019_MarksLevel2AsFurtherActionRequired() + public async Task Index_ValidateAdditionalQuestions_Valid_Calls_QualificationLevel3OrAboveMightBeRelevantAtLevel2() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - const int level = 6; - const int startMonth = 6; - const int startYear = 2016; - - var additionalRequirementQuestions = new List - { - new() - { - Sys = new SystemProperties - { - Id = "Some Id" - }, - Question = "Have they got pediatric first aid?", - AnswerToBeFullAndRelevant = true - } - }; - - //Answer here makes this qual not full and relevant - var listOfAdditionalReqsAnswered = new Dictionary - { - { "Have they got pediatric first aid?", "no" } - }; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel2BetweenSept14AndAug19 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Level 3 Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Level 6 Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - // Note that every qualification will be marked as relevant for unqualified work, this still counts as not F and R. - new() - { - RatioRequirementName = "Unqualified Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - RatioRequirements = ratioRequirements, - AdditionalRequirementQuestions = additionalRequirementQuestions - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(new DetailsPage - { - BackToConfirmAnswers = new NavigationLink - { - Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" - } - }); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((startMonth, startYear)); - mockUserJourneyCookieService.Setup(x => x.WasStartedBetweenSeptember2014AndAugust2019()).Returns(true); - - mockUserJourneyCookieService.Setup(x => x.GetAdditionalQuestionsAnswers()) - .Returns(listOfAdditionalReqsAnswered); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); + const string qualificationId = "qualificationId"; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(o => o.GetDetailsPage()).ReturnsAsync(DummyDetailsPage); + _mockQualificationDetailsService.Setup(o => o.GetQualification(qualificationId)).ReturnsAsync(DummyQualification); + _mockQualificationDetailsService.Setup(o => o.MapDetails(It.IsAny(), It.IsAny())).ReturnsAsync(DummyDetails); - result.Should().NotBeNull(); + var sut = GetSut(); + _ = await sut.Index(qualificationId); - var resultType = result as ViewResult; - resultType.Should().NotBeNull(); - - var model = resultType!.Model as QualificationDetailsModel; - model.Should().NotBeNull(); - - model!.QualificationId.Should().Be(qualificationResult.QualificationId); - model.QualificationName.Should().Be(qualificationResult.QualificationName); - model.AwardingOrganisationTitle.Should().Be(qualificationResult.AwardingOrganisationTitle); - model.QualificationLevel.Should().Be(qualificationResult.QualificationLevel); - model.FromWhichYear.Should().Be(qualificationResult.FromWhichYear); - model.QualificationNumber.Should().Be(qualificationResult.QualificationNumber); - - model.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.FurtherActionRequired); - model.RatioRequirements.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); + _mockQualificationDetailsService.Verify(o => o.QualificationLevel3OrAboveMightBeRelevantAtLevel2(It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] - public async Task Index_AllRatiosFound_ReturnsView() + public async Task Index_ValidateAdditionalQuestions_Valid_ReturnsView() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-145"; - const int level = 2; - const int startDateYear = 2022; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForLevel2After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Level 3 Ratio Requirements", - FullAndRelevantForLevel2After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Level 6 Ratio Requirements", - FullAndRelevantForLevel2After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Unqualified Ratio Requirements", - FullAndRelevantForLevel2After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - RatioRequirements = ratioRequirements - }; - - var detailsPage = new DetailsPage - { - BackToConfirmAnswers = new NavigationLink - { - Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" - } - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(detailsPage); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()) - .Returns((9, startDateYear)); - - mockUserJourneyCookieService.Setup(x => x.WasStartedBeforeSeptember2014()) - .Returns(false); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); + const string qualificationId = "qualificationId"; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(o => o.GetDetailsPage()).ReturnsAsync(DummyDetailsPage); + _mockQualificationDetailsService.Setup(o => o.GetQualification(qualificationId)).ReturnsAsync(DummyQualification); + _mockQualificationDetailsService.Setup(o => o.MapDetails(It.IsAny(), It.IsAny())).ReturnsAsync(DummyDetails); - result.Should().NotBeNull(); + var sut = GetSut(); + var result = await sut.Index(qualificationId); var resultType = result as ViewResult; resultType.Should().NotBeNull(); var model = resultType!.Model as QualificationDetailsModel; model.Should().NotBeNull(); + } - model!.QualificationId.Should().Be(qualificationResult.QualificationId); - model.QualificationName.Should().Be(qualificationResult.QualificationName); - model.AwardingOrganisationTitle.Should().Be(qualificationResult.AwardingOrganisationTitle); - model.QualificationLevel.Should().Be(qualificationResult.QualificationLevel); - model.FromWhichYear.Should().Be(qualificationResult.FromWhichYear); - model.QualificationNumber.Should().Be(qualificationResult.QualificationNumber); - - model.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); + [TestMethod] + public async Task Index_ValidateAdditionalQuestions_InValid_Calls_QualificationDetailsService_QualificationLevel3OrAboveMightBeRelevantAtLevel2() + { + const string qualificationId = "qualificationId"; + var details = new QualificationDetailsModel { AdditionalRequirementAnswers = [new AdditionalRequirementAnswerModel()] }; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(o => o.GetDetailsPage()).ReturnsAsync(DummyDetailsPage); + _mockQualificationDetailsService.Setup(o => o.GetQualification(qualificationId)).ReturnsAsync(DummyQualification); + _mockQualificationDetailsService.Setup(o => o.MapDetails(It.IsAny(), It.IsAny())).ReturnsAsync(details); + _mockQualificationDetailsService.Setup(o => o.AnswersIndicateNotFullAndRelevant(It.IsAny>())).Returns(true); + + var sut = GetSut(); + _ = await sut.Index(qualificationId); + + _mockQualificationDetailsService.Verify(o => o.QualificationLevel3OrAboveMightBeRelevantAtLevel2(It.IsAny(), It.IsAny()), Times.Once); } [TestMethod] - public async Task Index_QualificationContainsQts_UserAnswerDoesntMatch_NotApprovedAtL6() + public async Task Index_ValidateAdditionalQuestions_InValid_Returns_View() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-148"; - const int level = 6; - const int startDateYear = 2022; - const string requirementsForLevel = "Test"; - - var additionalRequirementQuestions = new List - { - new() - { - Sys = new SystemProperties - { - Id = AdditionalRequirementQuestions.QtsQuestion - }, - Question = "This is the Qts Question", - AnswerToBeFullAndRelevant = true - }, - new() - { - Sys = new SystemProperties - { - Id = "Some other Id" - }, - Question = "Have they got pediatric first aid?", - AnswerToBeFullAndRelevant = true - } - }; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Level 3 Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Level 6 Ratio Requirements", - FullAndRelevantForLevel6After2014 = false, - RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Unqualified Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - } - }; - - var listOfAdditionalReqsAnswered = new Dictionary - { - { "This is the Qts Question", "no" }, - { "Have they got pediatric first aid?", "yes" } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - AdditionalRequirementQuestions = additionalRequirementQuestions, - RatioRequirements = ratioRequirements - }; - - var detailsPage = new DetailsPage - { - BackToConfirmAnswers = new NavigationLink - { - Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" - } - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(detailsPage); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()) - .Returns((9, startDateYear)); - - mockUserJourneyCookieService.Setup(x => x.WasStartedBeforeSeptember2014()) - .Returns(false); - - mockUserJourneyCookieService.Setup(x => x.GetAdditionalQuestionsAnswers()) - .Returns(listOfAdditionalReqsAnswered); - - mockContentParser.Setup(x => x.ToHtml(It.IsAny())).ReturnsAsync(requirementsForLevel); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); + const string qualificationId = "qualificationId"; + var details = new QualificationDetailsModel { AdditionalRequirementAnswers = [new AdditionalRequirementAnswerModel()] }; + _mockQualificationDetailsService.Setup(o => o.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(o => o.GetDetailsPage()).ReturnsAsync(DummyDetailsPage); + _mockQualificationDetailsService.Setup(o => o.GetQualification(qualificationId)).ReturnsAsync(DummyQualification); + _mockQualificationDetailsService.Setup(o => o.MapDetails(It.IsAny(), It.IsAny())).ReturnsAsync(details); + _mockQualificationDetailsService.Setup(o => o.AnswersIndicateNotFullAndRelevant(It.IsAny>())).Returns(true); - result.Should().NotBeNull(); + var sut = GetSut(); + var result = await sut.Index(qualificationId); var resultType = result as ViewResult; resultType.Should().NotBeNull(); var model = resultType!.Model as QualificationDetailsModel; model.Should().NotBeNull(); - - model!.QualificationId.Should().Be(qualificationResult.QualificationId); - model.QualificationName.Should().Be(qualificationResult.QualificationName); - model.AwardingOrganisationTitle.Should().Be(qualificationResult.AwardingOrganisationTitle); - model.QualificationLevel.Should().Be(qualificationResult.QualificationLevel); - model.FromWhichYear.Should().Be(qualificationResult.FromWhichYear); - model.QualificationNumber.Should().Be(qualificationResult.QualificationNumber); - - model.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); - - model.RatioRequirements.RequirementsForLevel6.Should().Be(requirementsForLevel); - model.RatioRequirements.ShowRequirementsForLevel6ByDefault.Should().BeTrue(); } [TestMethod] - public async Task Index_QualificationContainsQts_UserAnswerMatches_ApprovedAtL6() + public async Task Index_QualificationHasAdditionalQuestionsButNoneAnswered_RedirectTotTheAdditionalQuestionsPage() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-148"; - const int level = 6; - const int startDateYear = 2022; - const string requirementsForLevel = "Test"; - - var additionalRequirementQuestions = new List - { - new() - { - Sys = new SystemProperties - { - Id = AdditionalRequirementQuestions.QtsQuestion - }, - Question = "This is the Qts Question", - AnswerToBeFullAndRelevant = true - }, - new() - { - Sys = new SystemProperties - { - Id = "Some other Id" - }, - Question = "Have they got pediatric first aid?", - AnswerToBeFullAndRelevant = true - } - }; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForQtsEtcAfter2014 = true, - RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Level 3 Ratio Requirements", - FullAndRelevantForQtsEtcAfter2014 = true, - RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Level 6 Ratio Requirements", - FullAndRelevantForQtsEtcAfter2014 = true, - RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Unqualified Ratio Requirements", - FullAndRelevantForQtsEtcAfter2014 = true, - RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - } - }; - - var listOfAdditionalReqsAnswered = new Dictionary - { - { "This is the Qts Question", "yes" }, - { "Have they got pediatric first aid?", "yes" } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - AdditionalRequirementQuestions = additionalRequirementQuestions, - RatioRequirements = ratioRequirements - }; - - var detailsPage = new DetailsPage - { - BackToConfirmAnswers = new NavigationLink - { - Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" - } - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(detailsPage); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()) - .Returns((9, startDateYear)); - - mockUserJourneyCookieService.Setup(x => x.WasStartedBeforeSeptember2014()) - .Returns(false); - - mockUserJourneyCookieService.Setup(x => x.GetAdditionalQuestionsAnswers()) - .Returns(listOfAdditionalReqsAnswered); - - mockContentParser.Setup(x => x.ToHtml(It.IsAny())).ReturnsAsync(requirementsForLevel); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); + const string qualificationId = "qualificationId"; - result.Should().NotBeNull(); + var qualificationDetailsModel = new QualificationDetailsModel { QualificationId = qualificationId, AdditionalRequirementAnswers = [] }; - var resultType = result as ViewResult; - resultType.Should().NotBeNull(); + _mockQualificationDetailsService.Setup(x => x.GetQualification(qualificationId)).ReturnsAsync(DummyQualification); + _mockQualificationDetailsService.Setup(x => x.GetDetailsPage()).ReturnsAsync(DummyDetailsPage); - var model = resultType!.Model as QualificationDetailsModel; - model.Should().NotBeNull(); + _mockQualificationDetailsService.Setup(x => x.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(x => x.MapDetails(It.IsAny(), It.IsAny())).ReturnsAsync(qualificationDetailsModel); + _mockQualificationDetailsService.Setup(x => x.DoAdditionalAnswersMatchQuestions(It.IsAny())).Returns(true); - model!.QualificationId.Should().Be(qualificationResult.QualificationId); - model.QualificationName.Should().Be(qualificationResult.QualificationName); - model.AwardingOrganisationTitle.Should().Be(qualificationResult.AwardingOrganisationTitle); - model.QualificationLevel.Should().Be(qualificationResult.QualificationLevel); - model.FromWhichYear.Should().Be(qualificationResult.FromWhichYear); - model.QualificationNumber.Should().Be(qualificationResult.QualificationNumber); + var sut = GetSut(); - model.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); + var result = await sut.Index(qualificationId); - model.AdditionalRequirementAnswers.Should().NotBeNull(); - model.AdditionalRequirementAnswers!.Count.Should().Be(1); + var resultType = result as RedirectToActionResult; + resultType.Should().NotBeNull(); + resultType!.ActionName.Should().Be("Index"); + resultType.ControllerName.Should().Be("CheckAdditionalRequirements"); + resultType.RouteValues.Should().Contain("qualificationId", qualificationId); + resultType.RouteValues.Should().Contain("questionIndex", 1); } [TestMethod] - public async Task Index_QualificationIsAutomaticallyApprovedAtL6_ApprovedAtL6() + public async Task Index_QualificationHasAdditionalQuestionsButAnswersAreNotCorrect_MarkAsNotRelevantAndReturn() { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-148"; - const int level = 6; - const int startDateYear = 2022; - const string requirementsForLevel = "Test"; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForQtsEtcAfter2014 = true, - RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Level 3 Ratio Requirements", - FullAndRelevantForQtsEtcAfter2014 = true, - RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Level 6 Ratio Requirements", - FullAndRelevantForQtsEtcAfter2014 = true, - RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Unqualified Ratio Requirements", - FullAndRelevantForQtsEtcAfter2014 = true, - RequirementForQtsEtcAfter2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - } - }; - - var listOfAdditionalReqsAnswered = new Dictionary - { - { "This is the Qts Question", "yes" }, - { "Have they got pediatric first aid?", "yes" } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - IsAutomaticallyApprovedAtLevel6 = true, - RatioRequirements = ratioRequirements - }; - - var detailsPage = new DetailsPage - { - BackToConfirmAnswers = new NavigationLink - { - Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" - } - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(detailsPage); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()) - .Returns((9, startDateYear)); - - mockUserJourneyCookieService.Setup(x => x.WasStartedBeforeSeptember2014()) - .Returns(false); - - mockUserJourneyCookieService.Setup(x => x.GetAdditionalQuestionsAnswers()) - .Returns(listOfAdditionalReqsAnswered); - - mockContentParser.Setup(x => x.ToHtml(It.IsAny())).ReturnsAsync(requirementsForLevel); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); + const string qualificationId = "qualificationId"; - result.Should().NotBeNull(); + var details = new QualificationDetailsModel { AdditionalRequirementAnswers = [] }; - var resultType = result as ViewResult; - resultType.Should().NotBeNull(); + _mockQualificationDetailsService.Setup(x => x.GetQualification(qualificationId)).ReturnsAsync(DummyQualification); + _mockQualificationDetailsService.Setup(x => x.GetDetailsPage()).ReturnsAsync(new DetailsPage()); + _mockQualificationDetailsService.Setup(x => x.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(x => x.MapDetails(It.IsAny(), It.IsAny())).ReturnsAsync(details); + _mockQualificationDetailsService.Setup(x => x.AnswersIndicateNotFullAndRelevant(It.IsAny>())).Returns(true); - var model = resultType!.Model as QualificationDetailsModel; - model.Should().NotBeNull(); + var sut = GetSut(); - model!.QualificationId.Should().Be(qualificationResult.QualificationId); - model.QualificationName.Should().Be(qualificationResult.QualificationName); - model.AwardingOrganisationTitle.Should().Be(qualificationResult.AwardingOrganisationTitle); - model.QualificationLevel.Should().Be(qualificationResult.QualificationLevel); - model.FromWhichYear.Should().Be(qualificationResult.FromWhichYear); - model.QualificationNumber.Should().Be(qualificationResult.QualificationNumber); - - model.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.Approved); - model.RatioRequirements.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); - } + var result = await sut.Index(qualificationId); - [TestMethod] - public async Task Index_QualificationContainsQts_UserAnswerDoesntMatch_OnlyApprovedAtUnqualified() - { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - var mockUserJourneyCookieService = new Mock(); - - const string qualificationId = "eyq-148"; - const int level = 6; - const int startDateYear = 2022; - const string requirementsForLevel = "Test"; - - var additionalRequirementQuestions = new List - { - new() - { - Sys = new SystemProperties - { - Id = AdditionalRequirementQuestions.QtsQuestion - }, - Question = "This is the Qts Question", - AnswerToBeFullAndRelevant = true - }, - new() - { - Sys = new SystemProperties - { - Id = "Some other Id" - }, - Question = "Have they got pediatric first aid?", - AnswerToBeFullAndRelevant = true - } - }; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Level 3 Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Level 6 Ratio Requirements", - FullAndRelevantForLevel6After2014 = false, - RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - }, - new() - { - RatioRequirementName = "Unqualified Ratio Requirements", - FullAndRelevantForLevel6After2014 = true, - RequirementForLevel6After2014 = ContentfulContentHelper.Paragraph(requirementsForLevel) - } - }; - - var listOfAdditionalReqsAnswered = new Dictionary - { - { "This is the Qts Question", "no" }, - { "Have they got pediatric first aid?", "no" } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - AdditionalRequirementQuestions = additionalRequirementQuestions, - RatioRequirements = ratioRequirements - }; - - var detailsPage = new DetailsPage - { - BackToConfirmAnswers = new NavigationLink - { - Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" - } - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(detailsPage); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()) - .Returns((9, startDateYear)); - - mockUserJourneyCookieService.Setup(x => x.WasStartedBeforeSeptember2014()) - .Returns(false); - - mockUserJourneyCookieService.Setup(x => x.GetAdditionalQuestionsAnswers()) - .Returns(listOfAdditionalReqsAnswered); - - mockContentParser.Setup(x => x.ToHtml(It.IsAny())).ReturnsAsync(requirementsForLevel); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); + _mockQualificationDetailsService.Verify(o => o.QualificationContainsQtsQuestion(It.IsAny()), Times.Once); + _mockQualificationDetailsService.Verify(o => o.DoAdditionalAnswersMatchQuestions(It.IsAny()), Times.Once); + _mockQualificationDetailsService.Verify(o => o.AnswersIndicateNotFullAndRelevant(It.IsAny>()), Times.Once); + _mockQualificationDetailsService.Verify(o => o.MarkAsNotFullAndRelevant(It.IsAny()), Times.Once); + _mockQualificationDetailsService.Verify(o => o.QualificationLevel3OrAboveMightBeRelevantAtLevel2(It.IsAny(), It.IsAny()), Times.Once); result.Should().NotBeNull(); - var resultType = result as ViewResult; resultType.Should().NotBeNull(); - - var model = resultType!.Model as QualificationDetailsModel; - model.Should().NotBeNull(); - - model!.QualificationId.Should().Be(qualificationResult.QualificationId); - model.QualificationName.Should().Be(qualificationResult.QualificationName); - model.AwardingOrganisationTitle.Should().Be(qualificationResult.AwardingOrganisationTitle); - model.QualificationLevel.Should().Be(qualificationResult.QualificationLevel); - model.FromWhichYear.Should().Be(qualificationResult.FromWhichYear); - model.QualificationNumber.Should().Be(qualificationResult.QualificationNumber); - - model.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.NotApproved); - model.RatioRequirements.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); - - model.RatioRequirements.RequirementsForLevel6.Should().Be(requirementsForLevel); - model.RatioRequirements.ShowRequirementsForLevel6ByDefault.Should().BeTrue(); } [TestMethod] - public async Task Index_BackToAdditionalQuestionsLinkIncludesQualificationId() + [DataRow(true)] + [DataRow(false)] + public async Task Index_QualificationHasAdditionalQuestionsButAnswers_ContainsQtsQuestion(bool userAnswerIsFullAndRelevant) { - var mockLogger = new Mock>(); - var mockRepository = new Mock(); - var mockContentService = new Mock(); - var mockContentParser = new Mock(); - - var mockUserJourneyCookieService = new Mock(); - mockUserJourneyCookieService.Setup(x => x.UserHasAnsweredAdditionalQuestions()) - .Returns(true); - - const string qualificationId = "eyq-145"; - const int level = 2; - const int startDateYear = 2022; - - var ratioRequirements = new List - { - new() - { - RatioRequirementName = "Level 2 Ratio Requirements", - FullAndRelevantForLevel2After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Level 3 Ratio Requirements", - FullAndRelevantForLevel2After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Level 6 Ratio Requirements", - FullAndRelevantForLevel2After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - }, - new() - { - RatioRequirementName = "Unqualified Ratio Requirements", - FullAndRelevantForLevel2After2014 = true, - RequirementForLevel2After2014 = ContentfulContentHelper.Paragraph("Test") - } - }; - - var qualificationResult = new Qualification(qualificationId, - "Qualification Name", - AwardingOrganisations.Ncfe, - level) - { - FromWhichYear = "2014", ToWhichYear = "2019", - QualificationNumber = "ABC/547/900", - AdditionalRequirements = "additional requirements", - RatioRequirements = ratioRequirements - }; - - var detailsPage = new DetailsPage - { - BackToConfirmAnswers = new NavigationLink - { - Href = "/qualifications/check-additional-questions/$[qualification-id]$/confirm-answers" - } - }; - - mockRepository.Setup(x => x.GetById(qualificationId)) - .ReturnsAsync(qualificationResult); - - mockContentService.Setup(x => x.GetDetailsPage()) - .ReturnsAsync(detailsPage); - - mockUserJourneyCookieService.Setup(x => x.GetWhenWasQualificationStarted()).Returns((9, startDateYear)); - mockUserJourneyCookieService.Setup(x => x.WasStartedBeforeSeptember2014()) - .Returns(false); - - var controller = - new QualificationDetailsController(mockLogger.Object, - mockRepository.Object, - mockContentService.Object, - mockContentParser.Object, - mockUserJourneyCookieService.Object) - { - ControllerContext = new ControllerContext - { - HttpContext = new DefaultHttpContext() - } - }; - - var result = await controller.Index(qualificationId); - - result.Should().NotBeNull(); - - var resultType = result as ViewResult; - resultType.Should().NotBeNull(); - - var model = resultType!.Model as QualificationDetailsModel; - - model!.BackButton!.Href.Should().Be("/qualifications/check-additional-questions/eyq-145/confirm-answers"); + const string qualificationId = "qualificationId"; + + var qualification = new Qualification(qualificationId, It.IsAny(), It.IsAny(), It.IsAny()) + { + AdditionalRequirementQuestions = new List + { + new() + { + Question = "QtsQuestion", + Sys = new SystemProperties + { + Id = AdditionalRequirementQuestions.QtsQuestion + } + }, + + new() + { + Question = "Question 1", + Sys = new SystemProperties + { + Id = "abcde" + } + } + } + }; + var qtsQuestion = qualification.AdditionalRequirementQuestions.First(o => o.Sys.Id == AdditionalRequirementQuestions.QtsQuestion); + var details = new QualificationDetailsModel + { + AdditionalRequirementAnswers = + [ + new AdditionalRequirementAnswerModel + { + Question = qtsQuestion.Question + }, + new AdditionalRequirementAnswerModel + { + Question = "Question 1" + } + ] + }; + + var notQtsAnswer = details.AdditionalRequirementAnswers.First(o => o.Question == "Question 1"); + + _mockQualificationDetailsService.Setup(x => x.GetQualification(qualificationId)).ReturnsAsync(qualification); + _mockQualificationDetailsService.Setup(x => x.GetDetailsPage()).ReturnsAsync(new DetailsPage()); + _mockQualificationDetailsService.Setup(x => x.HasStartDate()).Returns(true); + _mockQualificationDetailsService.Setup(x => x.MapDetails(It.IsAny(), It.IsAny())).ReturnsAsync(details); + _mockQualificationDetailsService.Setup(x => x.QualificationContainsQtsQuestion(It.IsAny())).Returns(true); + _mockQualificationDetailsService.Setup(x => x.UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, details.AdditionalRequirementAnswers)).Returns(userAnswerIsFullAndRelevant); + + var sut = GetSut(); + + var result = await sut.Index(qualificationId); + + _mockQualificationDetailsService.Verify(o => o.UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, details.AdditionalRequirementAnswers), Times.Once); + + if (userAnswerIsFullAndRelevant) + { + details.AdditionalRequirementAnswers.Should().NotContain(notQtsAnswer); + result.Should().NotBeNull(); + _mockQualificationDetailsService.Verify(o=>o.RemainingAnswersIndicateFullAndRelevant(details,qtsQuestion), Times.Never); + } + else + { + _mockQualificationDetailsService.Verify(o=>o.RemainingAnswersIndicateFullAndRelevant(details,qtsQuestion), Times.Once); + details.AdditionalRequirementAnswers.Should().Contain(notQtsAnswer); + } } } \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Extensions/ActionResultExtensions.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Extensions/ActionResultExtensions.cs new file mode 100644 index 00000000..ec0f1512 --- /dev/null +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Extensions/ActionResultExtensions.cs @@ -0,0 +1,13 @@ + +namespace Dfe.EarlyYearsQualification.UnitTests.Extensions; + +public static class ActionResultExtensions +{ + public static void VerifyRedirect(this IActionResult actionResult, string action, string controller) + { + actionResult.Should().BeAssignableTo(); + var redirect = (RedirectToActionResult)actionResult!; + redirect.ActionName.Should().Be(action); + redirect.ControllerName.Should().Be(controller); + } +} \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/GlobalUsings.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/GlobalUsings.cs index ab67c7ea..721fbb70 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/GlobalUsings.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/GlobalUsings.cs @@ -1 +1,6 @@ -global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using FluentAssertions; +global using Moq; +global using Microsoft.AspNetCore.Mvc; +global using Dfe.EarlyYearsQualification.UnitTests.Extensions; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationDetailsServiceTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationDetailsServiceTests.cs new file mode 100644 index 00000000..af66d0f1 --- /dev/null +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationDetailsServiceTests.cs @@ -0,0 +1,765 @@ +using System.Drawing.Printing; +using Contentful.Core.Models; +using Dfe.EarlyYearsQualification.Content.Constants; +using Dfe.EarlyYearsQualification.Content.Entities; +using Dfe.EarlyYearsQualification.Content.RichTextParsing; +using Dfe.EarlyYearsQualification.Content.Services.Interfaces; +using Dfe.EarlyYearsQualification.Web.Models; +using Dfe.EarlyYearsQualification.Web.Models.Content; +using Dfe.EarlyYearsQualification.Web.Services.QualificationDetails; +using Dfe.EarlyYearsQualification.Web.Services.UserJourneyCookieService; + +namespace Dfe.EarlyYearsQualification.UnitTests.Services; + +[TestClass] +public class QualificationDetailsServiceTests +{ + private Mock> _mockLogger; + private Mock _mockRepository = new(); + private Mock _mockContentService = new(); + private Mock _mockContentParser = new(); + private Mock _mockUserJourneyCookieService = new(); + + private IQualificationDetailsService GetSut() => new QualificationDetailsService( + _mockLogger.Object, + _mockRepository.Object, + _mockContentService.Object, + _mockContentParser.Object, + _mockUserJourneyCookieService.Object + ); + + [TestInitialize] + public void Initialize() + { + _mockLogger = new Mock>(); + _mockRepository = new Mock(); + _mockContentService = new Mock(); + _mockContentParser = new Mock(); + _mockUserJourneyCookieService = new Mock(); + } + + [TestMethod] + public async Task GetQualification_Calls_Repository_GetById() + { + const string qualificationId = "qualificationId"; + var sut = GetSut(); + + _ = await sut.GetQualification(qualificationId); + + _mockRepository.Verify(o => o.GetById(qualificationId), Times.Once); + } + + [TestMethod] + public async Task GetDetailsPage_Calls_Content_GetDetailsPage() + { + var sut = GetSut(); + + _ = await sut.GetDetailsPage(); + + _mockContentService.Verify(o => o.GetDetailsPage(), Times.Once); + } + + [TestMethod] + public void HasStartDate_Calls_Cookies_GetWhenQualificationStarted() + { + var sut = GetSut(); + + _ = sut.HasStartDate(); + + _mockUserJourneyCookieService.Verify(o => o.GetWhenWasQualificationStarted(), Times.Once); + } + + [TestMethod] + [DataRow(null, null)] + [DataRow(1, null)] + [DataRow(null, 1)] + public void HasStartDate_NullDates_ReturnsFalse(int? month, int? year) + { + _mockUserJourneyCookieService.Setup(o => o.GetWhenWasQualificationStarted()).Returns((month, year)); + var sut = GetSut(); + + var result = sut.HasStartDate(); + + result.Should().BeFalse(); + } + + [TestMethod] + [DataRow(1, 1)] + public void HasStartDate_GotDates_ReturnsTrue(int? month, int? year) + { + _mockUserJourneyCookieService.Setup(o => o.GetWhenWasQualificationStarted()).Returns((month, year)); + var sut = GetSut(); + + var result = sut.HasStartDate(); + + result.Should().BeTrue(); + } + + [TestMethod] + public async Task GetFeedbackBannerToHtml_NullBanner_ReturnsNull() + { + FeedbackBanner? feedbackBanner = null; + var sut = GetSut(); + + var result = await sut.GetFeedbackBannerBodyToHtml(feedbackBanner); + + result.Should().BeNull(); + } + + [TestMethod] + public async Task GetFeedbackBannerToHtml_GotBanner_CallsContentParser() + { + const string expectedContent = "

Feedback banner

"; + var feedbackBanner = new FeedbackBanner { Body = new Document() }; + _mockContentParser.Setup(o => o.ToHtml(feedbackBanner.Body)).ReturnsAsync(expectedContent); + var sut = GetSut(); + + var result = await sut.GetFeedbackBannerBodyToHtml(feedbackBanner); + + _mockContentParser.Verify(o => o.ToHtml(feedbackBanner.Body), Times.Once); + result.Should().BeEquivalentTo(expectedContent); + } + + [TestMethod] + public void QualificationContainsQtsQuestion_NullQuestions_ReturnsFalse() + { + var qualification = new Qualification(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + { + AdditionalRequirementQuestions = null + }; + + var sut = GetSut(); + + var result = sut.QualificationContainsQtsQuestion(qualification); + + result.Should().BeFalse(); + } + + [TestMethod] + [DataRow("abcde", false)] + [DataRow(AdditionalRequirementQuestions.QtsQuestion, true)] + [DataRow("uwxyz", false)] + public void QualificationContainsQtsQuestion_GotQuestions_ReturnsTrueIfQts(string questionId, bool expectedResult) + { + var qualification = new Qualification(It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + { + AdditionalRequirementQuestions = + [ + new AdditionalRequirementQuestion + { + Sys = new SystemProperties() { Id = questionId } + } + ] + }; + + var sut = GetSut(); + + var result = sut.QualificationContainsQtsQuestion(qualification); + + result.Should().Be(expectedResult); + } + + [TestMethod] + public void MarkAsNotFullAndRelevant_Marks_Correctly() + { + var model = new RatioRequirementModel(); + var sut = GetSut(); + + var result = sut.MarkAsNotFullAndRelevant(model); + + result.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.NotApproved); + result.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.NotApproved); + result.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.NotApproved); + result.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); + } + + [TestMethod] + public void DoAdditionalAnswersMatchQuestions_NoAnswers_ReturnsTrue() + { + var details = new QualificationDetailsModel + { + AdditionalRequirementAnswers = [] + }; + + var sut = GetSut(); + + var result = sut.DoAdditionalAnswersMatchQuestions(details); + + result.Should().BeTrue(); + } + + [TestMethod] + [DataRow(null, true)] + [DataRow("abcde", false)] + [DataRow("", true)] + [DataRow("uwxyz", false)] + public void DoAdditionalAnswersMatchQuestions_GotAnswers_ReturnsTrueIfExists(string answer, bool expectedResult) + { + var details = new QualificationDetailsModel + { + AdditionalRequirementAnswers = + [ + new AdditionalRequirementAnswerModel + { + Answer = answer + } + ] + }; + + var sut = GetSut(); + + var result = sut.DoAdditionalAnswersMatchQuestions(details); + + result.Should().Be(expectedResult); + } + + [TestMethod] + public void CalculateBackButton_Calls_Cookies_UserHasAnsweredAdditionalQuestions() + { + var detailsPage = new DetailsPage(); + const string qualificationId = "qualificationId"; + var sut = GetSut(); + + _ = sut.CalculateBackButton(detailsPage, qualificationId); + + _mockUserJourneyCookieService.Verify(o => o.UserHasAnsweredAdditionalQuestions(), Times.Once); + } + + [TestMethod] + public void CalculateBackButton_HasAnsweredAdditionalQuestions_BackToConfirmAnswersNull_ReturnsBackButton() + { + var detailsPage = new DetailsPage + { + BackToConfirmAnswers = null, + BackButton = new NavigationLink() + }; + const string qualificationId = "qualificationId"; + _mockUserJourneyCookieService.Setup(o => o.UserHasAnsweredAdditionalQuestions()).Returns(true); + var sut = GetSut(); + + var result = sut.CalculateBackButton(detailsPage, qualificationId); + + result.Should().Be(detailsPage.BackButton); + } + + [TestMethod] + public void CalculateBackButton_HasAnsweredAdditionalQuestions_BackToConfirmAnswersNotNull_ReturnsExpectedBackButton() + { + const string qualificationId = "qualificationId"; + const string expectedHref = "qualificationId LINK"; + + var detailsPage = new DetailsPage { BackToConfirmAnswers = new NavigationLink { Href = "$[qualification-id]$ LINK" } }; + + _mockUserJourneyCookieService.Setup(o => o.UserHasAnsweredAdditionalQuestions()).Returns(true); + var sut = GetSut(); + + var result = sut.CalculateBackButton(detailsPage, qualificationId); + result.Should().NotBeNull(); + result!.Href.Should().BeEquivalentTo(expectedHref); + } + + [TestMethod] + public void CalculateBackButton_NoAdditionalQuestions_Calls_Cookies_GetLevelOfQualification() + { + const string qualificationId = "qualificationId"; + + var detailsPage = new DetailsPage(); + + _mockUserJourneyCookieService.Setup(o => o.UserHasAnsweredAdditionalQuestions()).Returns(false); + var sut = GetSut(); + + _ = sut.CalculateBackButton(detailsPage, qualificationId); + + _mockUserJourneyCookieService.Verify(o => o.GetLevelOfQualification(), Times.Once); + } + + [TestMethod] + [DataRow(YesOrNo.No, 1, false)] + [DataRow(YesOrNo.No, 2, false)] + [DataRow(YesOrNo.No, 3, false)] + [DataRow(YesOrNo.No, 4, false)] + [DataRow(YesOrNo.No, 5, false)] + [DataRow(YesOrNo.No, 6, true)] + [DataRow(YesOrNo.Yes, 1, false)] + [DataRow(YesOrNo.Yes, 2, false)] + [DataRow(YesOrNo.Yes, 3, false)] + [DataRow(YesOrNo.Yes, 4, false)] + [DataRow(YesOrNo.Yes, 5, false)] + [DataRow(YesOrNo.Yes, 6, false)] + public void CalculateBackButton_NoAdditionalQuestions_Calls_Cookies_WasStartedBeforeSeptember2014_WhenItShould(YesOrNo selectedFromList, int level, bool shouldCall) + { + const string qualificationId = "qualificationId"; + + var detailsPage = new DetailsPage(); + + _mockUserJourneyCookieService.Setup(o => o.UserHasAnsweredAdditionalQuestions()).Returns(false); + _mockUserJourneyCookieService.Setup(o => o.GetQualificationWasSelectedFromList()).Returns(selectedFromList); + _mockUserJourneyCookieService.Setup(o => o.GetLevelOfQualification()).Returns(level); + + var sut = GetSut(); + + _ = sut.CalculateBackButton(detailsPage, qualificationId); + + _mockUserJourneyCookieService.Verify(o => o.WasStartedBeforeSeptember2014(), Times.Exactly(shouldCall ? 1 : 0)); + } + + [TestMethod] + public void CalculateBackButton_NoAdditionalQuestions_Lvl6NotInList_BeforeSept2014_Returns_BackToLevelSixAdviceBefore2014() + { + const string qualificationId = "qualificationId"; + const string backToLevelSixAdviceBefore2014 = "backToLevelSixAdviceBefore2014"; + + var detailsPage = new DetailsPage { BackToLevelSixAdviceBefore2014 = new NavigationLink { Href = backToLevelSixAdviceBefore2014 } }; + + _mockUserJourneyCookieService.Setup(o => o.UserHasAnsweredAdditionalQuestions()).Returns(false); + _mockUserJourneyCookieService.Setup(o => o.GetQualificationWasSelectedFromList()).Returns(YesOrNo.No); + _mockUserJourneyCookieService.Setup(o => o.GetLevelOfQualification()).Returns(6); + _mockUserJourneyCookieService.Setup(o => o.WasStartedBeforeSeptember2014()).Returns(true); + + var sut = GetSut(); + + var result = sut.CalculateBackButton(detailsPage, qualificationId); + result.Should().NotBeNull(); + result!.Href.Should().BeEquivalentTo(backToLevelSixAdviceBefore2014); + } + + [TestMethod] + public void CalculateBackButton_NoAdditionalQuestions_Lvl6NotInList_AfterSept2014_Returns_BackToLevelSixAdvice() + { + const string qualificationId = "qualificationId"; + const string backToLevelSixAdvice = "backToLevelSixAdvice"; + + var detailsPage = new DetailsPage { BackToLevelSixAdvice = new NavigationLink { Href = backToLevelSixAdvice } }; + + _mockUserJourneyCookieService.Setup(o => o.UserHasAnsweredAdditionalQuestions()).Returns(false); + _mockUserJourneyCookieService.Setup(o => o.GetQualificationWasSelectedFromList()).Returns(YesOrNo.No); + _mockUserJourneyCookieService.Setup(o => o.GetLevelOfQualification()).Returns(6); + _mockUserJourneyCookieService.Setup(o => o.WasStartedBeforeSeptember2014()).Returns(false); + + var sut = GetSut(); + + var result = sut.CalculateBackButton(detailsPage, qualificationId); + result.Should().NotBeNull(); + result!.Href.Should().BeEquivalentTo(backToLevelSixAdvice); + } + + [TestMethod] + public void CalculateBackButton_NoAdditionalQuestions_NotLvl6NotInList_AfterSept2014_Returns_BackToLevelSixAdvice() + { + const string qualificationId = "qualificationId"; + const string backButton = "backButton"; + + var detailsPage = new DetailsPage { BackButton = new NavigationLink { Href = backButton } }; + + _mockUserJourneyCookieService.Setup(o => o.UserHasAnsweredAdditionalQuestions()).Returns(false); + _mockUserJourneyCookieService.Setup(o => o.GetQualificationWasSelectedFromList()).Returns(YesOrNo.Yes); + _mockUserJourneyCookieService.Setup(o => o.GetLevelOfQualification()).Returns(1); + + var sut = GetSut(); + + var result = sut.CalculateBackButton(detailsPage, qualificationId); + result.Should().NotBeNull(); + result!.Href.Should().BeEquivalentTo(backButton); + } + + [TestMethod] + [DataRow(true, "no", true)] + [DataRow(true, "yes", false)] + [DataRow(false, "no", false)] + [DataRow(false, "yes", true)] + public void AnswersIndicateNotFullAndRelevant(bool fullAndRelevant, string answer, bool expectedResult) + { + var additionalRequirementsAnswers = new List + { + new() + { + AnswerToBeFullAndRelevant = fullAndRelevant, + Answer = answer + } + }; + + var sut = GetSut(); + + var result = sut.AnswersIndicateNotFullAndRelevant(additionalRequirementsAnswers); + + result.Should().Be(expectedResult); + } + + [TestMethod] + public void UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant_NoAnswers_Returns_False() + { + var qualification = new Qualification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()); + List additionalRequirementAnswerModels = null!; + var sut = GetSut(); + + var result = sut.UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, additionalRequirementAnswerModels); + + result.Should().BeFalse(); + } + + [TestMethod] + [DataRow("yes", true, true)] + [DataRow("no", true, false)] + [DataRow("yes", false, false)] + [DataRow("no", false, true)] + public void UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant_Returns_AnswerAsBool(string answer, bool qtsFullAndRelevant, bool expectedResult) + { + var qts = new AdditionalRequirementQuestion + { + Sys = new SystemProperties + { + Id = AdditionalRequirementQuestions.QtsQuestion, + }, + AnswerToBeFullAndRelevant = qtsFullAndRelevant + }; + var qualification = new Qualification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + { + AdditionalRequirementQuestions = [qts] + }; + var additionalRequirementAnswerModels = new List { new() { Question = qts.Question, Answer = answer } }; + var sut = GetSut(); + + var result = sut.UserAnswerMatchesQtsQuestionAnswerToBeFullAndRelevant(qualification, additionalRequirementAnswerModels); + + result.Should().Be(expectedResult); + } + + [TestMethod] + public async Task QualificationLevel3OrAboveMightBeRelevantAtLevel2_DoesNotHitEdgeCase() + { + var details = new QualificationDetailsModel(); + var qualification = new Qualification(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()); + + var sut = GetSut(); + + await sut.QualificationLevel3OrAboveMightBeRelevantAtLevel2(details, qualification); + + details.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.Approved); + _mockContentParser.Verify(o => o.ToHtml(It.IsAny()), Times.Never); + details.RatioRequirements.RequirementsForLevel2.Should().Be(string.Empty); + details.RatioRequirements.ShowRequirementsForLevel2ByDefault.Should().BeFalse(); + } + + [TestMethod] + public async Task QualificationLevel3OrAboveMightBeRelevantAtLevel2_DoesHitEdgeCase() + { + var details = new QualificationDetailsModel + { + RatioRequirements = new RatioRequirementModel + { + ApprovedForLevel2 = QualificationApprovalStatus.NotApproved, + ApprovedForLevel3 = QualificationApprovalStatus.NotApproved, + ApprovedForLevel6 = QualificationApprovalStatus.NotApproved, + } + }; + var qualification = new Qualification(It.IsAny(), It.IsAny(), It.IsAny(), 3) + { + RatioRequirements = + [ + new RatioRequirement + { + RatioRequirementName = RatioRequirements.Level2RatioRequirementName + } + ] + }; + const string requirementsForLevel2 = "requirementsForLevel2"; + + _mockContentParser.Setup(o => o.ToHtml(It.IsAny())).ReturnsAsync(requirementsForLevel2); + _mockUserJourneyCookieService.Setup(o => o.WasStartedBetweenSeptember2014AndAugust2019()).Returns(true); + var sut = GetSut(); + + await sut.QualificationLevel3OrAboveMightBeRelevantAtLevel2(details, qualification); + + details.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.FurtherActionRequired); + _mockContentParser.Verify(o => o.ToHtml(It.IsAny()), Times.Once); + details.RatioRequirements.RequirementsForLevel2.Should().Be(requirementsForLevel2); + details.RatioRequirements.ShowRequirementsForLevel2ByDefault.Should().BeTrue(); + } + + [TestMethod] + public async Task QualificationLevel3OrAboveMightBeRelevantAtLevel2_MissingRequirements_Throws() + { + const string qualificationId = "qualificationId"; + var details = new QualificationDetailsModel + { + RatioRequirements = new RatioRequirementModel + { + ApprovedForLevel2 = QualificationApprovalStatus.NotApproved, + ApprovedForLevel3 = QualificationApprovalStatus.NotApproved, + ApprovedForLevel6 = QualificationApprovalStatus.NotApproved, + } + }; + var qualification = new Qualification(qualificationId, It.IsAny(), It.IsAny(), 3); + const string requirementsForLevel2 = "requirementsForLevel2"; + + _mockContentParser.Setup(o => o.ToHtml(It.IsAny())).ReturnsAsync(requirementsForLevel2); + _mockUserJourneyCookieService.Setup(o => o.WasStartedBetweenSeptember2014AndAugust2019()).Returns(true); + var sut = GetSut(); + + var action = async () => await sut.QualificationLevel3OrAboveMightBeRelevantAtLevel2(details, qualification); + + await action.Should().ThrowAsync(); + + _mockLogger.VerifyError("Could not find property: RequirementForLevel2BetweenSept14AndAug19 within Level 2 Ratio Requirements for qualification: qualificationId"); + } + + [TestMethod] + public void MapAdditionalRequirementAnswers_Null_ReturnsNull() + { + List additionalRequirementQuestions = null!; + + var sut = GetSut(); + + var result = sut.MapAdditionalRequirementAnswers(additionalRequirementQuestions); + + result.Should().BeNull(); + } + + [TestMethod] + public void MapAdditionalRequirementAnswers_Calls_Cookies_GetAdditionalQuestionsAnswers() + { + var additionalRequirementQuestions = new List(); + + var sut = GetSut(); + + _ = sut.MapAdditionalRequirementAnswers(additionalRequirementQuestions); + + _mockUserJourneyCookieService.Verify(o => o.GetAdditionalQuestionsAnswers(), Times.Once); + } + + [TestMethod] + public void MapAdditionalRequirementAnswers_NullAnswers_ReturnsEmpty() + { + var additionalRequirementQuestions = new List(); + + _mockUserJourneyCookieService.Setup(o => o.GetAdditionalQuestionsAnswers()).Returns((Dictionary)null!); + var sut = GetSut(); + + var result = sut.MapAdditionalRequirementAnswers(additionalRequirementQuestions); + + result.Should().BeEquivalentTo(new List()); + } + + [TestMethod] + public void MapAdditionalRequirementAnswers_MapsCorrectly() + { + var additionalRequirementQuestions = new List + { + new() + { + Question = "Question 1", + AnswerToBeFullAndRelevant = true, + ConfirmationStatement = "confirmation statement 1", + }, + new() + { + Question = "Question 2", + AnswerToBeFullAndRelevant = false, + ConfirmationStatement = "confirmation statement 2", + }, + new() + { + Question = "Question 3", + AnswerToBeFullAndRelevant = true, + ConfirmationStatement = "confirmation statement 3", + } + }; + + var userAnswers = new Dictionary + { + { "Question 1", "Answer 1" }, + { "Question 2", "Answer 2" }, + { "Question 3", "Answer 3" }, + }; + + var expected = new List + { + new() + { + Question = "Question 1", + AnswerToBeFullAndRelevant = true, + ConfirmationStatement = "confirmation statement 1", + Answer = "Answer 1" + }, + new() + { + Question = "Question 2", + AnswerToBeFullAndRelevant = false, + ConfirmationStatement = "confirmation statement 2", + Answer = "Answer 2" + }, + new() + { + Question = "Question 3", + AnswerToBeFullAndRelevant = true, + ConfirmationStatement = "confirmation statement 3", + Answer = "Answer 3" + } + }; + + _mockUserJourneyCookieService.Setup(o => o.GetAdditionalQuestionsAnswers()).Returns(userAnswers); + var sut = GetSut(); + + var result = sut.MapAdditionalRequirementAnswers(additionalRequirementQuestions); + + result.Should().BeEquivalentTo(expected); + } + + [TestMethod] + public void RemainingAnswersIndicateFullAndRelevant_FullAndRelevant_ReturnsExpected() + { + var qtsQuestion = new AdditionalRequirementQuestion { Question = "Qts" }; + var details = new QualificationDetailsModel + { + AdditionalRequirementAnswers = + [ + new AdditionalRequirementAnswerModel + { + Question = qtsQuestion.Question + }, + new AdditionalRequirementAnswerModel + { + AnswerToBeFullAndRelevant = true, + Answer = "yes" + } + ] + }; + var sut = GetSut(); + + var result = sut.RemainingAnswersIndicateFullAndRelevant(details, qtsQuestion); + + result.isFullAndRelevant.Should().BeTrue(); + result.details.RatioRequirements.ShowRequirementsForLevel6ByDefault.Should().BeTrue(); + } + + [TestMethod] + public void RemainingAnswersIndicateFullAndRelevant_NotFullAndRelevant_ReturnsExpected() + { + var qtsQuestion = new AdditionalRequirementQuestion { Question = "Qts" }; + var details = new QualificationDetailsModel + { + AdditionalRequirementAnswers = + [ + new AdditionalRequirementAnswerModel + { + Question = qtsQuestion.Question + }, + new AdditionalRequirementAnswerModel + { + AnswerToBeFullAndRelevant = true, + Answer = "no" + } + ] + }; + var sut = GetSut(); + + var result = sut.RemainingAnswersIndicateFullAndRelevant(details, qtsQuestion); + + result.isFullAndRelevant.Should().BeFalse(); + result.details.RatioRequirements.ShowRequirementsForLevel6ByDefault.Should().BeTrue(); + } + + [TestMethod] + public async Task CheckLevel6Requirements_ChecksCorrectly() + { + var qualification = new Qualification(It.IsAny(), It.IsAny(), It.IsAny(), 6) + { + RatioRequirements = + [ + new RatioRequirement + { + RatioRequirementName = RatioRequirements.Level6RatioRequirementName, + } + ] + }; + var details = new QualificationDetailsModel(); + + _mockUserJourneyCookieService.Setup(o => o.WasStartedBeforeSeptember2014()).Returns(true); + + var sut = GetSut(); + + var result = await sut.CheckLevel6Requirements(qualification, details); + + result.RatioRequirements.ApprovedForLevel2.Should().Be(QualificationApprovalStatus.NotApproved); + result.RatioRequirements.ApprovedForLevel3.Should().Be(QualificationApprovalStatus.NotApproved); + result.RatioRequirements.ApprovedForLevel6.Should().Be(QualificationApprovalStatus.NotApproved); + result.RatioRequirements.ApprovedForUnqualified.Should().Be(QualificationApprovalStatus.Approved); + + _mockContentParser.Verify(o => o.ToHtml(It.IsAny()), Times.Once); + } + + [TestMethod] + [DataRow(null, null, "")] + [DataRow(null, 2024, "")] + [DataRow(1, null, "")] + [DataRow(1, 2024, "January 2024")] + public async Task MapDetails_(int? month, int? year, string dateStarted) + { + const string qualificationId = "qualificationId"; + const string qualificationName = "qualificationName"; + const string awardingOrganisationTitle = "awardingOrganisationTitle"; + const int qualificationLevel = 1; + const string checkAnotherQualification = "checkAnotherQualification"; + const string furtherInfo = "furtherInfo"; + const string requirements = "requirements"; + const string ratios = "ratios"; + const string feedback = "feedback"; + var checkAnotherQualificationText = new Document { NodeType = checkAnotherQualification }; + var furtherInfoText = new Document { NodeType = furtherInfo }; + var requirementsText = new Document { NodeType = requirements }; + var ratiosText = new Document { NodeType = ratios }; + var feedbackText = new Document { NodeType = feedback }; + var feedbackBanner = new FeedbackBanner { Body = feedbackText }; + var backButton = new NavigationLink { Href = "backbutton" }; + var qualification = new Qualification(qualificationId, qualificationName, awardingOrganisationTitle, qualificationLevel) { FromWhichYear = "FromWhichYear" }; + var detailsPage = new DetailsPage + { + CheckAnotherQualificationText = checkAnotherQualificationText, + FurtherInfoText = furtherInfoText, + RequirementsText = requirementsText, + RatiosText = ratiosText, + FeedbackBanner = feedbackBanner, + BackButton = backButton + }; + + _mockContentParser.Setup(o => o.ToHtml(checkAnotherQualificationText)).ReturnsAsync(checkAnotherQualification); + _mockContentParser.Setup(o => o.ToHtml(furtherInfoText)).ReturnsAsync(furtherInfo); + _mockContentParser.Setup(o => o.ToHtml(requirementsText)).ReturnsAsync(requirements); + _mockContentParser.Setup(o => o.ToHtml(ratiosText)).ReturnsAsync(ratios); + _mockContentParser.Setup(o => o.ToHtml(feedbackText)).ReturnsAsync(feedback); + _mockUserJourneyCookieService.Setup(o => o.GetWhenWasQualificationStarted()).Returns((month, year)); + + var sut = GetSut(); + var result = await sut.MapDetails(qualification, detailsPage); + + _mockContentParser.Verify(o => o.ToHtml(checkAnotherQualificationText), Times.Once); + _mockContentParser.Verify(o => o.ToHtml(furtherInfoText), Times.Once); + _mockContentParser.Verify(o => o.ToHtml(requirementsText), Times.Once); + _mockContentParser.Verify(o => o.ToHtml(ratiosText), Times.Once); + _mockContentParser.Verify(o => o.ToHtml(feedbackText), Times.Once); + + result.QualificationId.Should().Be(qualificationId); + result.QualificationLevel.Should().Be(qualificationLevel); + result.QualificationName.Should().Be(qualificationName); + result.AwardingOrganisationTitle.Should().Be(awardingOrganisationTitle); + result.FromWhichYear.Should().Be(qualification.FromWhichYear); + result.BackButton!.Href.Should().Be(backButton.Href); + result.AdditionalRequirementAnswers.Should().BeNullOrEmpty(); + result.DateStarted.Should().Be(dateStarted); + + var content = result.Content!; + content.CheckAnotherQualificationText.Should().Be(checkAnotherQualification); + content.FurtherInfoText.Should().Be(furtherInfo); + content.RequirementsText.Should().Be(requirements); + content.RatiosText.Should().Be(ratios); + content.FeedbackBanner!.Body.Should().Be(feedback); + } +} \ No newline at end of file From 382ba63ca8b1e3a4d619492c7f2696ff1d6b326a Mon Sep 17 00:00:00 2001 From: Tom Whittington Date: Thu, 12 Dec 2024 14:57:59 +0000 Subject: [PATCH 6/8] chore: made some methods private --- .../QualificationDetails/QualificationDetailsService.cs | 2 +- .../Services/QualificationSearch/QualificationSearchService.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/QualificationDetailsService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/QualificationDetailsService.cs index df27ec25..f536b5d4 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/QualificationDetailsService.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationDetails/QualificationDetailsService.cs @@ -169,7 +169,7 @@ public async Task QualificationLevel3OrAboveMightBeRelevantAtLevel2(Qualificatio } } - public T GetRatioProperty(string propertyToCheck, string ratioName, Qualification qualification) + private T GetRatioProperty(string propertyToCheck, string ratioName, Qualification qualification) { try { diff --git a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs index 62447385..bb9e4293 100644 --- a/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs +++ b/src/Dfe.EarlyYearsQualification.Web/Services/QualificationSearch/QualificationSearchService.cs @@ -107,9 +107,8 @@ public FilterModel GetFilterModel(QualificationListPage content) return filterModel; } - public List GetBasicQualificationsModels(List qualifications) + private static List GetBasicQualificationsModels(List qualifications) { - if (qualifications is null) return []; return qualifications.Select(qualification => new BasicQualificationModel(qualification)) .OrderBy(qualification => qualification.QualificationName) .ToList(); From 78499ffb76717795ab6dd07e6659bbcf67538b10 Mon Sep 17 00:00:00 2001 From: Tom Whittington Date: Mon, 6 Jan 2025 16:34:19 +0000 Subject: [PATCH 7/8] test: added couple new methods to test the basics of the ratioReqirement checker --- .../QualificationDetailsServiceTests.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationDetailsServiceTests.cs b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationDetailsServiceTests.cs index af66d0f1..a2dcbe65 100644 --- a/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationDetailsServiceTests.cs +++ b/tests/Dfe.EarlyYearsQualification.UnitTests/Services/QualificationDetailsServiceTests.cs @@ -762,4 +762,91 @@ public async Task MapDetails_(int? month, int? year, string dateStarted) content.RatiosText.Should().Be(ratios); content.FeedbackBanner!.Body.Should().Be(feedback); } + + [TestMethod] + public async Task CheckRatioRequirements_Calls_Cookies_WasStartedBeforeSeptember2014() + { + const bool wasStartedBeforeSeptember2014 = true; + const string qualificationId = "qualificationId"; + const string qualificationName = "qualificationName"; + const string awardingOrganisationTitle = "awardingOrganisationTitle"; + const int qualificationLevel = 2; + var qualification = new Qualification(qualificationId, qualificationName, awardingOrganisationTitle, qualificationLevel) + { + RatioRequirements = + [ + new RatioRequirement + { + RatioRequirementName = RatioRequirements.Level2RatioRequirementName + }, + new RatioRequirement + { + RatioRequirementName = RatioRequirements.Level3RatioRequirementName + }, + new RatioRequirement + { + RatioRequirementName = RatioRequirements.Level6RatioRequirementName + }, + new RatioRequirement + { + RatioRequirementName = RatioRequirements.UnqualifiedRatioRequirementName + } + ] + }; + var qualificationDetails = new QualificationDetailsModel(); + + _mockUserJourneyCookieService.Setup(o => o.WasStartedBeforeSeptember2014()).Returns(wasStartedBeforeSeptember2014); + + var sut = GetSut(); + + await sut.CheckRatioRequirements(qualification, qualificationDetails); + + _mockUserJourneyCookieService.Verify(o => o.WasStartedBeforeSeptember2014(), Times.Once); + } + + [TestMethod] + public async Task CheckRatioRequirements_AutomaticallyApproved() + { + const bool wasStartedBeforeSeptember2014 = true; + const string qualificationId = "qualificationId"; + const string qualificationName = "qualificationName"; + const string awardingOrganisationTitle = "awardingOrganisationTitle"; + const int qualificationLevel = 2; + var qualification = new Qualification(qualificationId, qualificationName, awardingOrganisationTitle, qualificationLevel) + { + IsAutomaticallyApprovedAtLevel6 = true, + RatioRequirements = + [ + new RatioRequirement + { + RatioRequirementName = RatioRequirements.Level2RatioRequirementName, + RequirementForQtsEtcBefore2014 = new Document() + }, + new RatioRequirement + { + RatioRequirementName = RatioRequirements.Level3RatioRequirementName, + RequirementForQtsEtcBefore2014 = new Document() + }, + new RatioRequirement + { + RatioRequirementName = RatioRequirements.Level6RatioRequirementName, + RequirementForQtsEtcBefore2014 = new Document() + }, + new RatioRequirement + { + RatioRequirementName = RatioRequirements.UnqualifiedRatioRequirementName, + RequirementForQtsEtcBefore2014 = new Document() + } + ] + }; + var qualificationDetails = new QualificationDetailsModel(); + + _mockUserJourneyCookieService.Setup(o => o.WasStartedBeforeSeptember2014()).Returns(wasStartedBeforeSeptember2014); + + var sut = GetSut(); + + await sut.CheckRatioRequirements(qualification, qualificationDetails); + + _mockUserJourneyCookieService.Verify(o => o.WasStartedBeforeSeptember2014(), Times.Once); + } } \ No newline at end of file From 2d20ee39e2d9274fd2ab9b20813738e0bd252a3f Mon Sep 17 00:00:00 2001 From: Tom Whittington Date: Tue, 7 Jan 2025 09:41:28 +0000 Subject: [PATCH 8/8] chore: reverted accidental local key push --- src/Dfe.EarlyYearsQualification.Web/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dfe.EarlyYearsQualification.Web/appsettings.json b/src/Dfe.EarlyYearsQualification.Web/appsettings.json index 327ae047..b0f4ce11 100644 --- a/src/Dfe.EarlyYearsQualification.Web/appsettings.json +++ b/src/Dfe.EarlyYearsQualification.Web/appsettings.json @@ -18,7 +18,7 @@ "ServiceAccess": { "IsPublic": false, "Keys": [ - "cx" + "" ] }, "Storage": {