diff --git a/xChanger.Api.Tests.Unit/Services/Foundations/Pets/PetServiceTests.Exceptions.Modify.cs b/xChanger.Api.Tests.Unit/Services/Foundations/Pets/PetServiceTests.Exceptions.Modify.cs new file mode 100644 index 0000000..972b1d6 --- /dev/null +++ b/xChanger.Api.Tests.Unit/Services/Foundations/Pets/PetServiceTests.Exceptions.Modify.cs @@ -0,0 +1,209 @@ +//- - - - - - - - - - - - - - - - - - - - - - - - - - +// Copyright (c) Coalition of Good-Hearted Engineers +// Free to Use for Precise File Conversion +//- - - - - - - - - - - - - - - - - - - - - - - - - - + +using FluentAssertions; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Moq; +using xChanger.Api.Models.Foundations.Pets; +using xChanger.Api.Models.Foundations.Pets.Exceptions; + +namespace xChanger.Api.Tests.Unit.Services.Foundations.Pets +{ + public partial class PetServiceTests + { + [Fact] + public async Task ShouldThrowCriticalDependencyExceptionOnModifyIfSqlErrorOccursAndLogItAsync() + { + // given + Pet randomPet = CreateRandomPet(); + Pet somePet = randomPet; + Guid petId = somePet.Id; + SqlException sqlException = GetSqlError(); + + var failedPetStorageException = + new FailedPetStorageException(sqlException); + + var expectedPetDependencyException = + new PetDependencyException(failedPetStorageException); + + this.storageBrokerMock.Setup(broker => + broker.SelectPetByIdAsync(petId)) + .Throws(sqlException); + + // when + ValueTask modifyPetTask = + this.petService.ModifyPetAsync(somePet); + + PetDependencyException actualPetDependencyException = + await Assert.ThrowsAsync(() => + modifyPetTask.AsTask()); + + // then + actualPetDependencyException.Should() + .BeEquivalentTo(expectedPetDependencyException); + + this.loggingBrokerMock.Verify(broker => + broker.LogCritical(It.Is(SameExceptionAs( + expectedPetDependencyException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.SelectPetByIdAsync(petId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdatePetAsync(somePet), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldThrowDependencyExceptionOnModifyIfDatabaseUpdateExceptionOccursAndLogItAsync() + { + // given + Pet randomPet = CreateRandomPet(); + Pet somePet = randomPet; + Guid petId = somePet.Id; + var databaseUpdateException = new DbUpdateException(); + + var failedPetStorageException = + new FailedPetStorageException(databaseUpdateException); + + var expectedPetDependencyException = + new PetDependencyException(failedPetStorageException); + + this.storageBrokerMock.Setup(broker => + broker.SelectPetByIdAsync(petId)) + .Throws(databaseUpdateException); + + // when + ValueTask modifyPetTask = + this.petService.ModifyPetAsync(somePet); + + PetDependencyException actualPetDependencyException = + await Assert.ThrowsAsync(() => + modifyPetTask.AsTask()); + + // then + actualPetDependencyException.Should() + .BeEquivalentTo(expectedPetDependencyException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedPetDependencyException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.SelectPetByIdAsync(petId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdatePetAsync(somePet), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldThrowDependencyValidationExceptionOnModifyIfDatabaseUpdateConcurrencyErrorOccursAndLogItAsync() + { + // given + Pet randomPet = CreateRandomPet(); + Pet somePet = randomPet; + Guid petId = somePet.Id; + var dbUpdateConcurrencyException = new DbUpdateConcurrencyException(); + + var lockedPetException = + new LockedPetException(dbUpdateConcurrencyException); + + var expectedPetDependencyValidationException = + new PetDependencyValidationException(lockedPetException); + + this.storageBrokerMock.Setup(broker => + broker.SelectPetByIdAsync(petId)) + .Throws(dbUpdateConcurrencyException); + + // when + ValueTask modifyPetTask = + this.petService.ModifyPetAsync(somePet); + + PetDependencyValidationException actualPetDependencyValidationException = + await Assert.ThrowsAsync(() => + modifyPetTask.AsTask()); + + // then + actualPetDependencyValidationException.Should() + .BeEquivalentTo(expectedPetDependencyValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedPetDependencyValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.SelectPetByIdAsync(petId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdatePetAsync(somePet), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldThrowServiceExceptionOnModifyIfDatabaseUpdateErrorOccursAndLogItAsync() + { + // given + Pet randomPet = CreateRandomPet(); + Pet somePet = randomPet; + Guid petId = somePet.Id; + var serviceException = new Exception(); + + var failedPetServiceException = + new FailedPetServiceException(serviceException); + + var expectedPetServiceException = + new PetServiceException(failedPetServiceException); + + this.storageBrokerMock.Setup(broker => + broker.SelectPetByIdAsync(petId)) + .Throws(serviceException); + + // when + ValueTask modifyPetTask = + this.petService.ModifyPetAsync(somePet); + + PetServiceException actualPetServiceException = + await Assert.ThrowsAsync(() => + modifyPetTask.AsTask()); + + // then + actualPetServiceException.Should() + .BeEquivalentTo(expectedPetServiceException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedPetServiceException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.SelectPetByIdAsync(petId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdatePetAsync(somePet), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/xChanger.Api.Tests.Unit/Services/Foundations/Pets/PetServiceTests.Logic.Modify.cs b/xChanger.Api.Tests.Unit/Services/Foundations/Pets/PetServiceTests.Logic.Modify.cs new file mode 100644 index 0000000..ef139f9 --- /dev/null +++ b/xChanger.Api.Tests.Unit/Services/Foundations/Pets/PetServiceTests.Logic.Modify.cs @@ -0,0 +1,53 @@ +//- - - - - - - - - - - - - - - - - - - - - - - - - - +// Copyright (c) Coalition of Good-Hearted Engineers +// Free to Use for Precise File Conversion +//- - - - - - - - - - - - - - - - - - - - - - - - - - + +using FluentAssertions; +using Force.DeepCloner; +using Moq; +using xChanger.Api.Models.Foundations.Pets; + +namespace xChanger.Api.Tests.Unit.Services.Foundations.Pets +{ + public partial class PetServiceTests + { + [Fact] + public async Task ShouldModifyPetAsync() + { + // given + Pet randomPet = CreateRandomPet(); + Pet inputPet = randomPet; + Pet persistedPet = inputPet.DeepClone(); + Pet updatedPet = inputPet; + Pet expectedPet = updatedPet.DeepClone(); + Guid InputPetId = inputPet.Id; + + this.storageBrokerMock.Setup(broker => + broker.SelectPetByIdAsync(InputPetId)) + .ReturnsAsync(persistedPet); + + this.storageBrokerMock.Setup(broker => + broker.UpdatePetAsync(inputPet)) + .ReturnsAsync(updatedPet); + + // when + Pet actualPet = + await this.petService.ModifyPetAsync(inputPet); + + // then + actualPet.Should().BeEquivalentTo(expectedPet); + + this.storageBrokerMock.Verify(broker => + broker.SelectPetByIdAsync(InputPetId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdatePetAsync(inputPet), + Times.Once); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/xChanger.Api.Tests.Unit/Services/Foundations/Pets/PetServiceTests.Validations.Modify.cs b/xChanger.Api.Tests.Unit/Services/Foundations/Pets/PetServiceTests.Validations.Modify.cs new file mode 100644 index 0000000..57a5533 --- /dev/null +++ b/xChanger.Api.Tests.Unit/Services/Foundations/Pets/PetServiceTests.Validations.Modify.cs @@ -0,0 +1,152 @@ +//- - - - - - - - - - - - - - - - - - - - - - - - - - +// Copyright (c) Coalition of Good-Hearted Engineers +// Free to Use for Precise File Conversion +//- - - - - - - - - - - - - - - - - - - - - - - - - - + +using FluentAssertions; +using Moq; +using xChanger.Api.Models.Foundations.Pets; +using xChanger.Api.Models.Foundations.Pets.Exceptions; + +namespace xChanger.Api.Tests.Unit.Services.Foundations.Pets +{ + public partial class PetServiceTests + { + [Fact] + public async Task ShouldThrowValidationExceptionOnModifyIfPetIsNullAndLogItAsync() + { + // given + Pet nullPet = null; + var nullPetException = new NullPetException(); + + var expectedPetValidationException = + new PetValidationException(nullPetException); + + // when + ValueTask modifyPetTask = + this.petService.ModifyPetAsync(nullPet); + + PetValidationException actualPetValidationException = + await Assert.ThrowsAsync( + modifyPetTask.AsTask); + + // then + actualPetValidationException.Should() + .BeEquivalentTo(expectedPetValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedPetValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdatePetAsync(It.IsAny()), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ShouldThrowValidationExceptionOnModifyIfPetIsInvalidAndLogItAsync( + string invalidText) + { + // given + Pet invalidPet = new Pet + { + Name = invalidText + }; + + var invalidPetException = new InvalidPetException(); + + invalidPetException.AddData( + key: nameof(Pet.Id), + values: "Id is required"); + + invalidPetException.AddData( + key: nameof(Pet.Name), + values: "Text is required"); + + invalidPetException.AddData( + key: nameof(Pet.PersonId), + values: "Id is required"); + + var expectedPetValidationException = + new PetValidationException(invalidPetException); + + // when + ValueTask modifyPetTask = + this.petService.ModifyPetAsync(invalidPet); + + PetValidationException actualPetValidationException = + await Assert.ThrowsAsync( + modifyPetTask.AsTask); + + // then + actualPetValidationException.Should() + .BeEquivalentTo(expectedPetValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedPetValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdatePetAsync(It.IsAny()), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldThrowValidationExceptionOnModifyIfPetDoesNotExistAndLogItAsync() + { + // given + Pet randomPet = CreateRandomPet(); + Pet nonExistPet = randomPet; + Pet nullPet = null; + + var notFoundPetException = + new NotFoundPetException(nonExistPet.Id); + + var expectedPetValidationException = + new PetValidationException(notFoundPetException); + + this.storageBrokerMock.Setup(broker => + broker.SelectPetByIdAsync(nonExistPet.Id)) + .ReturnsAsync(nullPet); + + // when + ValueTask modifyPetTask = + this.petService.ModifyPetAsync(nonExistPet); + + PetValidationException actualPetValidationException = + await Assert.ThrowsAsync(() => + modifyPetTask.AsTask()); + + // then + actualPetValidationException.Should() + .BeEquivalentTo(expectedPetValidationException); + + this.storageBrokerMock.Verify(broker => + broker.SelectPetByIdAsync(nonExistPet.Id), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedPetValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdatePetAsync(nonExistPet), + Times.Never); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/xChanger.Api/Models/Foundations/Pets/Exceptions/LockedPetException.cs b/xChanger.Api/Models/Foundations/Pets/Exceptions/LockedPetException.cs new file mode 100644 index 0000000..3907fe4 --- /dev/null +++ b/xChanger.Api/Models/Foundations/Pets/Exceptions/LockedPetException.cs @@ -0,0 +1,17 @@ +//- - - - - - - - - - - - - - - - - - - - - - - - - - +// Copyright (c) Coalition of Good-Hearted Engineers +// Free to Use for Precise File Conversion +//- - - - - - - - - - - - - - - - - - - - - - - - - - + +using Xeptions; + +namespace xChanger.Api.Models.Foundations.Pets.Exceptions +{ + public class LockedPetException : Xeption + { + public LockedPetException(Exception innerException) + : base(message: "Pet is locked, please try again", + innerException) + { } + } +} diff --git a/xChanger.Api/Services/Foundations/Pets/IPetService.cs b/xChanger.Api/Services/Foundations/Pets/IPetService.cs index fc6c342..003b5cd 100644 --- a/xChanger.Api/Services/Foundations/Pets/IPetService.cs +++ b/xChanger.Api/Services/Foundations/Pets/IPetService.cs @@ -12,5 +12,6 @@ public interface IPetService ValueTask AddPetAsync(Pet pet); IQueryable RetrieveAllPets(); ValueTask RetrievePetByIdAsync(Guid petId); + ValueTask ModifyPetAsync(Pet pet); } } diff --git a/xChanger.Api/Services/Foundations/Pets/PetService.Exceptions.cs b/xChanger.Api/Services/Foundations/Pets/PetService.Exceptions.cs index c3028ca..30491a8 100644 --- a/xChanger.Api/Services/Foundations/Pets/PetService.Exceptions.cs +++ b/xChanger.Api/Services/Foundations/Pets/PetService.Exceptions.cs @@ -5,6 +5,7 @@ using EFxceptions.Models.Exceptions; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; using xChanger.Api.Models.Foundations.Pets; using xChanger.Api.Models.Foundations.Pets.Exceptions; using Xeptions; @@ -41,6 +42,18 @@ private async ValueTask TryCatch(ReturningPetFunction returningPetFunction) throw CreateAndLogCriticalDependencyException(failedPetStorageException); } + catch (DbUpdateConcurrencyException dbUpdateConcurrencyException) + { + var lockedPetException = new LockedPetException(dbUpdateConcurrencyException); + + throw CreateAndLogDependencyValidationException(lockedPetException); + } + catch (DbUpdateException dbUpdateException) + { + var failedPetStorageException = new FailedPetStorageException(dbUpdateException); + + throw CreateAndLogDependencyException(failedPetStorageException); + } catch (DuplicateKeyException duplicateKeyException) { var alreadyExistsPetException = @@ -115,5 +128,13 @@ private PetServiceException CreateAndLogServiceException(Xeption exception) return petServiceException; } + + private PetDependencyException CreateAndLogDependencyException(Xeption exception) + { + var petDependencyException = new PetDependencyException(exception); + this.loggingBroker.LogError(petDependencyException); + + return petDependencyException; + } } } diff --git a/xChanger.Api/Services/Foundations/Pets/PetService.Validations.cs b/xChanger.Api/Services/Foundations/Pets/PetService.Validations.cs index 3b59c04..0afb40a 100644 --- a/xChanger.Api/Services/Foundations/Pets/PetService.Validations.cs +++ b/xChanger.Api/Services/Foundations/Pets/PetService.Validations.cs @@ -21,6 +21,28 @@ private void ValidatePetOnAdd(Pet pet) (Rule: IsInvalid(pet.PersonId), Parameter: nameof(Pet.PersonId))); } + private static void ValidatePetOnModify(Pet pet) + { + ValidatePetNotNull(pet); + + Validate( + (Rule: IsInvalid(pet.Id), Parameter: nameof(Pet.Id)), + (Rule: IsInvalid(pet.Name), Parameter: nameof(Pet.Name)), + (Rule: IsInvalid(pet.Type), Parameter: nameof(Pet.Type)), + (Rule: IsInvalid(pet.PersonId), Parameter: nameof(Pet.PersonId))); + } + + private static void ValidateAgainstStoragePetOnModify(Pet pet, Pet storagePet) + { + ValidateStoragePet(storagePet, pet.Id); + + Validate( + (Rule: IsInvalid(pet.Id), Parameter: nameof(Pet.Id)), + (Rule: IsInvalid(pet.Name), Parameter: nameof(Pet.Name)), + (Rule: IsInvalid(pet.Type), Parameter: nameof(Pet.Type)), + (Rule: IsInvalid(pet.PersonId), Parameter: nameof(Pet.PersonId))); + } + private static void ValidatePetId(Guid petId) => Validate((Rule: IsInvalid(petId), Parameter: nameof(Pet.Id))); diff --git a/xChanger.Api/Services/Foundations/Pets/PetService.cs b/xChanger.Api/Services/Foundations/Pets/PetService.cs index 1db6dca..f15bd96 100644 --- a/xChanger.Api/Services/Foundations/Pets/PetService.cs +++ b/xChanger.Api/Services/Foundations/Pets/PetService.cs @@ -45,5 +45,18 @@ public ValueTask RetrievePetByIdAsync(Guid petId) => return maybePet; }); + + public ValueTask ModifyPetAsync(Pet pet) => + TryCatch(async () => + { + ValidatePetOnModify(pet); + + Pet maybePet = + await this.storageBroker.SelectPetByIdAsync(pet.Id); + + ValidateAgainstStoragePetOnModify(pet, maybePet); + + return await storageBroker.UpdatePetAsync(pet); + }); } }