From 11a1f35c8ac25037a6c87fde4bfd47f0707fd452 Mon Sep 17 00:00:00 2001 From: Dan Ashton Date: Wed, 13 Nov 2024 20:51:18 +0000 Subject: [PATCH 1/3] FAI-1991 - Add handler for get candidate by email --- .../WhenHandlingGetCandidateByEmailQuery.cs | 45 +++++++++++++++++++ .../GetCandidateByEmailQuery.cs | 8 ++++ .../GetCandidateByEmailQueryHandler.cs | 17 +++++++ .../GetCandidateByEmailQueryResult.cs | 6 +++ 4 files changed, 76 insertions(+) create mode 100644 src/SFA.DAS.CandidateAccount.Application.UnitTests/Candidate/WhenHandlingGetCandidateByEmailQuery.cs create mode 100644 src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQuery.cs create mode 100644 src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQueryHandler.cs create mode 100644 src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQueryResult.cs diff --git a/src/SFA.DAS.CandidateAccount.Application.UnitTests/Candidate/WhenHandlingGetCandidateByEmailQuery.cs b/src/SFA.DAS.CandidateAccount.Application.UnitTests/Candidate/WhenHandlingGetCandidateByEmailQuery.cs new file mode 100644 index 00000000..1c4ab683 --- /dev/null +++ b/src/SFA.DAS.CandidateAccount.Application.UnitTests/Candidate/WhenHandlingGetCandidateByEmailQuery.cs @@ -0,0 +1,45 @@ +using AutoFixture.NUnit3; +using FluentAssertions; +using Moq; +using SFA.DAS.CandidateAccount.Application.Candidate.Queries.GetCandidateByEmail; +using SFA.DAS.CandidateAccount.Data.Candidate; +using SFA.DAS.CandidateAccount.Domain.Candidate; +using SFA.DAS.Testing.AutoFixture; + +namespace SFA.DAS.CandidateAccount.Application.UnitTests.Candidate; + +public class WhenHandlingGetCandidateByEmailQuery +{ + [Test, RecursiveMoqAutoData] + public async Task Then_The_Query_Is_Handled_And_Data_Candidate_Returned( + GetCandidateByEmailQuery query, + CandidateEntity entity, + [Frozen] Mock candidateRepository, + GetCandidateByEmailQueryHandler handler) + { + candidateRepository.Setup(x => x.GetCandidateByEmail(query.Email)).ReturnsAsync(entity); + + var actual = await handler.Handle(query, CancellationToken.None); + + actual.Candidate.Should().BeEquivalentTo(entity, options=>options + .Excluding(c=>c.CandidatePreferences) + .Excluding(c=>c.Address) + .Excluding(c=>c.AboutYou) + .Excluding(c=>c.Applications) + .Excluding(c=>c.Status) + ); + } + + [Test, MoqAutoData] + public async Task Then_If_Not_Found_Then_Null_Returned( + GetCandidateByEmailQuery query, + [Frozen] Mock candidateRepository, + GetCandidateByEmailQueryHandler handler) + { + candidateRepository.Setup(x => x.GetCandidateByEmail(query.Email)).ReturnsAsync((CandidateEntity?)null); + + var actual = await handler.Handle(query, CancellationToken.None); + + actual.Candidate.Should().BeNull(); + } +} \ No newline at end of file diff --git a/src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQuery.cs b/src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQuery.cs new file mode 100644 index 00000000..8c8c9b95 --- /dev/null +++ b/src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace SFA.DAS.CandidateAccount.Application.Candidate.Queries.GetCandidateByEmail; + +public class GetCandidateByEmailQuery : IRequest +{ + public string Email { get; set; } +} \ No newline at end of file diff --git a/src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQueryHandler.cs b/src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQueryHandler.cs new file mode 100644 index 00000000..e9d73324 --- /dev/null +++ b/src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQueryHandler.cs @@ -0,0 +1,17 @@ +using MediatR; +using SFA.DAS.CandidateAccount.Data.Candidate; + +namespace SFA.DAS.CandidateAccount.Application.Candidate.Queries.GetCandidateByEmail; + +public class GetCandidateByEmailQueryHandler(ICandidateRepository candidateRepository) : IRequestHandler +{ + public async Task Handle(GetCandidateByEmailQuery request, CancellationToken cancellationToken) + { + var candidate = await candidateRepository.GetCandidateByEmail(request.Email); + + return new GetCandidateByEmailQueryResult + { + Candidate = candidate + }; + } +} \ No newline at end of file diff --git a/src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQueryResult.cs b/src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQueryResult.cs new file mode 100644 index 00000000..24c09359 --- /dev/null +++ b/src/SFA.DAS.CandidateAccount.Application/Candidate/Queries/GetCandidateByEmail/GetCandidateByEmailQueryResult.cs @@ -0,0 +1,6 @@ +namespace SFA.DAS.CandidateAccount.Application.Candidate.Queries.GetCandidateByEmail; + +public class GetCandidateByEmailQueryResult +{ + public Domain.Candidate.Candidate? Candidate { get; set; } +} \ No newline at end of file From c880fcd951497451d0abf34dd5e5675595db7b4b Mon Sep 17 00:00:00 2001 From: Dan Ashton Date: Wed, 13 Nov 2024 21:04:04 +0000 Subject: [PATCH 2/3] FAI-1991 - Add endpoint for get by email --- .../WhenCallingGetCandidateByEmail.cs | 72 +++++++++++++++++++ .../Controllers/CandidateController.cs | 25 +++++++ 2 files changed, 97 insertions(+) create mode 100644 src/SFA.DAS.CandidateAccount.Api.UnitTests/Controllers/Candidate/WhenCallingGetCandidateByEmail.cs diff --git a/src/SFA.DAS.CandidateAccount.Api.UnitTests/Controllers/Candidate/WhenCallingGetCandidateByEmail.cs b/src/SFA.DAS.CandidateAccount.Api.UnitTests/Controllers/Candidate/WhenCallingGetCandidateByEmail.cs new file mode 100644 index 00000000..d3822ed1 --- /dev/null +++ b/src/SFA.DAS.CandidateAccount.Api.UnitTests/Controllers/Candidate/WhenCallingGetCandidateByEmail.cs @@ -0,0 +1,72 @@ +using System.Net; +using AutoFixture.NUnit3; +using FluentAssertions; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Moq; +using SFA.DAS.CandidateAccount.Api.Controllers; +using SFA.DAS.CandidateAccount.Application.Candidate.Queries.GetCandidateByEmail; +using SFA.DAS.Testing.AutoFixture; + +namespace SFA.DAS.CandidateAccount.Api.UnitTests.Controllers.Candidate; + +public class WhenCallingGetCandidateByEmail +{ + [Test, MoqAutoData] + public async Task Then_Mediator_Query_Is_Called_And_Candidate_Returned( + string email, + GetCandidateByEmailQueryResult queryResult, + [Frozen] Mock mediator, + [Greedy] CandidateController controller) + { + //Arrange + mediator.Setup(x => x.Send(It.Is(c => c.Email.Equals(email)), CancellationToken.None)) + .ReturnsAsync(queryResult); + + //Act + var actual = await controller.GetCandidateByEmail(email) as OkObjectResult; + + //Assert + Assert.That(actual, Is.Not.Null); + actual.StatusCode.Should().Be((int) HttpStatusCode.OK); + actual.Value.Should().BeEquivalentTo(queryResult.Candidate); + } + + [Test, MoqAutoData] + public async Task Then_If_Null_From_Mediator_Response_Not_Found_Returned( + string email, + GetCandidateByEmailQueryResult queryResult, + [Frozen] Mock mediator, + [Greedy] CandidateController controller) + { + //Arrange + queryResult.Candidate = null; + mediator.Setup(x => x.Send(It.Is(c => c.Email.Equals(email)), CancellationToken.None)) + .ReturnsAsync(queryResult); + + //Act + var actual = await controller.GetCandidateByEmail(email) as NotFoundResult; + + //Assert + Assert.That(actual, Is.Not.Null); + actual.StatusCode.Should().Be((int) HttpStatusCode.NotFound); + } + + [Test, MoqAutoData] + public async Task Then_If_Exception_Internal_Server_Error_Returned( + string email, + [Frozen] Mock mediator, + [Greedy] CandidateController controller) + { + //Arrange + mediator.Setup(x => x.Send(It.Is(c => c.Email.Equals(email)), CancellationToken.None)) + .ThrowsAsync(new Exception()); + + //Act + var actual = await controller.GetCandidateByEmail(email) as StatusCodeResult; + + //Assert + Assert.That(actual, Is.Not.Null); + actual.StatusCode.Should().Be((int) HttpStatusCode.InternalServerError); + } +} \ No newline at end of file diff --git a/src/SFA.DAS.CandidateAccount.Api/Controllers/CandidateController.cs b/src/SFA.DAS.CandidateAccount.Api/Controllers/CandidateController.cs index 51481b6f..206e4fee 100644 --- a/src/SFA.DAS.CandidateAccount.Api/Controllers/CandidateController.cs +++ b/src/SFA.DAS.CandidateAccount.Api/Controllers/CandidateController.cs @@ -6,6 +6,7 @@ using SFA.DAS.CandidateAccount.Application.Candidate.Commands.DeleteCandidate; using SFA.DAS.CandidateAccount.Application.Candidate.Commands.UpsertCandidate; using SFA.DAS.CandidateAccount.Application.Candidate.Queries.GetCandidate; +using SFA.DAS.CandidateAccount.Application.Candidate.Queries.GetCandidateByEmail; using SFA.DAS.CandidateAccount.Application.Candidate.Queries.GetCandidateByMigratedEmail; using SFA.DAS.CandidateAccount.Application.Candidate.Queries.GetCandidateByMigratedId; using SFA.DAS.CandidateAccount.Domain.Candidate; @@ -112,6 +113,30 @@ public async Task GetCandidateByMigratedEmail(string email) return new StatusCodeResult((int)HttpStatusCode.InternalServerError); } } + + + [HttpGet] + [Route("email/{email}")] + public async Task GetCandidateByEmail(string email) + { + try + { + var result = await mediator.Send(new GetCandidateByEmailQuery + { + Email = email + }); + if (result.Candidate == null) + { + return NotFound(); + } + return Ok(result.Candidate); + } + catch (Exception e) + { + logger.LogError(e, "Get candidate by email : An error occurred"); + return new StatusCodeResult((int)HttpStatusCode.InternalServerError); + } + } [HttpPut] [Route("{candidateId}")] From 051e7d645ace07f8b55c4f51ff0b1f0ab03d53bc Mon Sep 17 00:00:00 2001 From: Dan Ashton Date: Wed, 13 Nov 2024 22:51:31 +0000 Subject: [PATCH 3/3] FAI-1991 - Allow gov auth to be updated in put candidate request --- .../Controllers/Candidate/WhenCallingPutCandidate.cs | 6 ++++++ .../ApiRequests/PostCandidateRequest.cs | 1 + .../Controllers/CandidateController.cs | 3 ++- .../Repository/Candidate/WhenUpsertingCandidate.cs | 1 + .../Candidate/CandidateRepository.cs | 5 +---- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/SFA.DAS.CandidateAccount.Api.UnitTests/Controllers/Candidate/WhenCallingPutCandidate.cs b/src/SFA.DAS.CandidateAccount.Api.UnitTests/Controllers/Candidate/WhenCallingPutCandidate.cs index 08233c07..697909bb 100644 --- a/src/SFA.DAS.CandidateAccount.Api.UnitTests/Controllers/Candidate/WhenCallingPutCandidate.cs +++ b/src/SFA.DAS.CandidateAccount.Api.UnitTests/Controllers/Candidate/WhenCallingPutCandidate.cs @@ -32,6 +32,9 @@ public async Task Then_If_MediatorCall_Returns_Created_Then_Created_Result_Retur && c.Candidate.PhoneNumber.Equals(postCandidateRequest.PhoneNumber) && c.Candidate.DateOfBirth.Equals(postCandidateRequest.DateOfBirth) && c.Candidate.TermsOfUseAcceptedOn.Equals(postCandidateRequest.TermsOfUseAcceptedOn) + && c.Candidate.GovUkIdentifier.Equals(postCandidateRequest.GovUkIdentifier) + && c.Candidate.MigratedEmail.Equals(postCandidateRequest.MigratedEmail) + && c.Candidate.MigratedCandidateId.Equals(postCandidateRequest.MigratedCandidateId) ), CancellationToken.None)) .ReturnsAsync(upsertCandidateCommandResult); @@ -63,6 +66,9 @@ public async Task Then_If_MediatorCall_Returns_NotCreated_Then_Ok_Result_Returne && c.Candidate.PhoneNumber.Equals(postCandidateRequest.PhoneNumber) && c.Candidate.DateOfBirth.Equals(postCandidateRequest.DateOfBirth) && c.Candidate.TermsOfUseAcceptedOn.Equals(postCandidateRequest.TermsOfUseAcceptedOn) + && c.Candidate.GovUkIdentifier.Equals(postCandidateRequest.GovUkIdentifier) + && c.Candidate.MigratedEmail.Equals(postCandidateRequest.MigratedEmail) + && c.Candidate.MigratedCandidateId.Equals(postCandidateRequest.MigratedCandidateId) ), CancellationToken.None)) .ReturnsAsync(upsertCandidateCommandResult); diff --git a/src/SFA.DAS.CandidateAccount.Api/ApiRequests/PostCandidateRequest.cs b/src/SFA.DAS.CandidateAccount.Api/ApiRequests/PostCandidateRequest.cs index 02b7e10e..9798f6b2 100644 --- a/src/SFA.DAS.CandidateAccount.Api/ApiRequests/PostCandidateRequest.cs +++ b/src/SFA.DAS.CandidateAccount.Api/ApiRequests/PostCandidateRequest.cs @@ -23,4 +23,5 @@ public abstract class CandidateRequest public CandidateStatus? Status { get; set; } public string? MigratedEmail { get; set; } public Guid? MigratedCandidateId { get; set; } + public string? GovUkIdentifier { get; set; } } \ No newline at end of file diff --git a/src/SFA.DAS.CandidateAccount.Api/Controllers/CandidateController.cs b/src/SFA.DAS.CandidateAccount.Api/Controllers/CandidateController.cs index 206e4fee..2a4f3143 100644 --- a/src/SFA.DAS.CandidateAccount.Api/Controllers/CandidateController.cs +++ b/src/SFA.DAS.CandidateAccount.Api/Controllers/CandidateController.cs @@ -158,7 +158,8 @@ public async Task PutCandidate([FromRoute] Guid candidateId, PutC TermsOfUseAcceptedOn = postCandidateRequest.TermsOfUseAcceptedOn, Status = postCandidateRequest.Status, MigratedEmail = postCandidateRequest.MigratedEmail, - MigratedCandidateId = postCandidateRequest.MigratedCandidateId + MigratedCandidateId = postCandidateRequest.MigratedCandidateId, + GovUkIdentifier = postCandidateRequest.GovUkIdentifier } }); diff --git a/src/SFA.DAS.CandidateAccount.Data.UnitTests/Repository/Candidate/WhenUpsertingCandidate.cs b/src/SFA.DAS.CandidateAccount.Data.UnitTests/Repository/Candidate/WhenUpsertingCandidate.cs index e9bfc4b2..fca971a4 100644 --- a/src/SFA.DAS.CandidateAccount.Data.UnitTests/Repository/Candidate/WhenUpsertingCandidate.cs +++ b/src/SFA.DAS.CandidateAccount.Data.UnitTests/Repository/Candidate/WhenUpsertingCandidate.cs @@ -65,6 +65,7 @@ public async Task AndIdExistsThenCandidateIsUpdatedIfNotNull( actual.Item1.PhoneNumber.Should().Be(existingCandidate.PhoneNumber); actual.Item1.MigratedEmail.Should().Be(existingCandidate.MigratedEmail); actual.Item1.MigratedCandidateId.Should().Be(existingCandidate.MigratedCandidateId); + actual.Item1.GovUkIdentifier.Should().Be(existingCandidate.GovUkIdentifier); actual.Item1.UpdatedOn.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); } diff --git a/src/SFA.DAS.CandidateAccount.Data/Candidate/CandidateRepository.cs b/src/SFA.DAS.CandidateAccount.Data/Candidate/CandidateRepository.cs index b6b3beed..62609496 100644 --- a/src/SFA.DAS.CandidateAccount.Data/Candidate/CandidateRepository.cs +++ b/src/SFA.DAS.CandidateAccount.Data/Candidate/CandidateRepository.cs @@ -110,15 +110,11 @@ public async Task> UpsertCandidate(Domain.Candidate. .CandidateEntities .FirstOrDefaultAsync(c => c.Id == candidate.Id); - //TODO look at why we are doing this - and not doing all the fields if (existingCandidate == null) { var newCandidate = (CandidateEntity)candidate; newCandidate.CreatedOn = DateTime.UtcNow; newCandidate.UpdatedOn = null; - newCandidate.FirstName = candidate.FirstName; - newCandidate.LastName = candidate.LastName; - newCandidate.DateOfBirth = candidate.DateOfBirth; await dataContext.CandidateEntities.AddAsync(newCandidate); await dataContext.SaveChangesAsync(); return new Tuple(newCandidate, true); @@ -133,6 +129,7 @@ public async Task> UpsertCandidate(Domain.Candidate. existingCandidate.Status = candidate.Status.HasValue ? (short)candidate.Status : existingCandidate.Status; existingCandidate.MigratedEmail = candidate.MigratedEmail ?? existingCandidate.MigratedEmail; existingCandidate.MigratedCandidateId = candidate.MigratedCandidateId ?? existingCandidate.MigratedCandidateId; + existingCandidate.GovUkIdentifier = candidate.GovUkIdentifier ?? existingCandidate.GovUkIdentifier; dataContext.CandidateEntities.Update(existingCandidate); await dataContext.SaveChangesAsync();