From 823a5f8f8ee02c296877884f19f4d2bef278f412 Mon Sep 17 00:00:00 2001 From: Francis Pion Date: Fri, 26 Apr 2024 15:44:30 -0400 Subject: [PATCH] Added integration tests. --- .../Commands/SaveProjectCommandHandler.cs | 21 +++- .../Commands/CreateProjectCommandTests.cs | 2 +- .../Commands/DeleteProjectCommandTests.cs | 50 +++++++++ .../Commands/ReplaceProjectCommandTests.cs | 103 ++++++++++++++++++ .../Commands/UpdateProjectCommandTests.cs | 81 ++++++++++++++ .../Logitar.Master.IntegrationTests.csproj | 1 + 6 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/DeleteProjectCommandTests.cs create mode 100644 backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/ReplaceProjectCommandTests.cs create mode 100644 backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/UpdateProjectCommandTests.cs diff --git a/backend/src/Logitar.Master.Application/Projects/Commands/SaveProjectCommandHandler.cs b/backend/src/Logitar.Master.Application/Projects/Commands/SaveProjectCommandHandler.cs index d9178bd..b6df577 100644 --- a/backend/src/Logitar.Master.Application/Projects/Commands/SaveProjectCommandHandler.cs +++ b/backend/src/Logitar.Master.Application/Projects/Commands/SaveProjectCommandHandler.cs @@ -1,4 +1,6 @@ -using Logitar.Master.Domain.Projects; +using Logitar.EventSourcing; +using Logitar.Master.Domain.Projects; +using Logitar.Master.Domain.Projects.Events; using MediatR; namespace Logitar.Master.Application.Projects.Commands; @@ -16,9 +18,22 @@ public async Task Handle(SaveProjectCommand command, CancellationToken cancellat { ProjectAggregate project = command.Project; - if (await _projectRepository.LoadAsync(project.UniqueKey, cancellationToken) != null) + bool hasUniqueKeyChanged = false; + foreach (DomainEvent change in project.Changes) { - throw new UniqueKeyAlreadyUsedException(project.UniqueKey); + if (change is ProjectCreatedEvent) + { + hasUniqueKeyChanged = true; + } + } + + if (hasUniqueKeyChanged) + { + ProjectAggregate? conflict = await _projectRepository.LoadAsync(project.UniqueKey, cancellationToken); + if (conflict?.Equals(project) == false) + { + throw new UniqueKeyAlreadyUsedException(conflict.UniqueKey); + } } await _projectRepository.SaveAsync(project, cancellationToken); diff --git a/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/CreateProjectCommandTests.cs b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/CreateProjectCommandTests.cs index f1a937d..fed2224 100644 --- a/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/CreateProjectCommandTests.cs +++ b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/CreateProjectCommandTests.cs @@ -34,7 +34,7 @@ public async Task It_should_create_a_new_project() Assert.True(project.CreatedOn < project.UpdatedOn); Assert.Equal(payload.UniqueKey.Trim(), project.UniqueKey); - Assert.Equal(payload.DisplayName?.CleanTrim(), project.DisplayName); + Assert.Equal(payload.DisplayName.Trim(), project.DisplayName); Assert.Equal(payload.Description?.CleanTrim(), project.Description); ProjectEntity? entity = await MasterContext.Projects.AsNoTracking() diff --git a/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/DeleteProjectCommandTests.cs b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/DeleteProjectCommandTests.cs new file mode 100644 index 0000000..c07f1e5 --- /dev/null +++ b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/DeleteProjectCommandTests.cs @@ -0,0 +1,50 @@ +using Logitar.Master.Contracts.Projects; +using Logitar.Master.Domain.Projects; +using Logitar.Master.EntityFrameworkCore.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Master.Application.Projects.Commands; + +[Trait(Traits.Category, Categories.Integration)] +public class DeleteProjectCommandTests : IntegrationTests +{ + private readonly IProjectRepository _projectRepository; + + private readonly ProjectAggregate _project; + + public DeleteProjectCommandTests() : base() + { + _projectRepository = ServiceProvider.GetRequiredService(); + + _project = new(new UniqueKeyUnit("MASTER")); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await _projectRepository.SaveAsync(_project); + } + + [Fact(DisplayName = "It should update an existing project.")] + public async Task It_should_delete_an_existing_project() + { + DeleteProjectCommand command = new(_project.Id.ToGuid()); + Project? project = await Mediator.Send(command); + Assert.NotNull(project); + Assert.Equal(_project.Id.ToGuid(), project.Id); + + ProjectEntity? entity = await MasterContext.Projects.AsNoTracking() + .SingleOrDefaultAsync(x => x.AggregateId == _project.Id.AggregateId.Value); + Assert.Null(entity); + } + + [Fact(DisplayName = "It should return null when the project cannot be found.")] + public async Task It_should_return_null_when_the_project_cannot_be_found() + { + DeleteProjectCommand command = new(Id: Guid.Empty); + Project? project = await Mediator.Send(command); + Assert.Null(project); + } +} diff --git a/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/ReplaceProjectCommandTests.cs b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/ReplaceProjectCommandTests.cs new file mode 100644 index 0000000..4fd314a --- /dev/null +++ b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/ReplaceProjectCommandTests.cs @@ -0,0 +1,103 @@ +using FluentValidation.Results; +using Logitar.Master.Contracts.Projects; +using Logitar.Master.Domain.Projects; +using Logitar.Master.Domain.Shared; +using Logitar.Security.Cryptography; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Master.Application.Projects.Commands; + +[Trait(Traits.Category, Categories.Integration)] +public class ReplaceProjectCommandTests : IntegrationTests +{ + private readonly IProjectRepository _projectRepository; + + private readonly ProjectAggregate _project; + + public ReplaceProjectCommandTests() : base() + { + _projectRepository = ServiceProvider.GetRequiredService(); + + _project = new(new UniqueKeyUnit("MASTER")); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await _projectRepository.SaveAsync(_project); + } + + [Fact(DisplayName = "It should replace an existing project with delta.")] + public async Task It_should_replace_an_existing_project_with_delta() + { + long version = _project.Version; + _project.Description = new DescriptionUnit("This is the master project."); + _project.Update(); + await _projectRepository.SaveAsync(_project); + + ReplaceProjectPayload payload = new() + { + DisplayName = " Master ", + Description = " " + }; + ReplaceProjectCommand command = new(_project.Id.ToGuid(), payload, version); + Project? project = await Mediator.Send(command); + Assert.NotNull(project); + + Assert.Equal(_project.Id.ToGuid(), project.Id); + Assert.Equal(3, project.Version); + // TODO(fpion): CreatedBy, UpdatedBy + Assert.True(project.CreatedOn < project.UpdatedOn); + + Assert.Equal(_project.UniqueKey.Value, project.UniqueKey); + Assert.Equal(payload.DisplayName.Trim(), project.DisplayName); + Assert.Equal(_project.Description.Value, project.Description); + } + + [Fact(DisplayName = "It should replace an existing project.")] + public async Task It_should_replace_an_existing_project() + { + ReplaceProjectPayload payload = new() + { + DisplayName = " Master ", + Description = " " + }; + ReplaceProjectCommand command = new(_project.Id.ToGuid(), payload, Version: null); + Project? project = await Mediator.Send(command); + Assert.NotNull(project); + + Assert.Equal(_project.Id.ToGuid(), project.Id); + Assert.Equal(2, project.Version); + // TODO(fpion): CreatedBy, UpdatedBy + Assert.True(project.CreatedOn < project.UpdatedOn); + + Assert.Equal(_project.UniqueKey.Value, project.UniqueKey); + Assert.Equal(payload.DisplayName.Trim(), project.DisplayName); + Assert.Equal(payload.Description?.CleanTrim(), project.Description); + } + + [Fact(DisplayName = "It should return null when the project cannot be found.")] + public async Task It_should_return_null_when_the_project_cannot_be_found() + { + ReplaceProjectPayload payload = new(); + ReplaceProjectCommand command = new(Id: Guid.Empty, payload, Version: null); + Project? project = await Mediator.Send(command); + Assert.Null(project); + } + + [Fact(DisplayName = "It should throw ValidationException when the payload is not valid.")] + public async Task It_should_throw_ValidationException_when_the_payload_is_not_valid() + { + ReplaceProjectPayload payload = new() + { + DisplayName = RandomStringGenerator.GetString(DisplayNameUnit.MaximumLength + 1) + }; + ReplaceProjectCommand command = new(_project.Id.ToGuid(), payload, Version: null); + var exception = await Assert.ThrowsAsync(async () => await Mediator.Send(command)); + + ValidationFailure error = Assert.Single(exception.Errors); + Assert.Equal("MaximumLengthValidator", error.ErrorCode); + Assert.Equal("DisplayName", error.PropertyName); + } +} diff --git a/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/UpdateProjectCommandTests.cs b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/UpdateProjectCommandTests.cs new file mode 100644 index 0000000..a738f6d --- /dev/null +++ b/backend/tests/Logitar.Master.IntegrationTests/Application/Projects/Commands/UpdateProjectCommandTests.cs @@ -0,0 +1,81 @@ +using FluentValidation.Results; +using Logitar.Master.Contracts; +using Logitar.Master.Contracts.Projects; +using Logitar.Master.Domain.Projects; +using Logitar.Master.Domain.Shared; +using Logitar.Security.Cryptography; +using Microsoft.Extensions.DependencyInjection; + +namespace Logitar.Master.Application.Projects.Commands; + +[Trait(Traits.Category, Categories.Integration)] +public class UpdateProjectCommandTests : IntegrationTests +{ + private readonly IProjectRepository _projectRepository; + + private readonly ProjectAggregate _project; + + public UpdateProjectCommandTests() : base() + { + _projectRepository = ServiceProvider.GetRequiredService(); + + _project = new(new UniqueKeyUnit("MASTER")); + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await _projectRepository.SaveAsync(_project); + } + + [Fact(DisplayName = "It should return null when the project cannot be found.")] + public async Task It_should_return_null_when_the_project_cannot_be_found() + { + UpdateProjectPayload payload = new(); + UpdateProjectCommand command = new(Id: Guid.Empty, payload); + Project? project = await Mediator.Send(command); + Assert.Null(project); + } + + [Fact(DisplayName = "It should throw ValidationException when the payload is not valid.")] + public async Task It_should_throw_ValidationException_when_the_payload_is_not_valid() + { + UpdateProjectPayload payload = new() + { + DisplayName = new Change(RandomStringGenerator.GetString(DisplayNameUnit.MaximumLength + 1)) + }; + UpdateProjectCommand command = new(_project.Id.ToGuid(), payload); + var exception = await Assert.ThrowsAsync(async () => await Mediator.Send(command)); + + ValidationFailure error = Assert.Single(exception.Errors); + Assert.Equal("MaximumLengthValidator", error.ErrorCode); + Assert.Equal("DisplayName.Value", error.PropertyName); + } + + [Fact(DisplayName = "It should update an existing project.")] + public async Task It_should_update_an_existing_project() + { + _project.DisplayName = new DisplayNameUnit("Master"); + _project.Description = new DescriptionUnit("This is the master project."); + _project.Update(); + await _projectRepository.SaveAsync(_project); + + UpdateProjectPayload payload = new() + { + Description = new Change(" ") + }; + UpdateProjectCommand command = new(_project.Id.ToGuid(), payload); + Project? project = await Mediator.Send(command); + Assert.NotNull(project); + + Assert.Equal(_project.Id.ToGuid(), project.Id); + Assert.Equal(3, project.Version); + // TODO(fpion): CreatedBy, UpdatedBy + Assert.True(project.CreatedOn < project.UpdatedOn); + + Assert.Equal(_project.UniqueKey.Value, project.UniqueKey); + Assert.Equal(_project.DisplayName.Value, project.DisplayName); + Assert.Equal(payload.Description.Value?.CleanTrim(), project.Description); + } +} diff --git a/backend/tests/Logitar.Master.IntegrationTests/Logitar.Master.IntegrationTests.csproj b/backend/tests/Logitar.Master.IntegrationTests/Logitar.Master.IntegrationTests.csproj index e2ed7fa..61fed67 100644 --- a/backend/tests/Logitar.Master.IntegrationTests/Logitar.Master.IntegrationTests.csproj +++ b/backend/tests/Logitar.Master.IntegrationTests/Logitar.Master.IntegrationTests.csproj @@ -35,6 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive +