diff --git a/MailContainerTest.Tests/Builders/MailContainerBuilder.cs b/MailContainerTest.Tests/Builders/MailContainerBuilder.cs new file mode 100644 index 0000000..1602a48 --- /dev/null +++ b/MailContainerTest.Tests/Builders/MailContainerBuilder.cs @@ -0,0 +1,53 @@ +using MailContainerTest.Types; + +namespace MailContainerTest.Tests.Builders; + +public sealed class MailContainerBuilder +{ + private MailContainerNumber _mailContainerNumber; + private MailContainerCapacity _capacity; + private MailContainerStatus _status; + private AllowedMailType _allowedMailType; + + public MailContainerBuilder WithMailContainerNumber(MailContainerNumber mailContainerNumber) + { + _mailContainerNumber = mailContainerNumber; + + return this; + } + + public MailContainerBuilder WithCapacity(MailContainerCapacity capacity) + { + _capacity = capacity; + + return this; + } + + public MailContainerBuilder WithStatus(MailContainerStatus status) + { + _status = status; + + return this; + } + + public MailContainerBuilder WithAllowedMailType(AllowedMailType allowedMailType) + { + _allowedMailType = allowedMailType; + + return this; + } + + public MailContainer Build() + { + var mailContainer = new MailContainer + { + MailContainerNumber = _mailContainerNumber, + Status = _status, + AllowedMailType = _allowedMailType + }; + + mailContainer.IncreaseCapacity(_capacity); + + return mailContainer; + } +} \ No newline at end of file diff --git a/MailContainerTest.Tests/Builders/MakeMailTransferRequestBuilder.cs b/MailContainerTest.Tests/Builders/MakeMailTransferRequestBuilder.cs new file mode 100644 index 0000000..545ee60 --- /dev/null +++ b/MailContainerTest.Tests/Builders/MakeMailTransferRequestBuilder.cs @@ -0,0 +1,48 @@ +using MailContainerTest.Types; + +namespace MailContainerTest.Tests.Builders; + +public sealed class MakeMailTransferRequestBuilder +{ + private MakeMailTransferRequest _makeMailTransferRequest = new(); + + public MakeMailTransferRequestBuilder WithSourceMailContainerNumber(MailContainerNumber sourceMailContainerNumber) + { + _makeMailTransferRequest = _makeMailTransferRequest with { SourceMailContainerNumber = sourceMailContainerNumber }; + + return this; + } + + public MakeMailTransferRequestBuilder WithDestinationMailContainerNumber(MailContainerNumber destinationMailContainerNumber) + { + _makeMailTransferRequest = _makeMailTransferRequest with { DestinationMailContainerNumber = destinationMailContainerNumber }; + + return this; + } + + public MakeMailTransferRequestBuilder WithNumberOfMailItems(int numberOfMailItems) + { + _makeMailTransferRequest = _makeMailTransferRequest with { NumberOfMailItems = numberOfMailItems }; + + return this; + } + + public MakeMailTransferRequestBuilder WithTransferDate(DateTime transferDate) + { + _makeMailTransferRequest = _makeMailTransferRequest with { TransferDate = transferDate }; + + return this; + } + + public MakeMailTransferRequestBuilder WithMailType(MailType mailType) + { + _makeMailTransferRequest = _makeMailTransferRequest with { MailType = mailType }; + + return this; + } + + public MakeMailTransferRequest Build() + { + return _makeMailTransferRequest; + } +} \ No newline at end of file diff --git a/MailContainerTest.Tests/LargeLetterStrategyTests.cs b/MailContainerTest.Tests/LargeLetterStrategyTests.cs new file mode 100644 index 0000000..18fa3b3 --- /dev/null +++ b/MailContainerTest.Tests/LargeLetterStrategyTests.cs @@ -0,0 +1,164 @@ +using FluentAssertions; +using MailContainerTest.Abstractions; +using MailContainerTest.Strategies; +using MailContainerTest.Tests.Builders; +using MailContainerTest.Types; +using NSubstitute; +using NSubstitute.ReceivedExtensions; +using Xunit; + +namespace MailContainerTest.Tests; + +public sealed class LargeLetterStrategyTests +{ + private readonly IAllowedMailTypeBehaviour _allowedMailTypeBehaviour = Substitute.For(); + private readonly IOperationalStatusBehaviour _operationalStatusBehaviour = Substitute.For(); + private readonly ICapacityBehaviour _capacityBehaviour = Substitute.For(); + + private readonly LargeLetterStrategy _sut; + + public LargeLetterStrategyTests() + { + _sut = new LargeLetterStrategy(_allowedMailTypeBehaviour, _operationalStatusBehaviour, _capacityBehaviour); + } + + [Fact] + public void IsSuccess_ShouldReturnTrue_WhenAllowedMailTypeIsEqualAndOperationalAndWithinCapacity() + { + // Arrange + var sourceContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.LargeLetter) + .Build(); + + var destContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.SmallParcel) + .Build(); + + var request = new MakeMailTransferRequestBuilder() + .WithMailType(MailType.LargeLetter) + .Build(); + + _allowedMailTypeBehaviour.IsAllowedMailType(Arg.Any(), Arg.Any()).Returns(true); + _operationalStatusBehaviour.IsOperationalStatus(Arg.Any(), Arg.Any()).Returns(true); + _capacityBehaviour.IsWithinCapacity(Arg.Any(), Arg.Any()).Returns(true); + + // Act + var result = _sut.IsSuccess(sourceContainer, destContainer, request); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void IsSuccess_ShouldReturnFalse_WhenAllowedMailTypeNotEqual() + { + // Arrange + var sourceContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.LargeLetter) + .Build(); + + var destContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.SmallParcel) + .Build(); + + var request = new MakeMailTransferRequestBuilder() + .WithMailType(MailType.LargeLetter) + .Build(); + + // Act + var result = _sut.IsSuccess(sourceContainer, destContainer, request); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void IsSuccess_ShouldReturnFalse_WhenOneOfMailContainerStatusIsNotOperational() + { + // Arrange + var sourceContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.LargeLetter) + .WithStatus(MailContainerStatus.Operational) + .Build(); + + var destContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.LargeLetter) + .WithStatus(MailContainerStatus.OutOfService) + .Build(); + + var request = new MakeMailTransferRequestBuilder() + .WithMailType(MailType.LargeLetter) + .Build(); + + // Act + var result = _sut.IsSuccess(sourceContainer, destContainer, request); + + // Assert + result.Should().BeFalse(); + } + + // [Theory] + // [MemberData(nameof(FailingTestData))] + // public void IsSuccess_ShouldReturnFalse_WhenAllowedMailTypeIsNotEqualAndNotOperationalAndNotWithinCapacity( + // MailContainer sourceContainer, + // MailContainer destContainer, + // MakeMailTransferRequest request) + // { + // // Arrange + // + // + // // Act + // var result = _sut.IsSuccess(sourceContainer, destContainer, request); + // + // // Assert + // result.Should().BeFalse(); + // } + // + // public static IEnumerable FailingTestData => + // new List + // { + // new object[] + // { + // new MailContainerBuilder() + // .WithAllowedMailType(AllowedMailType.LargeLetter) + // .Build(), + // new MailContainerBuilder() + // .WithAllowedMailType(AllowedMailType.SmallParcel) + // .Build(), + // new MakeMailTransferRequestBuilder() + // .WithMailType(MailType.LargeLetter) + // .Build() + // }, + // new object[] + // { + // new MailContainerBuilder() + // .WithAllowedMailType(AllowedMailType.LargeLetter) + // .WithStatus(MailContainerStatus.Operational) + // .Build(), + // new MailContainerBuilder() + // .WithAllowedMailType(AllowedMailType.LargeLetter) + // .WithStatus(MailContainerStatus.OutOfService) + // .Build(), + // new MakeMailTransferRequestBuilder() + // .WithMailType(MailType.LargeLetter) + // .Build() + // }, + // new object[] + // { + // new MailContainerBuilder() + // .WithAllowedMailType(AllowedMailType.LargeLetter) + // .WithStatus(MailContainerStatus.Operational) + // .WithCapacity(100) + // .Build(), + // new MailContainerBuilder() + // .WithAllowedMailType(AllowedMailType.LargeLetter) + // .WithStatus(MailContainerStatus.Operational) + // .WithCapacity(100) + // .Build(), + // new MakeMailTransferRequestBuilder() + // .WithMailType(MailType.LargeLetter) + // .WithNumberOfMailItems(200) + // .Build() + // } + // }; +} \ No newline at end of file diff --git a/MailContainerTest.Tests/MailContainerTest.Tests.csproj b/MailContainerTest.Tests/MailContainerTest.Tests.csproj index 132c02c..5c391bf 100644 --- a/MailContainerTest.Tests/MailContainerTest.Tests.csproj +++ b/MailContainerTest.Tests/MailContainerTest.Tests.csproj @@ -6,4 +6,25 @@ enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/MailContainerTest.Tests/MailTransferServiceTests.cs b/MailContainerTest.Tests/MailTransferServiceTests.cs new file mode 100644 index 0000000..5ba9ae5 --- /dev/null +++ b/MailContainerTest.Tests/MailTransferServiceTests.cs @@ -0,0 +1,152 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using MailContainerTest.Abstractions; +using MailContainerTest.Data; +using MailContainerTest.Services; +using MailContainerTest.Tests.Builders; +using MailContainerTest.Types; +using NSubstitute; +using Xunit; + +namespace MailContainerTest.Tests; + +public sealed class MailTransferServiceTests +{ + private readonly IMailContainerDataStoreFactory _mailContainerDataStoreFactory = Substitute.For(); + private readonly IMailContainerDataStore _mailContainerDataStore = Substitute.For(); + private readonly IMailTransferStrategyFactory _mailTransferStrategyFactory = Substitute.For(); + private readonly IUnitOfWork _unitOfWork = Substitute.For(); + private readonly ILoggerAdapter _loggerAdapter = Substitute.For>(); + + private readonly MailTransferService _sut; + + public MailTransferServiceTests() + { + _sut = new MailTransferService(_mailContainerDataStoreFactory, _mailTransferStrategyFactory, _unitOfWork, _loggerAdapter); + } + + [Fact] + public void MakeMailTransfer_ShouldReturnFalse_WhenStrategyNotSuccessful() + { + // Arrange + var request = new MakeMailTransferRequestBuilder() + .WithSourceMailContainerNumber("1") + .WithDestinationMailContainerNumber("2") + .WithMailType(MailType.LargeLetter) + .WithNumberOfMailItems(1) + .Build(); + + _mailContainerDataStoreFactory.CreateMailContainerDataStore() + .Returns(new MailContainerDataStore()); + + _mailTransferStrategyFactory.CreateMakeMailTransferStrategy(Arg.Any()) + .IsSuccess(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(false); + + // Act + var result = _sut.MakeMailTransfer(request); + + // Assert + using (new AssertionScope()) + { + _unitOfWork.DidNotReceive().Commit(); + result.Success.Should().BeFalse(); + } + } + + [Fact] + public void MakeMailTransfer_ShouldReturnTrue_WhenStrategySuccessful() + { + // Arrange + var request = new MakeMailTransferRequestBuilder() + .WithSourceMailContainerNumber("1") + .WithDestinationMailContainerNumber("2") + .WithMailType(MailType.LargeLetter) + .WithNumberOfMailItems(1) + .Build(); + + var sourceContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.LargeLetter) + .WithMailContainerNumber("1") + .WithStatus(MailContainerStatus.Operational) + .WithCapacity(100) + .Build(); + + var destContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.LargeLetter) + .WithMailContainerNumber("2") + .WithStatus(MailContainerStatus.Operational) + .WithCapacity(100) + .Build(); + + _mailContainerDataStoreFactory.CreateMailContainerDataStore() + .Returns(_mailContainerDataStore); + + _mailContainerDataStore.GetMailContainer(Arg.Any()) + .Returns(sourceContainer, destContainer); + + _mailTransferStrategyFactory.CreateMakeMailTransferStrategy(Arg.Any()) + .IsSuccess(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + + // Act + var result = _sut.MakeMailTransfer(request); + + // Assert + using (new AssertionScope()) + { + _unitOfWork.Received(1).Commit(); + result.Success.Should().BeTrue(); + } + } + + [Fact] + public void MakeMailTransfer_ShouldReturnFalse_WhenCommitNotSuccessful() + { + // Arrange + var request = new MakeMailTransferRequestBuilder() + .WithSourceMailContainerNumber("1") + .WithDestinationMailContainerNumber("2") + .WithMailType(MailType.LargeLetter) + .WithNumberOfMailItems(1) + .Build(); + + var sourceContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.LargeLetter) + .WithMailContainerNumber("1") + .WithStatus(MailContainerStatus.Operational) + .WithCapacity(100) + .Build(); + + var destContainer = new MailContainerBuilder() + .WithAllowedMailType(AllowedMailType.LargeLetter) + .WithMailContainerNumber("2") + .WithStatus(MailContainerStatus.Operational) + .WithCapacity(100) + .Build(); + + _mailContainerDataStoreFactory.CreateMailContainerDataStore() + .Returns(_mailContainerDataStore); + + _mailContainerDataStore.GetMailContainer(Arg.Any()) + .Returns(sourceContainer, destContainer); + + _mailTransferStrategyFactory.CreateMakeMailTransferStrategy(Arg.Any()) + .IsSuccess(Arg.Is(sourceContainer), Arg.Is(destContainer), Arg.Any()) + .Returns(true); + + _unitOfWork.When(static x => x.Commit()).Throw(); + + // Act + var result = _sut.MakeMailTransfer(request); + + // Assert + using (new AssertionScope()) + { + _unitOfWork.Received(1).Commit(); + _unitOfWork.Received(1).Rollback(); + _loggerAdapter.Received(1).LogError(Arg.Any(), "Error saving changes to containers"); + result.Success.Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/MailContainerTest.Tests/MailTransferStrategyFactoryTests.cs b/MailContainerTest.Tests/MailTransferStrategyFactoryTests.cs new file mode 100644 index 0000000..5cbc98f --- /dev/null +++ b/MailContainerTest.Tests/MailTransferStrategyFactoryTests.cs @@ -0,0 +1,67 @@ +using FluentAssertions; +using MailContainerTest.Abstractions; +using MailContainerTest.Factories; +using MailContainerTest.Strategies; +using MailContainerTest.Strategies.Behaviours; +using MailContainerTest.Types; +using Xunit; + +namespace MailContainerTest.Tests; + +public sealed class MailTransferStrategyFactoryTests +{ + private readonly MailTransferStrategyFactory _sut; + + public MailTransferStrategyFactoryTests() + { + _sut = new MailTransferStrategyFactory(new AllowedMailTypeBehaviour(), new OperationalStatusBehaviour(), new CapacityBehaviour()); + } + + [Fact] + public void CreateMakeMailTransferStrategy_ShouldReturnStandardLetter_WhenMailTypeIsStandardLetter() + { + // Arrange + + // Act + var result = _sut.CreateMakeMailTransferStrategy(MailType.StandardLetter); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public void CreateMakeMailTransferStrategy_ShouldReturnLargeLetterStrategy_WhenMailTypeIsLargeLetter() + { + // Arrange + + // Act + var result = _sut.CreateMakeMailTransferStrategy(MailType.LargeLetter); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public void CreateMakeMailTransferStrategy_ShouldReturnSmallParcelStrategy_WhenMailTypeIsSmallParcel() + { + // Arrange + + // Act + var result = _sut.CreateMakeMailTransferStrategy(MailType.SmallParcel); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public void CreateMakeMailTransferStrategy_ShouldThrowArgumentOutOfRangeException_WhenMailTypeIsNotInEnumRange() + { + // Arrange + + // Act + Action result = () => _sut.CreateMakeMailTransferStrategy((MailType) (-1)); + + // Assert + result.Should().Throw().WithMessage("Mail type is not in enum range*"); + } +} \ No newline at end of file diff --git a/MailContainerTest.Tests/obj/Debug/net6.0/MailContainerTest.Tests.GeneratedMSBuildEditorConfig.editorconfig b/MailContainerTest.Tests/obj/Debug/net6.0/MailContainerTest.Tests.GeneratedMSBuildEditorConfig.editorconfig index ed46c72..ef93c82 100644 --- a/MailContainerTest.Tests/obj/Debug/net6.0/MailContainerTest.Tests.GeneratedMSBuildEditorConfig.editorconfig +++ b/MailContainerTest.Tests/obj/Debug/net6.0/MailContainerTest.Tests.GeneratedMSBuildEditorConfig.editorconfig @@ -5,6 +5,7 @@ build_property.UsingMicrosoftNETSdkWeb = build_property.ProjectTypeGuids = build_property.InvariantGlobalization = build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = build_property._SupportedPlatformList = Linux,macOS,Windows build_property.RootNamespace = MailContainerTest.Tests -build_property.ProjectDir = c:\repos\test\MailContainerTest\MailContainerTest\MailContainerTest.Tests\ +build_property.ProjectDir = C:\Users\ricar\source\github\MailContainerTest\MailContainerTest.Tests\ diff --git a/MailContainerTest/Abstractions/IAllowedMailTypeBehaviour.cs b/MailContainerTest/Abstractions/IAllowedMailTypeBehaviour.cs new file mode 100644 index 0000000..d67f16a --- /dev/null +++ b/MailContainerTest/Abstractions/IAllowedMailTypeBehaviour.cs @@ -0,0 +1,8 @@ +using MailContainerTest.Types; + +namespace MailContainerTest.Abstractions; + +public interface IAllowedMailTypeBehaviour +{ + public bool IsAllowedMailType(MailContainer sourceContainer, MailContainer destContainer); +} \ No newline at end of file diff --git a/MailContainerTest/Abstractions/ICapacityBehaviour.cs b/MailContainerTest/Abstractions/ICapacityBehaviour.cs new file mode 100644 index 0000000..f37dbea --- /dev/null +++ b/MailContainerTest/Abstractions/ICapacityBehaviour.cs @@ -0,0 +1,8 @@ +using MailContainerTest.Types; + +namespace MailContainerTest.Abstractions; + +public interface ICapacityBehaviour +{ + bool IsWithinCapacity(MailContainer sourceContainer, MakeMailTransferRequest request); +} \ No newline at end of file diff --git a/MailContainerTest/Abstractions/ILoggerAdapter.cs b/MailContainerTest/Abstractions/ILoggerAdapter.cs new file mode 100644 index 0000000..8ac1506 --- /dev/null +++ b/MailContainerTest/Abstractions/ILoggerAdapter.cs @@ -0,0 +1,6 @@ +namespace MailContainerTest.Abstractions; + +public interface ILoggerAdapter +{ + void LogError(Exception ex,string message); +} \ No newline at end of file diff --git a/MailContainerTest/Abstractions/IMailContainerDataStore.cs b/MailContainerTest/Abstractions/IMailContainerDataStore.cs new file mode 100644 index 0000000..af2ff73 --- /dev/null +++ b/MailContainerTest/Abstractions/IMailContainerDataStore.cs @@ -0,0 +1,10 @@ +using MailContainerTest.Types; + +namespace MailContainerTest.Abstractions; + +public interface IMailContainerDataStore +{ + MailContainer GetMailContainer(in MailContainerNumber mailContainerNumber); + + void UpdateMailContainer(MailContainer mailContainer); +} \ No newline at end of file diff --git a/MailContainerTest/Abstractions/IMailContainerDataStoreFactory.cs b/MailContainerTest/Abstractions/IMailContainerDataStoreFactory.cs new file mode 100644 index 0000000..78c6914 --- /dev/null +++ b/MailContainerTest/Abstractions/IMailContainerDataStoreFactory.cs @@ -0,0 +1,8 @@ +using MailContainerTest.Types; + +namespace MailContainerTest.Abstractions; + +public interface IMailContainerDataStoreFactory +{ + IMailContainerDataStore CreateMailContainerDataStore(); +} \ No newline at end of file diff --git a/MailContainerTest/Services/IMailTransferService.cs b/MailContainerTest/Abstractions/IMailTransferService.cs similarity index 80% rename from MailContainerTest/Services/IMailTransferService.cs rename to MailContainerTest/Abstractions/IMailTransferService.cs index fd1b275..1607465 100644 --- a/MailContainerTest/Services/IMailTransferService.cs +++ b/MailContainerTest/Abstractions/IMailTransferService.cs @@ -1,6 +1,6 @@ using MailContainerTest.Types; -namespace MailContainerTest.Services +namespace MailContainerTest.Abstractions { public interface IMailTransferService { diff --git a/MailContainerTest/Abstractions/IMailTransferStrategy.cs b/MailContainerTest/Abstractions/IMailTransferStrategy.cs new file mode 100644 index 0000000..eb357f2 --- /dev/null +++ b/MailContainerTest/Abstractions/IMailTransferStrategy.cs @@ -0,0 +1,8 @@ +using MailContainerTest.Types; + +namespace MailContainerTest.Abstractions; + +public interface IMailTransferStrategy +{ + bool IsSuccess(MailContainer sourceContainer, MailContainer destContainer, MakeMailTransferRequest request); +} \ No newline at end of file diff --git a/MailContainerTest/Abstractions/IMailTransferStrategyFactory.cs b/MailContainerTest/Abstractions/IMailTransferStrategyFactory.cs new file mode 100644 index 0000000..a16de75 --- /dev/null +++ b/MailContainerTest/Abstractions/IMailTransferStrategyFactory.cs @@ -0,0 +1,8 @@ +using MailContainerTest.Types; + +namespace MailContainerTest.Abstractions; + +public interface IMailTransferStrategyFactory +{ + IMailTransferStrategy CreateMakeMailTransferStrategy(MailType mailType); +} \ No newline at end of file diff --git a/MailContainerTest/Abstractions/IOperationalStatusBehaviour.cs b/MailContainerTest/Abstractions/IOperationalStatusBehaviour.cs new file mode 100644 index 0000000..d664858 --- /dev/null +++ b/MailContainerTest/Abstractions/IOperationalStatusBehaviour.cs @@ -0,0 +1,8 @@ +using MailContainerTest.Types; + +namespace MailContainerTest.Abstractions; + +public interface IOperationalStatusBehaviour +{ + bool IsOperationalStatus(MailContainer sourceContainer, MailContainer destContainer); +} \ No newline at end of file diff --git a/MailContainerTest/Abstractions/IUnitOfWork.cs b/MailContainerTest/Abstractions/IUnitOfWork.cs new file mode 100644 index 0000000..052b893 --- /dev/null +++ b/MailContainerTest/Abstractions/IUnitOfWork.cs @@ -0,0 +1,8 @@ +namespace MailContainerTest.Abstractions; + +public interface IUnitOfWork +{ + void Commit(); + + void Rollback(); +} \ No newline at end of file diff --git a/MailContainerTest/Adapters/LoggerAdapter.cs b/MailContainerTest/Adapters/LoggerAdapter.cs new file mode 100644 index 0000000..f960d1a --- /dev/null +++ b/MailContainerTest/Adapters/LoggerAdapter.cs @@ -0,0 +1,19 @@ +using MailContainerTest.Abstractions; +using Microsoft.Extensions.Logging; + +namespace MailContainerTest.Adapters; + +public sealed class LoggerAdapter : ILoggerAdapter where T : class +{ + private readonly ILogger _logger; + + public LoggerAdapter(ILogger logger) + { + _logger = logger; + } + + public void LogError(Exception ex, string message) + { + _logger.LogError(message, ex); + } +} \ No newline at end of file diff --git a/MailContainerTest/Data/BackupMailContainerDataStore.cs b/MailContainerTest/Data/BackupMailContainerDataStore.cs index 614b1dc..4cc69b6 100644 --- a/MailContainerTest/Data/BackupMailContainerDataStore.cs +++ b/MailContainerTest/Data/BackupMailContainerDataStore.cs @@ -1,10 +1,13 @@ -using MailContainerTest.Types; +using MailContainerTest.Abstractions; +using MailContainerTest.Types; namespace MailContainerTest.Data { - public class BackupMailContainerDataStore + public sealed class BackupMailContainerDataStore : IMailContainerDataStore { - public MailContainer GetMailContainer(string mailContainerNumber) + public static string DataStoreType => "Backup"; + + public MailContainer GetMailContainer(in MailContainerNumber mailContainerNumber) { // Access the database and return the retrieved mail container. Implementation not required for this exercise. return new MailContainer(); diff --git a/MailContainerTest/Data/MailContainerDataStore.cs b/MailContainerTest/Data/MailContainerDataStore.cs index c57cf54..ff68aca 100644 --- a/MailContainerTest/Data/MailContainerDataStore.cs +++ b/MailContainerTest/Data/MailContainerDataStore.cs @@ -1,10 +1,11 @@ -using MailContainerTest.Types; +using MailContainerTest.Abstractions; +using MailContainerTest.Types; namespace MailContainerTest.Data { - public class MailContainerDataStore + public sealed class MailContainerDataStore : IMailContainerDataStore { - public MailContainer GetMailContainer(string mailContainerNumber) + public MailContainer GetMailContainer(in MailContainerNumber mailContainerNumber) { // Access the database and return the retrieved mail container. Implementation not required for this exercise. return new MailContainer(); diff --git a/MailContainerTest/Factories/MailContainerDataStoreFactory.cs b/MailContainerTest/Factories/MailContainerDataStoreFactory.cs new file mode 100644 index 0000000..0b4d49e --- /dev/null +++ b/MailContainerTest/Factories/MailContainerDataStoreFactory.cs @@ -0,0 +1,27 @@ +using MailContainerTest.Abstractions; +using MailContainerTest.Data; +using MailContainerTest.Options; +using MailContainerTest.Types; +using Microsoft.Extensions.Options; + +namespace MailContainerTest.Factories; + +public sealed class MailContainerDataStoreFactory : IMailContainerDataStoreFactory +{ + private readonly IOptionsSnapshot _options; + + public MailContainerDataStoreFactory(IOptionsSnapshot options) + { + _options = options; + } + + public IMailContainerDataStore CreateMailContainerDataStore() + { + if (_options.Value.Equals(BackupMailContainerDataStore.DataStoreType)) + { + return new BackupMailContainerDataStore(); + } + + return new MailContainerDataStore(); + } +} \ No newline at end of file diff --git a/MailContainerTest/Factories/MailTransferStrategyFactory.cs b/MailContainerTest/Factories/MailTransferStrategyFactory.cs new file mode 100644 index 0000000..100da14 --- /dev/null +++ b/MailContainerTest/Factories/MailTransferStrategyFactory.cs @@ -0,0 +1,32 @@ +using MailContainerTest.Abstractions; +using MailContainerTest.Strategies; +using MailContainerTest.Types; + +namespace MailContainerTest.Factories; + +public sealed class MailTransferStrategyFactory : IMailTransferStrategyFactory +{ + private readonly IAllowedMailTypeBehaviour _allowedMailTypeBehaviour; + private readonly IOperationalStatusBehaviour _operationalStatusBehaviour; + private readonly ICapacityBehaviour _capacityBehaviour; + + public MailTransferStrategyFactory(IAllowedMailTypeBehaviour allowedMailTypeBehaviour, + IOperationalStatusBehaviour operationalStatusBehaviour, + ICapacityBehaviour capacityBehaviour) + { + _allowedMailTypeBehaviour = allowedMailTypeBehaviour; + _operationalStatusBehaviour = operationalStatusBehaviour; + _capacityBehaviour = capacityBehaviour; + } + + public IMailTransferStrategy CreateMakeMailTransferStrategy(MailType mailType) + { + return mailType switch + { + MailType.StandardLetter => new StandardLetterStrategy(_allowedMailTypeBehaviour), + MailType.LargeLetter => new LargeLetterStrategy(_allowedMailTypeBehaviour, _operationalStatusBehaviour, _capacityBehaviour), + MailType.SmallParcel => new SmallParcelStrategy(_allowedMailTypeBehaviour, _operationalStatusBehaviour), + _ => throw new ArgumentOutOfRangeException(nameof(mailType), mailType, "Mail type is not in enum range") + }; + } +} \ No newline at end of file diff --git a/MailContainerTest/MailContainerTest.csproj b/MailContainerTest/MailContainerTest.csproj index 03f4d06..f5de6b3 100644 --- a/MailContainerTest/MailContainerTest.csproj +++ b/MailContainerTest/MailContainerTest.csproj @@ -8,6 +8,8 @@ + + diff --git a/MailContainerTest/Options/MailContainerDataStoreOptions.cs b/MailContainerTest/Options/MailContainerDataStoreOptions.cs new file mode 100644 index 0000000..7fd4c65 --- /dev/null +++ b/MailContainerTest/Options/MailContainerDataStoreOptions.cs @@ -0,0 +1,6 @@ +namespace MailContainerTest.Options; + +public sealed class MailContainerDataStoreOptions +{ + public string? DataStoreType { get; set; } +} \ No newline at end of file diff --git a/MailContainerTest/Services/MailTransferService.cs b/MailContainerTest/Services/MailTransferService.cs index a124501..460314b 100644 --- a/MailContainerTest/Services/MailTransferService.cs +++ b/MailContainerTest/Services/MailTransferService.cs @@ -1,93 +1,70 @@ using MailContainerTest.Data; using MailContainerTest.Types; using System.Configuration; +using MailContainerTest.Abstractions; +using Microsoft.Extensions.Logging; namespace MailContainerTest.Services { - public class MailTransferService : IMailTransferService + public sealed class MailTransferService : IMailTransferService { + private readonly IMailContainerDataStoreFactory _mailContainerDataStoreFactory; + private readonly IMailTransferStrategyFactory _mailTransferStrategyFactory; + private readonly IUnitOfWork _unitOfWork; + private readonly ILoggerAdapter _loggerAdapter; + + public MailTransferService(IMailContainerDataStoreFactory mailContainerDataStoreFactory, + IMailTransferStrategyFactory mailTransferStrategyFactory, + IUnitOfWork unitOfWork, + ILoggerAdapter loggerAdapter) + { + _mailContainerDataStoreFactory = mailContainerDataStoreFactory; + _mailTransferStrategyFactory = mailTransferStrategyFactory; + _unitOfWork = unitOfWork; + _loggerAdapter = loggerAdapter; + } + public MakeMailTransferResult MakeMailTransfer(MakeMailTransferRequest request) { - var dataStoreType = ConfigurationManager.AppSettings["DataStoreType"]; + var containerDataStore = _mailContainerDataStoreFactory.CreateMailContainerDataStore(); - MailContainer mailContainer = null; + var sourceMailContainer = containerDataStore.GetMailContainer(request.SourceMailContainerNumber); + var destMailContainer = containerDataStore.GetMailContainer(request.DestinationMailContainerNumber); - if (dataStoreType == "Backup") - { - var mailContainerDataStore = new BackupMailContainerDataStore(); - mailContainer = mailContainerDataStore.GetMailContainer(request.SourceMailContainerNumber); + var mailTransfer = new MakeMailTransferResult + { + Success = _mailTransferStrategyFactory.CreateMakeMailTransferStrategy(request.MailType) + .IsSuccess(sourceMailContainer, destMailContainer, request) + }; - } else + if (mailTransfer.Success) { - var mailContainerDataStore = new MailContainerDataStore(); - mailContainer = mailContainerDataStore.GetMailContainer(request.SourceMailContainerNumber); + ApplyMailContainerChanges(request, sourceMailContainer, destMailContainer, containerDataStore, mailTransfer); } - var result = new MakeMailTransferResult(); + return mailTransfer; + } - switch (request.MailType) + private void ApplyMailContainerChanges(MakeMailTransferRequest request, MailContainer sourceMailContainer, MailContainer destMailContainer, IMailContainerDataStore containerDataStore, MakeMailTransferResult result) + { + try { - case MailType.StandardLetter: - if (mailContainer == null) - { - result.Success = false; - } - else if (!mailContainer.AllowedMailType.HasFlag(AllowedMailType.StandardLetter)) - { - result.Success = false; - } - break; - - case MailType.LargeLetter: - if (mailContainer == null) - { - result.Success = false; - } - else if (!mailContainer.AllowedMailType.HasFlag(AllowedMailType.LargeLetter)) - { - result.Success = false; - } - else if (mailContainer.Capacity < request.NumberOfMailItems) - { - result.Success = false; - } - break; + sourceMailContainer.DecreaseCapacity(request.NumberOfMailItems); + destMailContainer.IncreaseCapacity(request.NumberOfMailItems); - case MailType.SmallParcel: - if (mailContainer == null) - { - result.Success = false; - } - else if (!mailContainer.AllowedMailType.HasFlag(AllowedMailType.SmallParcel)) - { - result.Success = false; + containerDataStore.UpdateMailContainer(sourceMailContainer); + containerDataStore.UpdateMailContainer(destMailContainer); - } - else if (mailContainer.Status != MailContainerStatus.Operational) - { - result.Success = false; - } - break; + _unitOfWork.Commit(); } - - if (result.Success) + catch (Exception ex) { - mailContainer.Capacity -= request.NumberOfMailItems; + result.Success = false; - if (dataStoreType == "Backup") - { - var mailContainerDataStore = new BackupMailContainerDataStore(); - mailContainerDataStore.UpdateMailContainer(mailContainer); + _unitOfWork.Rollback(); - } - else - { - var mailContainerDataStore = new MailContainerDataStore(); - mailContainerDataStore.UpdateMailContainer(mailContainer); - } + _loggerAdapter.LogError(ex, "Error saving changes to containers"); } - - return result; } } -} +} \ No newline at end of file diff --git a/MailContainerTest/Strategies/Behaviours/AllowedMailTypeBehaviour.cs b/MailContainerTest/Strategies/Behaviours/AllowedMailTypeBehaviour.cs new file mode 100644 index 0000000..9fd905e --- /dev/null +++ b/MailContainerTest/Strategies/Behaviours/AllowedMailTypeBehaviour.cs @@ -0,0 +1,12 @@ +using MailContainerTest.Abstractions; +using MailContainerTest.Types; + +namespace MailContainerTest.Strategies.Behaviours; + +public sealed class AllowedMailTypeBehaviour : IAllowedMailTypeBehaviour +{ + public bool IsAllowedMailType(MailContainer sourceContainer, MailContainer destContainer) + { + return sourceContainer.AllowedMailType.HasFlag(AllowedMailType.LargeLetter) && destContainer.AllowedMailType.HasFlag(AllowedMailType.LargeLetter); + } +} \ No newline at end of file diff --git a/MailContainerTest/Strategies/Behaviours/CapacityBehaviour.cs b/MailContainerTest/Strategies/Behaviours/CapacityBehaviour.cs new file mode 100644 index 0000000..ba14350 --- /dev/null +++ b/MailContainerTest/Strategies/Behaviours/CapacityBehaviour.cs @@ -0,0 +1,12 @@ +using MailContainerTest.Abstractions; +using MailContainerTest.Types; + +namespace MailContainerTest.Strategies.Behaviours; + +public sealed class CapacityBehaviour : ICapacityBehaviour +{ + public bool IsWithinCapacity(MailContainer sourceContainer, MakeMailTransferRequest request) + { + return sourceContainer.Capacity < request.NumberOfMailItems; + } +} \ No newline at end of file diff --git a/MailContainerTest/Strategies/Behaviours/OperationalStatusBehaviour.cs b/MailContainerTest/Strategies/Behaviours/OperationalStatusBehaviour.cs new file mode 100644 index 0000000..3ce5d67 --- /dev/null +++ b/MailContainerTest/Strategies/Behaviours/OperationalStatusBehaviour.cs @@ -0,0 +1,12 @@ +using MailContainerTest.Abstractions; +using MailContainerTest.Types; + +namespace MailContainerTest.Strategies.Behaviours; + +public sealed class OperationalStatusBehaviour : IOperationalStatusBehaviour +{ + public bool IsOperationalStatus(MailContainer sourceContainer, MailContainer destContainer) + { + return sourceContainer.Status == MailContainerStatus.Operational && destContainer.Status == MailContainerStatus.Operational; + } +} \ No newline at end of file diff --git a/MailContainerTest/Strategies/LargeLetterStrategy.cs b/MailContainerTest/Strategies/LargeLetterStrategy.cs new file mode 100644 index 0000000..a612eff --- /dev/null +++ b/MailContainerTest/Strategies/LargeLetterStrategy.cs @@ -0,0 +1,56 @@ +using MailContainerTest.Abstractions; +using MailContainerTest.Types; + +namespace MailContainerTest.Strategies; + +public sealed class LargeLetterStrategy : IMailTransferStrategy +{ + private readonly IAllowedMailTypeBehaviour _allowedMailTypeBehaviour; + private readonly IOperationalStatusBehaviour _operationalStatusBehaviour; + private readonly ICapacityBehaviour _capacityBehaviour; + + public LargeLetterStrategy(IAllowedMailTypeBehaviour allowedMailTypeBehaviour, + IOperationalStatusBehaviour operationalStatusBehaviour, + ICapacityBehaviour capacityBehaviour) + { + _allowedMailTypeBehaviour = allowedMailTypeBehaviour; + _operationalStatusBehaviour = operationalStatusBehaviour; + _capacityBehaviour = capacityBehaviour; + } + + public bool IsSuccess(MailContainer sourceContainer, MailContainer destContainer, MakeMailTransferRequest request) + { + if (_allowedMailTypeBehaviour.IsAllowedMailType(sourceContainer, destContainer) is false) + { + return false; + } + + if (_operationalStatusBehaviour.IsOperationalStatus(sourceContainer, destContainer) is false) + { + return false; + } + + if (_capacityBehaviour.IsWithinCapacity(sourceContainer, request) is false) + { + return false; + } + + // Unit tests for member data (also commented out in MailContainerTest.Tests\LargeLetterStrategyTests.cs): + // if (!sourceContainer.AllowedMailType.HasFlag(AllowedMailType.LargeLetter) || !destContainer.AllowedMailType.HasFlag(AllowedMailType.LargeLetter)) + // { + // return false; + // } + // + // if (sourceContainer.Status != MailContainerStatus.Operational || destContainer.Status != MailContainerStatus.Operational) + // { + // return false; + // } + // + // if (sourceContainer.Capacity < request.NumberOfMailItems) + // { + // return false; + // } + + return true; + } +} \ No newline at end of file diff --git a/MailContainerTest/Strategies/SmallParcelStrategy.cs b/MailContainerTest/Strategies/SmallParcelStrategy.cs new file mode 100644 index 0000000..a2119f2 --- /dev/null +++ b/MailContainerTest/Strategies/SmallParcelStrategy.cs @@ -0,0 +1,32 @@ +using MailContainerTest.Abstractions; +using MailContainerTest.Types; + +namespace MailContainerTest.Strategies; + +public sealed class SmallParcelStrategy : IMailTransferStrategy +{ + private readonly IAllowedMailTypeBehaviour _allowedMailTypeBehaviour; + private readonly IOperationalStatusBehaviour _operationalStatusBehaviour; + + public SmallParcelStrategy(IAllowedMailTypeBehaviour allowedMailTypeBehaviour, + IOperationalStatusBehaviour operationalStatusBehaviour) + { + _allowedMailTypeBehaviour = allowedMailTypeBehaviour; + _operationalStatusBehaviour = operationalStatusBehaviour; + } + + public bool IsSuccess(MailContainer sourceContainer, MailContainer destContainer, MakeMailTransferRequest request) + { + if (_allowedMailTypeBehaviour.IsAllowedMailType(sourceContainer, destContainer) is false) + { + return false; + } + + if (_operationalStatusBehaviour.IsOperationalStatus(sourceContainer, destContainer) is false) + { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/MailContainerTest/Strategies/StandardLetterStrategy.cs b/MailContainerTest/Strategies/StandardLetterStrategy.cs new file mode 100644 index 0000000..c91ad23 --- /dev/null +++ b/MailContainerTest/Strategies/StandardLetterStrategy.cs @@ -0,0 +1,24 @@ +using MailContainerTest.Abstractions; +using MailContainerTest.Types; + +namespace MailContainerTest.Strategies; + +public sealed class StandardLetterStrategy : IMailTransferStrategy +{ + private readonly IAllowedMailTypeBehaviour _allowedMailTypeBehaviour; + + public StandardLetterStrategy(IAllowedMailTypeBehaviour allowedMailTypeBehaviour) + { + _allowedMailTypeBehaviour = allowedMailTypeBehaviour; + } + + public bool IsSuccess(MailContainer sourceContainer, MailContainer destContainer, MakeMailTransferRequest request) + { + if (_allowedMailTypeBehaviour.IsAllowedMailType(sourceContainer, destContainer) is false) + { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/MailContainerTest/Types/AllowedMailType.cs b/MailContainerTest/Types/AllowedMailType.cs index 2c7612b..2adec11 100644 --- a/MailContainerTest/Types/AllowedMailType.cs +++ b/MailContainerTest/Types/AllowedMailType.cs @@ -2,8 +2,8 @@ { public enum AllowedMailType { - StandardLetter = 1 , + StandardLetter = 1, LargeLetter = 2, - SmallParcel = 3 + SmallParcel = 4 } } \ No newline at end of file diff --git a/MailContainerTest/Types/MailContainer.cs b/MailContainerTest/Types/MailContainer.cs index 873dbe0..d319231 100644 --- a/MailContainerTest/Types/MailContainer.cs +++ b/MailContainerTest/Types/MailContainer.cs @@ -1,11 +1,34 @@ namespace MailContainerTest.Types { - public class MailContainer + public sealed class MailContainer { - public string MailContainerNumber { get; set; } - public int Capacity { get; set; } - public MailContainerStatus Status { get; set; } - public AllowedMailType AllowedMailType { get; set; } + public MailContainerNumber MailContainerNumber { get; init; } + public MailContainerCapacity Capacity { get; private set; } + public MailContainerStatus Status { get; init; } + public AllowedMailType AllowedMailType { get; init; } + public void IncreaseCapacity(int numberOfMailItems) + { + if (numberOfMailItems < 0) + { + throw new ArgumentOutOfRangeException(nameof(numberOfMailItems), + numberOfMailItems, + $"{nameof(numberOfMailItems)} must be a positive number higher than 0."); + } + + Capacity += numberOfMailItems; + } + + public void DecreaseCapacity(int numberOfMailItems) + { + if (numberOfMailItems < 0) + { + throw new ArgumentOutOfRangeException(nameof(numberOfMailItems), + numberOfMailItems, + $"{nameof(numberOfMailItems)} must be a positive number higher than 0."); + } + + Capacity -= numberOfMailItems; + } } -} +} \ No newline at end of file diff --git a/MailContainerTest/Types/MailContainerCapacity.cs b/MailContainerTest/Types/MailContainerCapacity.cs new file mode 100644 index 0000000..b96d963 --- /dev/null +++ b/MailContainerTest/Types/MailContainerCapacity.cs @@ -0,0 +1,20 @@ +namespace MailContainerTest.Types; + +public readonly record struct MailContainerCapacity +{ + private int Value { get; } + + private MailContainerCapacity(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Capacity can never be negative. (overflow)"); + } + + Value = value; + } + + public static implicit operator int(MailContainerCapacity mailContainerCapacity) => mailContainerCapacity.Value; + + public static implicit operator MailContainerCapacity(int value) => new(value); +} \ No newline at end of file diff --git a/MailContainerTest/Types/MailContainerNumber.cs b/MailContainerTest/Types/MailContainerNumber.cs new file mode 100644 index 0000000..8856adc --- /dev/null +++ b/MailContainerTest/Types/MailContainerNumber.cs @@ -0,0 +1,20 @@ +namespace MailContainerTest.Types; + +public readonly record struct MailContainerNumber +{ + private string Value { get; } + + private MailContainerNumber(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException(); + } + + Value = value; + } + + public static implicit operator string(MailContainerNumber mailContainerNumber) => mailContainerNumber.Value; + + public static implicit operator MailContainerNumber(string value) => new(value); +} \ No newline at end of file diff --git a/MailContainerTest/Types/MakeMailTransferRequest.cs b/MailContainerTest/Types/MakeMailTransferRequest.cs index aab1add..87ca1df 100644 --- a/MailContainerTest/Types/MakeMailTransferRequest.cs +++ b/MailContainerTest/Types/MakeMailTransferRequest.cs @@ -1,11 +1,11 @@ namespace MailContainerTest.Types { - public class MakeMailTransferRequest + public sealed record MakeMailTransferRequest { - public string SourceMailContainerNumber { get; set; } - public string DestinationMailContainerNumber { get; set; } - public int NumberOfMailItems { get; set; } - public DateTime TransferDate { get; set; } - public MailType MailType { get; set; } + public MailContainerNumber SourceMailContainerNumber { get; init; } + public MailContainerNumber DestinationMailContainerNumber { get; init; } + public int NumberOfMailItems { get; init; } + public DateTime TransferDate { get; init; } + public MailType MailType { get; init; } } } diff --git a/README.md b/README.md index e876a4d..edab595 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,10 @@ You should add suitable tests into the MailContainerTest.Test project. There are no additional constraints, use the packages and approach you feel appropriate, aim to spend no more than 2 hours. Please update the readme with specific comments on any areas that are unfinished and what you would cover given more time. + +Dev notes: + - Assumed that code logic was incomplete according to requirements details + - Took aprox 2h to complete (timeboxed according with requirements details) + - Tests not fully covered. Just some to show variety of examples + - Some implementations not completed since time was limited. Interfaces used for demonstration instead (ie: unit of work) + - Project structure kept the same for simplicity. By preference I usually go for physical partitons using the Clean Architecture design but also looking into Vertical Slice Architecture design for simplicity and higher cohesion