From fc917578edbe40aa37a3d7c33dde1eb5fdd89b58 Mon Sep 17 00:00:00 2001 From: Kamil Oberaj Date: Tue, 7 May 2024 22:45:38 +0200 Subject: [PATCH] [feat]: Equipment Tests --- .../Equipment/EquipmentReadDto.cs | 5 - .../Equipments/EquipmentReadDto.cs | 5 + .../EquipmentWriteDto.cs | 2 +- .../Pagination/PagingResultDto.cs | 2 +- .../Controllers/V1/EquipmentController.cs | 2 +- .../Handle.cs | 97 ++++++++++++- .../Validator.cs | 2 +- .../Handle.cs | 117 ++++++++++++++++ .../Validator.cs | 38 +++++ .../Handle.cs | 87 ++++++++++++ .../Handle.cs | 130 ++++++++++++++++++ .../Validator.cs | 46 +++++++ .../Equipments/EquipmentProjectionTests.cs | 40 ++++++ .../PaginationExtensions.cs | 33 +---- .../Equipments/Common/EquipmentChecker.cs | 2 +- .../Create/CreateEquipmentCommand.cs | 2 +- .../GetAll/GetAllEquipmentsQuery.cs | 2 +- ...ler.cs => GetAllEquipmentsQueryHandler.cs} | 4 +- .../MapperProjections/EquipmentProjection.cs | 2 +- .../Update/UpdateEquipmentCommand.cs | 2 +- .../Validators/EquipmentWriteDtoValidator.cs | 2 +- .../Common/Results/CheckResult.cs | 5 +- .../Common/Results/CommandResult.cs | 4 +- .../Common/Results/QueryResult.cs | 4 +- StudioManager.Domain/Entities/Equipment.cs | 13 +- .../Entities/EquipmentType.cs | 2 +- StudioManager.Domain/ErrorMessages/EX.cs | 1 + .../DbContextExtensions/TestDbMigrator.cs | 2 +- .../IntegrationTestBase.cs | 12 +- 29 files changed, 606 insertions(+), 59 deletions(-) delete mode 100644 StudioManager.API.Contracts/Equipment/EquipmentReadDto.cs create mode 100644 StudioManager.API.Contracts/Equipments/EquipmentReadDto.cs rename StudioManager.API.Contracts/{Equipment => Equipments}/EquipmentWriteDto.cs (66%) create mode 100644 StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Handle.cs create mode 100644 StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Validator.cs create mode 100644 StudioManager.Application.Tests/Equipments/GetAllEquipmentsQueryHandlerTests/Handle.cs create mode 100644 StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Handle.cs create mode 100644 StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Validator.cs create mode 100644 StudioManager.Application.Tests/Mapps/Equipments/EquipmentProjectionTests.cs rename StudioManager.Application/Equipments/GetAll/{GetAllEquipmentTypesQueryHandler.cs => GetAllEquipmentsQueryHandler.cs} (91%) diff --git a/StudioManager.API.Contracts/Equipment/EquipmentReadDto.cs b/StudioManager.API.Contracts/Equipment/EquipmentReadDto.cs deleted file mode 100644 index 3c001d9..0000000 --- a/StudioManager.API.Contracts/Equipment/EquipmentReadDto.cs +++ /dev/null @@ -1,5 +0,0 @@ -using StudioManager.API.Contracts.EquipmentTypes; - -namespace StudioManager.API.Contracts.Equipment; - -public sealed record EquipmentReadDto(Guid Id, string Name, int Quantity, string ImageUrl, EquipmentTypeReadDto EquipmentType); \ No newline at end of file diff --git a/StudioManager.API.Contracts/Equipments/EquipmentReadDto.cs b/StudioManager.API.Contracts/Equipments/EquipmentReadDto.cs new file mode 100644 index 0000000..a73dc79 --- /dev/null +++ b/StudioManager.API.Contracts/Equipments/EquipmentReadDto.cs @@ -0,0 +1,5 @@ +using StudioManager.API.Contracts.EquipmentTypes; + +namespace StudioManager.API.Contracts.Equipments; + +public sealed record EquipmentReadDto(Guid Id, string Name, int Quantity, int InitialQuantity, string ImageUrl, EquipmentTypeReadDto EquipmentType); \ No newline at end of file diff --git a/StudioManager.API.Contracts/Equipment/EquipmentWriteDto.cs b/StudioManager.API.Contracts/Equipments/EquipmentWriteDto.cs similarity index 66% rename from StudioManager.API.Contracts/Equipment/EquipmentWriteDto.cs rename to StudioManager.API.Contracts/Equipments/EquipmentWriteDto.cs index 05dcf04..b1258ea 100644 --- a/StudioManager.API.Contracts/Equipment/EquipmentWriteDto.cs +++ b/StudioManager.API.Contracts/Equipments/EquipmentWriteDto.cs @@ -1,3 +1,3 @@ -namespace StudioManager.API.Contracts.Equipment; +namespace StudioManager.API.Contracts.Equipments; public sealed record EquipmentWriteDto(string Name, Guid EquipmentTypeId, int Quantity/*, byte[] Image*/); \ No newline at end of file diff --git a/StudioManager.API.Contracts/Pagination/PagingResultDto.cs b/StudioManager.API.Contracts/Pagination/PagingResultDto.cs index c78b905..da65f59 100644 --- a/StudioManager.API.Contracts/Pagination/PagingResultDto.cs +++ b/StudioManager.API.Contracts/Pagination/PagingResultDto.cs @@ -2,6 +2,6 @@ public sealed class PagingResultDto { - public IReadOnlyList Data { get; set; } = default!; + public List Data { get; set; } = default!; public PaginationDetailsDto Pagination { get; set; } = default!; } \ No newline at end of file diff --git a/StudioManager.API/Controllers/V1/EquipmentController.cs b/StudioManager.API/Controllers/V1/EquipmentController.cs index 3127034..87c6bf0 100644 --- a/StudioManager.API/Controllers/V1/EquipmentController.cs +++ b/StudioManager.API/Controllers/V1/EquipmentController.cs @@ -3,7 +3,7 @@ using MediatR; using Microsoft.AspNetCore.Mvc; using StudioManager.API.Base; -using StudioManager.API.Contracts.Equipment; +using StudioManager.API.Contracts.Equipments; using StudioManager.API.Contracts.Pagination; using StudioManager.Application.Equipments.Create; using StudioManager.Application.Equipments.Delete; diff --git a/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Handle.cs index f0b4c8a..76b24d2 100644 --- a/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Handle.cs +++ b/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Handle.cs @@ -1,8 +1,103 @@ -using StudioManager.Tests.Common; +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Equipments; +using StudioManager.Application.Equipments.Create; +using StudioManager.Domain.Entities; +using StudioManager.Domain.ErrorMessages; +using StudioManager.Infrastructure; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.DbContextExtensions; namespace StudioManager.Application.Tests.Equipments.CreateEquipmentCommandHandlerTests; public class Handle : IntegrationTestBase { + private static CreateEquipmentCommandHandler _testCandidate = null!; + private static TestDbContextFactory _testDbContextFactory = null!; + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _testDbContextFactory = new TestDbContextFactory(connectionString); + _testCandidate = new CreateEquipmentCommandHandler(_testDbContextFactory); + } + + [Test] + public async Task should_return_error_when_equipment_type_does_not_exist_async() + { + // Arrange + var dto = new EquipmentWriteDto("Equipment-Test-Name", Guid.NewGuid(), 1); + var command = new CreateEquipmentCommand(dto); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(NotFoundStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be($"[NOT FOUND] {nameof(EquipmentType)} with id '{dto.EquipmentTypeId}' does not exist"); + } + [Test] + public async Task should_return_error_when_equipment_has_duplicated_name_and_type_async() + { + // Arrange + + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); + var existing = Equipment.Create("Equipment-Test-Name", equipmentType.Id, 1); + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) + { + await AddEntitiesToTable(dbContext, equipmentType); + await AddEntitiesToTable(dbContext, existing); + } + + var dto = new EquipmentWriteDto("Equipment-Test-Name", existing.EquipmentTypeId, 1); + var command = new CreateEquipmentCommand(dto); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(ConflictStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be(string.Format(DB_FORMAT.EQUIPMENT_DUPLICATE_NAME_TYPE, + existing.Name, existing.EquipmentTypeId)); + } + + [Test] + public async Task should_return_success_async() + { + // Arrange + + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) + { + await ClearTableContentsForAsync(dbContext); + await ClearTableContentsForAsync(dbContext); + await AddEntitiesToTable(dbContext, equipmentType); + } + + var dto = new EquipmentWriteDto("Equipment-Test-Name", equipmentType.Id, 1); + var command = new CreateEquipmentCommand(dto); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeTrue(); + result.StatusCode.Should().Be(OkStatusCode); + result.Data.Should().NotBeNull(); + result.Error.Should().BeNullOrWhiteSpace(); + var parseResult = Guid.TryParse(result.Data!.ToString(), out var guid); + parseResult.Should().BeTrue(); + guid.Should().NotBeEmpty(); + } } \ No newline at end of file diff --git a/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Validator.cs index 09bc474..ebae975 100644 --- a/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Validator.cs +++ b/StudioManager.Application.Tests/Equipments/CreateEquipmentCommandHandlerTests/Validator.cs @@ -1,6 +1,6 @@ using FluentAssertions; using NUnit.Framework; -using StudioManager.API.Contracts.Equipment; +using StudioManager.API.Contracts.Equipments; using StudioManager.Application.Equipments.Create; using StudioManager.Application.Equipments.Validators; diff --git a/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Handle.cs new file mode 100644 index 0000000..76dd9e8 --- /dev/null +++ b/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Handle.cs @@ -0,0 +1,117 @@ +using FluentAssertions; +using NUnit.Framework; +using StudioManager.Application.Equipments.Delete; +using StudioManager.Domain.Common.Results; +using StudioManager.Domain.Entities; +using StudioManager.Domain.ErrorMessages; +using StudioManager.Infrastructure; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.Equipments.DeleteEquipmentCommandHandlerTests; + +public sealed class Handle : IntegrationTestBase +{ + private static DeleteEquipmentCommandHandler _testCandidate = null!; + private static TestDbContextFactory _testDbContextFactory = null!; + + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _testDbContextFactory = new TestDbContextFactory(connectionString); + _testCandidate = new DeleteEquipmentCommandHandler(_testDbContextFactory); + } + + [Test] + public async Task should_return_not_found_when_updating_non_existing_entity_async() + { + // Arrange + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) + { + await ClearTableContentsForAsync(dbContext); + } + + var id = Guid.NewGuid(); + + var command = new DeleteEquipmentCommand(id); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + result.Should().NotBeNull(); + result.Should().BeOfType(); + result.Data.Should().BeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(NotFoundStatusCode); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be($"[NOT FOUND] {nameof(Equipment)} with id '{id}' does not exist"); + } + + [Test] + public async Task should_return_error_when_initial_count_is_invalid_async() + { + // Arrange + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); + var equipment = Equipment.Create("Test-Equipment", equipmentType.Id, 10); + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) + { + await ClearTableContentsForAsync(dbContext); + await ClearTableContentsForAsync(dbContext); + await AddEntitiesToTable(dbContext, equipmentType); + await AddEntitiesToTable(dbContext, equipment); + equipment.Reserve(1); + dbContext.Equipments.Update(equipment); + await dbContext.SaveChangesAsync(Cts.Token); + } + + var command = new DeleteEquipmentCommand(equipment.Id); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + result.Should().NotBeNull(); + result.Should().BeOfType(); + result.Data.Should().BeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(ConflictStatusCode); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be(string.Format(DB_FORMAT.EQUIPMENT_QUANTITY_MISSING_WHEN_REMOVING, + equipment.InitialQuantity, + equipment.Quantity)); + } + + [Test] + public async Task should_return_success_async() + { + // Arrange + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); + var equipment = Equipment.Create("Test-Equipment", equipmentType.Id, 1); + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) + { + await ClearTableContentsForAsync(dbContext); + await ClearTableContentsForAsync(dbContext); + await AddEntitiesToTable(dbContext, equipmentType); + await AddEntitiesToTable(dbContext, equipment); + } + + var command = new DeleteEquipmentCommand(equipment.Id); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + result.Should().NotBeNull(); + result.Should().BeOfType(); + result.Data.Should().BeNull(); + result.Succeeded.Should().BeTrue(); + result.StatusCode.Should().Be(OkStatusCode); + result.Error.Should().BeNullOrWhiteSpace(); + + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) + { + var databaseCheck = await dbContext.Equipments.FindAsync(equipment.Id); + databaseCheck.Should().BeNull(); + } + } +} \ No newline at end of file diff --git a/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Validator.cs new file mode 100644 index 0000000..9650507 --- /dev/null +++ b/StudioManager.Application.Tests/Equipments/DeleteEquipmentCommandHandlerTests/Validator.cs @@ -0,0 +1,38 @@ +using FluentAssertions; +using NUnit.Framework; +using StudioManager.Application.Equipments.Delete; + +namespace StudioManager.Application.Tests.Equipments.DeleteEquipmentCommandHandlerTests; + +public sealed class Validator +{ + [Test] + public async Task validator_should_return_validation_error_when_name_is_empty() + { + // Arrange + var command = new DeleteEquipmentCommand(Guid.Empty); + var validator = new DeleteEquipmentCommandValidator(); + + // Act + var result = await validator.ValidateAsync(command, CancellationToken.None); + + // Assert + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(x => x.PropertyName == "Id" && x.ErrorMessage == "'Id' must not be empty."); + } + + [Test] + public async Task validator_should_return_success_when_name_is_not_empty() + { + // Arrange + var command = new DeleteEquipmentCommand(Guid.NewGuid()); + var validator = new DeleteEquipmentCommandValidator(); + + // Act + var result = await validator.ValidateAsync(command, CancellationToken.None); + + // Assert + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/StudioManager.Application.Tests/Equipments/GetAllEquipmentsQueryHandlerTests/Handle.cs b/StudioManager.Application.Tests/Equipments/GetAllEquipmentsQueryHandlerTests/Handle.cs new file mode 100644 index 0000000..590225d --- /dev/null +++ b/StudioManager.Application.Tests/Equipments/GetAllEquipmentsQueryHandlerTests/Handle.cs @@ -0,0 +1,87 @@ +using AutoMapper; +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Equipments; +using StudioManager.API.Contracts.Pagination; +using StudioManager.Application.Equipments.GetAll; +using StudioManager.Domain.Entities; +using StudioManager.Domain.Filters; +using StudioManager.Infrastructure; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.AutoMapperExtensions; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.Equipments.GetAllEquipmentsQueryHandlerTests; + +public sealed class Handle : IntegrationTestBase +{ + private static GetAllEquipmentsQueryHandler _testCandidate = null!; + private static IMapper _mapper = null!; + private static TestDbContextFactory _testDbContextFactory = null!; + + [SetUp] + public async Task SetUpAsync() + { + _mapper = MappingTestHelper.Mapper; + var connectionString = await DbMigrator.MigrateDbAsync(); + _testDbContextFactory = new TestDbContextFactory(connectionString); + _testCandidate = new GetAllEquipmentsQueryHandler(_mapper, _testDbContextFactory); + } + + [Test] + public async Task should_return_empty_data_with_pagination_async() + { + // Arrange + var query = new GetAllEquipmentsQuery(new EquipmentFilter(), PaginationDto.Default()); + + // Act + var result = await _testCandidate.Handle(query, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Error.Should().BeNullOrWhiteSpace(); + result.StatusCode.Should().Be(OkStatusCode); + result.Succeeded.Should().BeTrue(); + result.Data.Should().NotBeNull(); + result.Data.Should().BeOfType>(); + result.Data!.Data.Should().BeEmpty(); + result.Data.Data.Should().BeOfType>(); + } + + [Test] + public async Task should_return_mapped_data_with_pagination_async() + { + // Arrange + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync()) + { + await ClearTableContentsForAsync(dbContext); + await ClearTableContentsForAsync(dbContext); + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); + await AddEntitiesToTable(dbContext, equipmentType); + var equipments = Enumerable.Range(0, 5) + .Select(x => + Equipment.Create(x.ToString(), equipmentType.Id, x)) + .ToArray(); + await AddEntitiesToTable(dbContext, equipments); + } + var query = new GetAllEquipmentsQuery(new EquipmentFilter(), PaginationDto.Default()); + + // Act + var result = await _testCandidate.Handle(query, CancellationToken.None); + + // Assert + result.Should().NotBeNull(); + result.Error.Should().BeNullOrWhiteSpace(); + result.StatusCode.Should().Be(OkStatusCode); + result.Succeeded.Should().BeTrue(); + result.Data.Should().NotBeNull(); + result.Data.Should().BeOfType>(); + result.Data!.Pagination.Should().NotBeNull(); + result.Data.Pagination.Limit.Should().Be(25); + result.Data.Pagination.Page.Should().Be(1); + result.Data.Pagination.Total.Should().Be(5); + result.Data.Data.Should().NotBeEmpty(); + result.Data.Data.Should().HaveCount(5); + result.Data.Data.Should().BeOfType>(); + } +} \ No newline at end of file diff --git a/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Handle.cs b/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Handle.cs new file mode 100644 index 0000000..e1624e6 --- /dev/null +++ b/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Handle.cs @@ -0,0 +1,130 @@ +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Equipments; +using StudioManager.Application.Equipments.Update; +using StudioManager.Domain.Entities; +using StudioManager.Domain.ErrorMessages; +using StudioManager.Infrastructure; +using StudioManager.Tests.Common; +using StudioManager.Tests.Common.DbContextExtensions; + +namespace StudioManager.Application.Tests.Equipments.UpdateEquipmentCommandHandlerTests; + +public sealed class Handle : IntegrationTestBase +{ + private static UpdateEquipmentCommandHandler _testCandidate = null!; + private static TestDbContextFactory _testDbContextFactory = null!; + + [SetUp] + public async Task SetUpAsync() + { + var connectionString = await DbMigrator.MigrateDbAsync(); + _testDbContextFactory = new TestDbContextFactory(connectionString); + _testCandidate = new UpdateEquipmentCommandHandler(_testDbContextFactory); + } + + [Test] + public async Task should_return_not_found_when_equipment_type_does_not_exist_async() + { + // Arrange + var dto = new EquipmentWriteDto("Test-Equipment", Guid.NewGuid(), 1); + var command = new UpdateEquipmentCommand(Guid.NewGuid(), dto); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(NotFoundStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be($"[NOT FOUND] {nameof(EquipmentType)} with id '{dto.EquipmentTypeId}' does not exist"); + } + + [Test] + public async Task should_return_conflict_when_equipment_name_and_type_is_not_unique_async() + { + // Arrange + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); + var equipment = Equipment.Create("Test-Equipment", equipmentType.Id, 1); + + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) + { + await AddEntitiesToTable(dbContext, equipmentType); + await AddEntitiesToTable(dbContext, equipment); + } + + var dto = new EquipmentWriteDto(equipment.Name, equipmentType.Id, 100); + var command = new UpdateEquipmentCommand(Guid.NewGuid(), dto); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(ConflictStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be(string.Format(DB_FORMAT.EQUIPMENT_DUPLICATE_NAME_TYPE, + equipment.Name, equipmentType.Id)); + } + + [Test] + public async Task should_return_not_found_when_equipment_does_not_exist_async() + { + // Arrange + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); + var equipment = Equipment.Create("Test-Equipment", equipmentType.Id, 1); + + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) + { + await ClearTableContentsForAsync(dbContext); + await ClearTableContentsForAsync(dbContext); + await AddEntitiesToTable(dbContext, equipmentType); + await AddEntitiesToTable(dbContext, equipment); + } + + var dto = new EquipmentWriteDto("Test-Equipment-Updated", equipmentType.Id, 100); + var command = new UpdateEquipmentCommand(Guid.NewGuid(), dto); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeFalse(); + result.StatusCode.Should().Be(NotFoundStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().NotBeNullOrWhiteSpace(); + result.Error.Should().Be($"[NOT FOUND] {nameof(Equipment)} with id '{command.Id}' does not exist"); + } + + [Test] + public async Task should_return_success_async() + { + // Arrange + var equipmentType = EquipmentType.Create("Test-Equipment-Type"); + var equipment = Equipment.Create("Test-Equipment", equipmentType.Id, 1); + + await using (var dbContext = await _testDbContextFactory.CreateDbContextAsync(Cts.Token)) + { + await AddEntitiesToTable(dbContext, equipmentType); + await AddEntitiesToTable(dbContext, equipment); + } + + var dto = new EquipmentWriteDto("Test-Equipment-Updated", equipmentType.Id, 100); + var command = new UpdateEquipmentCommand(equipment.Id, dto); + + // Act + var result = await _testCandidate.Handle(command, Cts.Token); + + // Assert + result.Should().NotBeNull(); + result.Succeeded.Should().BeTrue(); + result.StatusCode.Should().Be(OkStatusCode); + result.Data.Should().BeNull(); + result.Error.Should().BeNullOrWhiteSpace(); + } +} \ No newline at end of file diff --git a/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Validator.cs b/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Validator.cs new file mode 100644 index 0000000..1d930dd --- /dev/null +++ b/StudioManager.Application.Tests/Equipments/UpdateEquipmentCommandHandlerTests/Validator.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Equipments; +using StudioManager.Application.Equipments.Update; +using StudioManager.Application.Equipments.Validators; + +namespace StudioManager.Application.Tests.Equipments.UpdateEquipmentCommandHandlerTests; + +public sealed class Validator +{ + private const string Name = "Validation-Name"; + private static readonly Guid EquipmentTypeId = Guid.NewGuid(); + private const int Quantity = 1; + + [Test] + public async Task should_return_error_when_id_is_empty_async() + { + var validator = new UpdateEquipmentCommandValidator(new EquipmentWriteDtoValidator()); + var command = new UpdateEquipmentCommand(Guid.Empty, new EquipmentWriteDto(Name, EquipmentTypeId, Quantity)); + var result = await validator.ValidateAsync(command); + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(x => x.PropertyName == nameof(command.Id) && x.ErrorMessage == "'Id' must not be empty."); + } + + [Test] + public async Task should_return_error_when_equipment_is_null_async() + { + var validator = new UpdateEquipmentCommandValidator(new EquipmentWriteDtoValidator()); + var command = new UpdateEquipmentCommand(EquipmentTypeId, null!); + var result = await validator.ValidateAsync(command); + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(x => x.PropertyName == nameof(command.Equipment) && x.ErrorMessage == "'Equipment' must not be empty."); + } + + [Test] + public async Task should_return_error_when_equipment_model_is_invalid_async() + { + var validator = new UpdateEquipmentCommandValidator(new EquipmentWriteDtoValidator()); + var command = new UpdateEquipmentCommand(EquipmentTypeId, new EquipmentWriteDto(string.Empty, Guid.Empty, 0)); + var result = await validator.ValidateAsync(command); + result.IsValid.Should().BeFalse(); + result.Errors.Should().Contain(x => x.PropertyName == "Equipment.Name" && x.ErrorMessage == "'Name' must not be empty."); + result.Errors.Should().Contain(x => x.PropertyName == "Equipment.EquipmentTypeId" && x.ErrorMessage == "'Equipment Type Id' must not be empty."); + result.Errors.Should().Contain(x => x.PropertyName == "Equipment.Quantity" && x.ErrorMessage == "'Quantity' must be greater than or equal to '1'."); + } +} \ No newline at end of file diff --git a/StudioManager.Application.Tests/Mapps/Equipments/EquipmentProjectionTests.cs b/StudioManager.Application.Tests/Mapps/Equipments/EquipmentProjectionTests.cs new file mode 100644 index 0000000..58e8675 --- /dev/null +++ b/StudioManager.Application.Tests/Mapps/Equipments/EquipmentProjectionTests.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using FluentAssertions; +using NUnit.Framework; +using StudioManager.API.Contracts.Equipments; +using StudioManager.API.Contracts.EquipmentTypes; +using StudioManager.Domain.Entities; +using StudioManager.Tests.Common.AutoMapperExtensions; + +namespace StudioManager.Application.Tests.Mapps.Equipments; + +[ExcludeFromCodeCoverage] +public sealed class EquipmentProjectionTests +{ + [Test] + public void should_map_equipment_to_equipment_projection() + { + // Arrange + var equipmentType = EquipmentType.Create("Test Equipment Type"); + var equipment = Equipment.Create("Test Equipment", equipmentType.Id, 1); + equipment.EquipmentType = equipmentType; + var mapper = MappingTestHelper.Mapper; + + // Act + var result = mapper.Map(equipment); + + // Assert + result.Should().NotBeNull(); + result.Id.Should().NotBeEmpty(); + result.Id.Should().Be(equipment.Id); + result.Name.Should().NotBeNullOrWhiteSpace(); + result.Name.Should().Be(equipment.Name); + result.Quantity.Should().Be(equipment.Quantity); + result.InitialQuantity.Should().Be(equipment.InitialQuantity); + result.EquipmentType.Should().NotBeNull(); + result.EquipmentType.Should().BeOfType(); + result.EquipmentType.Id.Should().Be(equipmentType.Id); + result.EquipmentType.Name.Should().NotBeNullOrWhiteSpace(); + result.EquipmentType.Name.Should().Be(equipmentType.Name); + } +} \ No newline at end of file diff --git a/StudioManager.Application/DbContextExtensions/PaginationExtensions.cs b/StudioManager.Application/DbContextExtensions/PaginationExtensions.cs index 9eb2ea2..7b12dcb 100644 --- a/StudioManager.Application/DbContextExtensions/PaginationExtensions.cs +++ b/StudioManager.Application/DbContextExtensions/PaginationExtensions.cs @@ -1,35 +1,20 @@ -using AutoMapper; +using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore; using StudioManager.API.Contracts.Pagination; using StudioManager.Domain.Common.Results; namespace StudioManager.Application.DbContextExtensions; +[ExcludeFromCodeCoverage] public static class PaginationExtensions { - public static IQueryable ApplyPaging(this IQueryable queryable, PaginationDto paginationDto) + private static IQueryable ApplyPaging(this IQueryable queryable, PaginationDto paginationDto) { return paginationDto.Limit == 0 ? queryable : queryable.Skip(paginationDto.GetOffset()).Take(paginationDto.Limit); } - public static IEnumerable ApplyPaging(this IEnumerable enumerable, PaginationDto paginationDto) - { - return paginationDto.Limit == 0 - ? enumerable - : enumerable.Skip(paginationDto.GetOffset()).Take(paginationDto.Limit); - } - - public static async Task>> ApplyPagingAndMapAsync( - this IQueryable queryable, - PaginationDto pagination, - IMapper mapper) - { - return await queryable.ApplyPagingAndMapAsync(pagination, - x => Task.FromResult(mapper.Map>(x))); - } - public static async Task>> ApplyPagingAsync( this IQueryable queryable, PaginationDto pagination) @@ -40,18 +25,6 @@ public static async Task>> ApplyPagingAsync( return CreateResult(data, count, pagination); } - public static async Task>> ApplyPagingAndMapAsync( - this IQueryable queryable, - PaginationDto pagination, - Func, Task>> mappingFunc) - { - var data = await queryable.ApplyPaging(pagination).ToListAsync(); - var mappedData = await mappingFunc(data); - var count = await queryable.CountAsync(); - - return CreateResult(mappedData.ToList(), count, pagination); - } - private static QueryResult> CreateResult(List data, int count, PaginationDto pagination) { return QueryResult.Success(new PagingResultDto diff --git a/StudioManager.Application/Equipments/Common/EquipmentChecker.cs b/StudioManager.Application/Equipments/Common/EquipmentChecker.cs index c71e169..97cbb5e 100644 --- a/StudioManager.Application/Equipments/Common/EquipmentChecker.cs +++ b/StudioManager.Application/Equipments/Common/EquipmentChecker.cs @@ -1,4 +1,4 @@ -using StudioManager.API.Contracts.Equipment; +using StudioManager.API.Contracts.Equipments; using StudioManager.Application.DbContextExtensions; using StudioManager.Domain.Common.Results; using StudioManager.Domain.Entities; diff --git a/StudioManager.Application/Equipments/Create/CreateEquipmentCommand.cs b/StudioManager.Application/Equipments/Create/CreateEquipmentCommand.cs index fbbeb74..91cc7b7 100644 --- a/StudioManager.Application/Equipments/Create/CreateEquipmentCommand.cs +++ b/StudioManager.Application/Equipments/Create/CreateEquipmentCommand.cs @@ -1,5 +1,5 @@ using MediatR; -using StudioManager.API.Contracts.Equipment; +using StudioManager.API.Contracts.Equipments; using StudioManager.Domain.Common.Results; namespace StudioManager.Application.Equipments.Create; diff --git a/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQuery.cs b/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQuery.cs index 4de3a00..06d3f1d 100644 --- a/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQuery.cs +++ b/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQuery.cs @@ -1,5 +1,5 @@ using MediatR; -using StudioManager.API.Contracts.Equipment; +using StudioManager.API.Contracts.Equipments; using StudioManager.API.Contracts.Pagination; using StudioManager.Domain.Common.Results; using StudioManager.Domain.Filters; diff --git a/StudioManager.Application/Equipments/GetAll/GetAllEquipmentTypesQueryHandler.cs b/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQueryHandler.cs similarity index 91% rename from StudioManager.Application/Equipments/GetAll/GetAllEquipmentTypesQueryHandler.cs rename to StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQueryHandler.cs index fe24409..a64fab5 100644 --- a/StudioManager.Application/Equipments/GetAll/GetAllEquipmentTypesQueryHandler.cs +++ b/StudioManager.Application/Equipments/GetAll/GetAllEquipmentsQueryHandler.cs @@ -2,7 +2,7 @@ using AutoMapper.QueryableExtensions; using MediatR; using Microsoft.EntityFrameworkCore; -using StudioManager.API.Contracts.Equipment; +using StudioManager.API.Contracts.Equipments; using StudioManager.API.Contracts.Pagination; using StudioManager.Application.DbContextExtensions; using StudioManager.Domain.Common.Results; @@ -10,7 +10,7 @@ namespace StudioManager.Application.Equipments.GetAll; -public sealed class GetAllEquipmentTypesQueryHandler( +public sealed class GetAllEquipmentsQueryHandler( IMapper mapper, IDbContextFactory dbContextFactory) : IRequestHandler>> diff --git a/StudioManager.Application/Equipments/MapperProjections/EquipmentProjection.cs b/StudioManager.Application/Equipments/MapperProjections/EquipmentProjection.cs index 840ac0b..80f2bb7 100644 --- a/StudioManager.Application/Equipments/MapperProjections/EquipmentProjection.cs +++ b/StudioManager.Application/Equipments/MapperProjections/EquipmentProjection.cs @@ -1,5 +1,5 @@ using AutoMapper; -using StudioManager.API.Contracts.Equipment; +using StudioManager.API.Contracts.Equipments; using StudioManager.Domain.Entities; namespace StudioManager.Application.Equipments.MapperProjections; diff --git a/StudioManager.Application/Equipments/Update/UpdateEquipmentCommand.cs b/StudioManager.Application/Equipments/Update/UpdateEquipmentCommand.cs index f90f0e3..1f6465a 100644 --- a/StudioManager.Application/Equipments/Update/UpdateEquipmentCommand.cs +++ b/StudioManager.Application/Equipments/Update/UpdateEquipmentCommand.cs @@ -1,5 +1,5 @@ using MediatR; -using StudioManager.API.Contracts.Equipment; +using StudioManager.API.Contracts.Equipments; using StudioManager.Domain.Common.Results; namespace StudioManager.Application.Equipments.Update; diff --git a/StudioManager.Application/Equipments/Validators/EquipmentWriteDtoValidator.cs b/StudioManager.Application/Equipments/Validators/EquipmentWriteDtoValidator.cs index 557cf16..5e531e9 100644 --- a/StudioManager.Application/Equipments/Validators/EquipmentWriteDtoValidator.cs +++ b/StudioManager.Application/Equipments/Validators/EquipmentWriteDtoValidator.cs @@ -1,5 +1,5 @@ using FluentValidation; -using StudioManager.API.Contracts.Equipment; +using StudioManager.API.Contracts.Equipments; namespace StudioManager.Application.Equipments.Validators; diff --git a/StudioManager.Domain/Common/Results/CheckResult.cs b/StudioManager.Domain/Common/Results/CheckResult.cs index dd9beac..2a82908 100644 --- a/StudioManager.Domain/Common/Results/CheckResult.cs +++ b/StudioManager.Domain/Common/Results/CheckResult.cs @@ -1,5 +1,8 @@ -namespace StudioManager.Domain.Common.Results; +using System.Diagnostics.CodeAnalysis; +namespace StudioManager.Domain.Common.Results; + +[ExcludeFromCodeCoverage] public sealed class CheckResult { private CheckResult() { } diff --git a/StudioManager.Domain/Common/Results/CommandResult.cs b/StudioManager.Domain/Common/Results/CommandResult.cs index b5b2a2a..4800d46 100644 --- a/StudioManager.Domain/Common/Results/CommandResult.cs +++ b/StudioManager.Domain/Common/Results/CommandResult.cs @@ -1,7 +1,9 @@ -using System.Net; +using System.Diagnostics.CodeAnalysis; +using System.Net; namespace StudioManager.Domain.Common.Results; +[ExcludeFromCodeCoverage] public sealed class CommandResult : IRequestResult { private CommandResult() { } diff --git a/StudioManager.Domain/Common/Results/QueryResult.cs b/StudioManager.Domain/Common/Results/QueryResult.cs index 886c84a..93b8600 100644 --- a/StudioManager.Domain/Common/Results/QueryResult.cs +++ b/StudioManager.Domain/Common/Results/QueryResult.cs @@ -1,7 +1,9 @@ -using System.Net; +using System.Diagnostics.CodeAnalysis; +using System.Net; namespace StudioManager.Domain.Common.Results; +[ExcludeFromCodeCoverage] public class QueryResult : IRequestResult { public bool Succeeded { get; private init; } diff --git a/StudioManager.Domain/Entities/Equipment.cs b/StudioManager.Domain/Entities/Equipment.cs index f76c471..8fdb8ef 100644 --- a/StudioManager.Domain/Entities/Equipment.cs +++ b/StudioManager.Domain/Entities/Equipment.cs @@ -1,4 +1,6 @@ -namespace StudioManager.Domain.Entities; +using StudioManager.Domain.ErrorMessages; + +namespace StudioManager.Domain.Entities; public sealed class Equipment : EntityBase { @@ -15,6 +17,15 @@ public sealed class Equipment : EntityBase #endregion + public void Reserve(int quantity) + { + Quantity -= quantity; + if (Quantity < 0) + { + throw new InvalidOperationException(EX.EQUIPMENT_NEGATIVE_QUANTITY); + } + } + public static Equipment Create(string name, Guid equipmentTypeId, int quantity) { const string imagePlaceholder = "https://cdn3.iconfinder.com/data/icons/objects/512/equipment-512.png"; diff --git a/StudioManager.Domain/Entities/EquipmentType.cs b/StudioManager.Domain/Entities/EquipmentType.cs index d44fe96..df3adaf 100644 --- a/StudioManager.Domain/Entities/EquipmentType.cs +++ b/StudioManager.Domain/Entities/EquipmentType.cs @@ -6,7 +6,7 @@ public sealed class EquipmentType : EntityBase #region EntityRelations - public IQueryable Equipments { get; set; } = default!; + public ICollection Equipments { get; set; } = new List(); #endregion diff --git a/StudioManager.Domain/ErrorMessages/EX.cs b/StudioManager.Domain/ErrorMessages/EX.cs index 185dc41..3e477d6 100644 --- a/StudioManager.Domain/ErrorMessages/EX.cs +++ b/StudioManager.Domain/ErrorMessages/EX.cs @@ -5,4 +5,5 @@ public static class EX { public const string SUCCESS_FROM_ERROR = "Cannot create success result from error"; public const string ERROR_FROM_SUCCESS = "Cannot create error result from success"; + public const string EQUIPMENT_NEGATIVE_QUANTITY = "Equipment quantity cannot be negative"; } \ No newline at end of file diff --git a/StudioManager.Tests.Common/DbContextExtensions/TestDbMigrator.cs b/StudioManager.Tests.Common/DbContextExtensions/TestDbMigrator.cs index 70d9f21..78d557f 100644 --- a/StudioManager.Tests.Common/DbContextExtensions/TestDbMigrator.cs +++ b/StudioManager.Tests.Common/DbContextExtensions/TestDbMigrator.cs @@ -12,7 +12,7 @@ public class TestDbMigrator public async Task StartDbAsync() { - _postgresContainer = new PostgreSqlBuilder().WithDatabase("testdb").Build(); + _postgresContainer = new PostgreSqlBuilder().WithDatabase($"StudioManager_Test_{Guid.NewGuid()}").Build(); await _postgresContainer.StartAsync(); _postgresContainer.GetConnectionString(); } diff --git a/StudioManager.Tests.Common/IntegrationTestBase.cs b/StudioManager.Tests.Common/IntegrationTestBase.cs index 74ae9b7..6eb8da9 100644 --- a/StudioManager.Tests.Common/IntegrationTestBase.cs +++ b/StudioManager.Tests.Common/IntegrationTestBase.cs @@ -1,4 +1,5 @@ -using System.Linq.Expressions; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using System.Net; using AutoMapper; using Microsoft.EntityFrameworkCore; @@ -10,6 +11,7 @@ namespace StudioManager.Tests.Common; +[ExcludeFromCodeCoverage] public abstract class IntegrationTestBase { protected static readonly CancellationTokenSource Cts = new(); @@ -44,11 +46,11 @@ protected static async Task ClearTableContentsForAsync( { if (filter is not null) { - await db.Set().Where(filter).ExecuteDeleteAsync(); + await db.Set().Where(filter).ExecuteDeleteAsync(Cts.Token); } else { - await db.Set().ExecuteDeleteAsync(); + await db.Set().ExecuteDeleteAsync(Cts.Token); } await db.SaveChangesAsync(); @@ -59,7 +61,7 @@ protected static async Task AddEntitiesToTable( params T[] entities) where T : class { - await db.Set().AddRangeAsync(entities); - await db.SaveChangesAsync(); + await db.Set().AddRangeAsync(entities, Cts.Token); + await db.SaveChangesAsync(Cts.Token); } } \ No newline at end of file