diff --git a/LibraryManagement.Api.Tests.Unit/Services/Foundations/Readers/ReaderServiceTests.Exceptions.Modify.cs b/LibraryManagement.Api.Tests.Unit/Services/Foundations/Readers/ReaderServiceTests.Exceptions.Modify.cs new file mode 100644 index 0000000..93cc7f9 --- /dev/null +++ b/LibraryManagement.Api.Tests.Unit/Services/Foundations/Readers/ReaderServiceTests.Exceptions.Modify.cs @@ -0,0 +1,209 @@ +//----------------------------------------------------------- +// Copyright (c) Coalition of Good-Hearted Engineers +// Free To Use To Build Reliable Library Management Solutions +//----------------------------------------------------------- + +using FluentAssertions; +using LibraryManagement.Api.Models.Foundations.Readers; +using LibraryManagement.Api.Models.Foundations.Readers.Exceptions; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Moq; + +namespace LibraryManagement.Api.Tests.Unit.Services.Foundations.Readers +{ + public partial class ReaderServiceTests + { + [Fact] + public async Task ShouldThrowCriticalDependencyExceptionOnModifyIfSqlErrorOccursAndLogItAsync() + { + // given + Reader randomReader = CreateRandomReader(); + Reader someReader = randomReader; + Guid readerId = someReader.ReaderId; + SqlException sqlException = GetSqlError(); + + var failedReaderStorageException = + new FailedReaderStorageException(sqlException); + + var expectedReaderDependencyException = + new ReaderDependencyException(failedReaderStorageException); + + this.storageBrokerMock.Setup(broker => + broker.SelectReaderByIdAsync(readerId)) + .Throws(sqlException); + + // when + ValueTask modifyReaderTask = + this.readerService.ModifyReaderAsync(someReader); + + ReaderDependencyException actualReaderDependencyException = + await Assert.ThrowsAsync(() => + modifyReaderTask.AsTask()); + + // then + actualReaderDependencyException.Should() + .BeEquivalentTo(expectedReaderDependencyException); + + this.loggingBrokerMock.Verify(broker => + broker.LogCritical(It.Is(SameExceptionAs( + expectedReaderDependencyException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.SelectReaderByIdAsync(readerId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdateReaderAsync(someReader), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldThrowDependencyExceptionOnModifyIfDatabaseUpdateExceptionOccursAndLogItAsync() + { + // given + Reader randomReader = CreateRandomReader(); + Reader someReader = randomReader; + Guid readerId = someReader.ReaderId; + var databaseUpdateException = new DbUpdateException(); + + var failedReaderStorageException = + new FailedReaderStorageException(databaseUpdateException); + + var expectedReaderDependencyException = + new ReaderDependencyException(failedReaderStorageException); + + this.storageBrokerMock.Setup(broker => + broker.SelectReaderByIdAsync(readerId)) + .Throws(databaseUpdateException); + + // when + ValueTask modifyReaderTask = + this.readerService.ModifyReaderAsync(someReader); + + ReaderDependencyException actualReaderDependencyException = + await Assert.ThrowsAsync(() => + modifyReaderTask.AsTask()); + + // then + actualReaderDependencyException.Should() + .BeEquivalentTo(expectedReaderDependencyException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedReaderDependencyException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.SelectReaderByIdAsync(readerId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdateReaderAsync(someReader), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldThrowDependencyValidationExceptionOnModifyIfDatabaseUpdateConcurrencyErrorOccursAndLogItAsync() + { + // given + Reader randomReader = CreateRandomReader(); + Reader someReader = randomReader; + Guid readerId = someReader.ReaderId; + var dbUpdateConcurrencyException = new DbUpdateConcurrencyException(); + + var lockedReaderException = + new LockedReaderException(dbUpdateConcurrencyException); + + var expectedReaderDependencyValidationException = + new ReaderDependencyValidationException(lockedReaderException); + + this.storageBrokerMock.Setup(broker => + broker.SelectReaderByIdAsync(readerId)) + .Throws(dbUpdateConcurrencyException); + + // when + ValueTask modifyReaderTask = + this.readerService.ModifyReaderAsync(someReader); + + ReaderDependencyValidationException actualReaderDependencyValidationException = + await Assert.ThrowsAsync(() => + modifyReaderTask.AsTask()); + + // then + actualReaderDependencyValidationException.Should() + .BeEquivalentTo(expectedReaderDependencyValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedReaderDependencyValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.SelectReaderByIdAsync(readerId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdateReaderAsync(someReader), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldThrowServiceExceptionOnModifyIfDatabaseUpdateErrorOccursAndLogItAsync() + { + // given + Reader randomReader = CreateRandomReader(); + Reader someReader = randomReader; + Guid readerId = someReader.ReaderId; + var serviceException = new Exception(); + + var failedReaderServiceException = + new FailedReaderServiceException(serviceException); + + var expectedReaderServiceException = + new ReaderServiceException(failedReaderServiceException); + + this.storageBrokerMock.Setup(broker => + broker.SelectReaderByIdAsync(readerId)) + .Throws(serviceException); + + // when + ValueTask modifyReaderTask = + this.readerService.ModifyReaderAsync(someReader); + + ReaderServiceException actualReaderServiceException = + await Assert.ThrowsAsync(() => + modifyReaderTask.AsTask()); + + // then + actualReaderServiceException.Should() + .BeEquivalentTo(expectedReaderServiceException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedReaderServiceException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.SelectReaderByIdAsync(readerId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdateReaderAsync(someReader), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/LibraryManagement.Api.Tests.Unit/Services/Foundations/Readers/ReaderServiceTests.Logic.Modify.cs b/LibraryManagement.Api.Tests.Unit/Services/Foundations/Readers/ReaderServiceTests.Logic.Modify.cs new file mode 100644 index 0000000..5f49556 --- /dev/null +++ b/LibraryManagement.Api.Tests.Unit/Services/Foundations/Readers/ReaderServiceTests.Logic.Modify.cs @@ -0,0 +1,54 @@ +//----------------------------------------------------------- +// Copyright (c) Coalition of Good-Hearted Engineers +// Free To Use To Build Reliable Library Management Solutions +//----------------------------------------------------------- + +using FluentAssertions; +using Force.DeepCloner; +using LibraryManagement.Api.Models.Foundations.Readers; +using Moq; + +namespace LibraryManagement.Api.Tests.Unit.Services.Foundations.Readers +{ + public partial class ReaderServiceTests + { + [Fact] + public async Task ShouldModifyReaderAsync() + { + // given + Reader randomReader = CreateRandomReader(); + Reader inputReader = randomReader; + Reader persistedReader = inputReader.DeepClone(); + Reader updatedReader = inputReader; + Reader expectedReader = updatedReader.DeepClone(); + Guid InputReaderId = inputReader.ReaderId; + + this.storageBrokerMock.Setup(broker => + broker.SelectReaderByIdAsync(InputReaderId)) + .ReturnsAsync(persistedReader); + + this.storageBrokerMock.Setup(broker => + broker.UpdateReaderAsync(inputReader)) + .ReturnsAsync(updatedReader); + + // when + Reader actualReader = + await this.readerService + .ModifyReaderAsync(inputReader); + + // then + actualReader.Should().BeEquivalentTo(expectedReader); + + this.storageBrokerMock.Verify(broker => + broker.SelectReaderByIdAsync(InputReaderId), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdateReaderAsync(inputReader), + Times.Once); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/LibraryManagement.Api.Tests.Unit/Services/Foundations/Readers/ReaderServiceTests.Validations.Modify.cs b/LibraryManagement.Api.Tests.Unit/Services/Foundations/Readers/ReaderServiceTests.Validations.Modify.cs new file mode 100644 index 0000000..11b3485 --- /dev/null +++ b/LibraryManagement.Api.Tests.Unit/Services/Foundations/Readers/ReaderServiceTests.Validations.Modify.cs @@ -0,0 +1,156 @@ +//----------------------------------------------------------- +// Copyright (c) Coalition of Good-Hearted Engineers +// Free To Use To Build Reliable Library Management Solutions +//----------------------------------------------------------- + +using FluentAssertions; +using LibraryManagement.Api.Models.Foundations.Readers; +using LibraryManagement.Api.Models.Foundations.Readers.Exceptions; +using Moq; + +namespace LibraryManagement.Api.Tests.Unit.Services.Foundations.Readers +{ + public partial class ReaderServiceTests + { + [Fact] + public async Task ShouldThrowValidationExceptionOnModifyIfReaderIsNullAndLogItAsync() + { + // given + Reader nullReader = null; + var nullReaderException = new NullReaderException(); + + var expectedReaderValidationException = + new ReaderValidationException(nullReaderException); + + // when + ValueTask modifyReaderTask = + this.readerService.ModifyReaderAsync(nullReader); + + ReaderValidationException actualReaderValidationException = + await Assert.ThrowsAsync(() => + modifyReaderTask.AsTask()); + + // then + actualReaderValidationException.Should() + .BeEquivalentTo(expectedReaderValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedReaderValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdateReaderAsync(It.IsAny()), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ShouldThrowValidationExceptionOnModifyIfReaderIsInvalidAndLogItAsync( + string invalidText) + { + // given + var invalidReader = new Reader + { + FirstName = invalidText + }; + + var invalidReaderException = new InvalidReaderException(); + + invalidReaderException.AddData( + key: nameof(Reader.ReaderId), + values: "Id is required"); + + invalidReaderException.AddData( + key: nameof(Reader.FirstName), + values: "Text is required"); + + invalidReaderException.AddData( + key: nameof(Reader.LastName), + values: "Text is required"); + + invalidReaderException.AddData( + key: nameof(Reader.DateOfBirth), + values: "Date is required"); + + var expectedReaderValidationException = + new ReaderValidationException(invalidReaderException); + + // when + ValueTask modifyReaderTask = + this.readerService.ModifyReaderAsync(invalidReader); + + ReaderValidationException actualReaderValidationException = + await Assert.ThrowsAsync(() => + modifyReaderTask.AsTask()); + + // then + actualReaderValidationException.Should() + .BeEquivalentTo(expectedReaderValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedReaderValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdateReaderAsync(It.IsAny()), + Times.Never); + + this.loggingBrokerMock.VerifyNoOtherCalls(); + this.storageBrokerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ShouldThrowValidationExceptionOnModifyIfReaderDoesNotExistAndLogItAsync() + { + // given + Reader randomReader = CreateRandomReader(); + Reader nonExistReader = randomReader; + Reader nullReader = null; + + var notFoundReaderException = + new NotFoundReaderException(nonExistReader.ReaderId); + + var expectedReaderValidationException = + new ReaderValidationException(notFoundReaderException); + + this.storageBrokerMock.Setup(broker => + broker.SelectReaderByIdAsync(nonExistReader.ReaderId)) + .ReturnsAsync(nullReader); + + // when + ValueTask modifyReaderTask = + this.readerService.ModifyReaderAsync(nonExistReader); + + ReaderValidationException actualReaderValidationException = + await Assert.ThrowsAsync(() => + modifyReaderTask.AsTask()); + + // then + actualReaderValidationException.Should() + .BeEquivalentTo(expectedReaderValidationException); + + this.storageBrokerMock.Verify(broker => + broker.SelectReaderByIdAsync(nonExistReader.ReaderId), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedReaderValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.UpdateReaderAsync(nonExistReader), + Times.Never); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/LibraryManagement.Api/Models/Foundations/Readers/Exceptions/LockedReaderException.cs b/LibraryManagement.Api/Models/Foundations/Readers/Exceptions/LockedReaderException.cs new file mode 100644 index 0000000..3c67c4a --- /dev/null +++ b/LibraryManagement.Api/Models/Foundations/Readers/Exceptions/LockedReaderException.cs @@ -0,0 +1,17 @@ +//----------------------------------------------------------- +// Copyright (c) Coalition of Good-Hearted Engineers +// Free To Use To Build Reliable Library Management Solutions +//----------------------------------------------------------- + +using Xeptions; + +namespace LibraryManagement.Api.Models.Foundations.Readers.Exceptions +{ + public class LockedReaderException : Xeption + { + public LockedReaderException(Exception innerException) + : base(message: "Reader is locked, please try again later.", + innerException) + { } + } +} diff --git a/LibraryManagement.Api/Services/Foundations/Readers/IReaderService.cs b/LibraryManagement.Api/Services/Foundations/Readers/IReaderService.cs index 44822b2..0c1fea4 100644 --- a/LibraryManagement.Api/Services/Foundations/Readers/IReaderService.cs +++ b/LibraryManagement.Api/Services/Foundations/Readers/IReaderService.cs @@ -12,5 +12,6 @@ public interface IReaderService ValueTask AddReaderAsync(Reader reader); IQueryable RetrieveAllReaders(); ValueTask RetrieveReaderByIdAsync(Guid readerId); + ValueTask ModifyReaderAsync(Reader reader); } } diff --git a/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.Exceptions.cs b/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.Exceptions.cs index c046ebc..40768cf 100644 --- a/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.Exceptions.cs +++ b/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.Exceptions.cs @@ -7,6 +7,7 @@ using LibraryManagement.Api.Models.Foundations.Readers; using LibraryManagement.Api.Models.Foundations.Readers.Exceptions; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; using Xeptions; namespace LibraryManagement.Api.Services.Foundations.Readers @@ -34,6 +35,20 @@ private async ValueTask TryCatch(ReturningReaderFunction returningReader { throw CreateAndLogValidationException(notFoundReaderException); } + catch (DbUpdateConcurrencyException dbUpdateConcurrencyException) + { + var lockedReaderException = + new LockedReaderException(dbUpdateConcurrencyException); + + throw CreateAndLogDependencyValidationException(lockedReaderException); + } + catch (DbUpdateException dbUpdateException) + { + var failedReaderStorageException = + new FailedReaderStorageException(dbUpdateException); + + throw CreateAndLogDependencyException(failedReaderStorageException); + } catch (SqlException sqlException) { var failedReaderStorageException = @@ -115,5 +130,13 @@ private ReaderServiceException CreateAndLogServiceException(Xeption exception) return readerServiceException; } + + private ReaderDependencyException CreateAndLogDependencyException(Xeption exception) + { + var readerDependencyException = new ReaderDependencyException(exception); + this.loggingBroker.LogError(readerDependencyException); + + return readerDependencyException; + } } } diff --git a/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.Validations.cs b/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.Validations.cs index 613cb5e..4de8d2a 100644 --- a/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.Validations.cs +++ b/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.Validations.cs @@ -21,7 +21,29 @@ private void ValidateReaderOnAdd(Reader reader) (Rule: IsInvalid(reader.DateOfBirth), Parameter: nameof(Reader.DateOfBirth))); } - private void ValidateReaderNotNull(Reader reader) + private static void ValidateReaderOnModify(Reader reader) + { + ValidateReaderNotNull(reader); + + Validate( + (Rule: IsInvalid(reader.ReaderId), Parameter: nameof(Reader.ReaderId)), + (Rule: IsInvalid(reader.FirstName), Parameter: nameof(Reader.FirstName)), + (Rule: IsInvalid(reader.LastName), Parameter: nameof(Reader.LastName)), + (Rule: IsInvalid(reader.DateOfBirth), Parameter: nameof(Reader.DateOfBirth))); + } + + private static void ValidateAgainstStorageReaderOnModify(Reader reader, Reader storageReader) + { + ValidateStorageReader(storageReader, reader.ReaderId); + + Validate( + (Rule: IsInvalid(reader.ReaderId), Parameter: nameof(Reader.ReaderId)), + (Rule: IsInvalid(reader.FirstName), Parameter: nameof(Reader.FirstName)), + (Rule: IsInvalid(reader.LastName), Parameter: nameof(Reader.LastName)), + (Rule: IsInvalid(reader.DateOfBirth), Parameter: nameof(Reader.DateOfBirth))); + } + + private static void ValidateReaderNotNull(Reader reader) { if (reader is null) { diff --git a/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.cs b/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.cs index a6a7753..12769f2 100644 --- a/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.cs +++ b/LibraryManagement.Api/Services/Foundations/Readers/ReaderService.cs @@ -45,5 +45,18 @@ public ValueTask RetrieveReaderByIdAsync(Guid readerId) => return maybeReader; }); + + public ValueTask ModifyReaderAsync(Reader reader) => + TryCatch(async () => + { + ValidateReaderOnModify(reader); + + Reader maybeReader = + await this.storageBroker.SelectReaderByIdAsync(reader.ReaderId); + + ValidateAgainstStorageReaderOnModify(reader, maybeReader); + + return await this.storageBroker.UpdateReaderAsync(reader); + }); } }