diff --git a/.github/workflows/createrelease.yml b/.github/workflows/createrelease.yml index f0777fa3..b73b63a7 100644 --- a/.github/workflows/createrelease.yml +++ b/.github/workflows/createrelease.yml @@ -36,7 +36,7 @@ jobs: dotnet test "TransactionProcessor.BusinessLogic.Tests\TransactionProcessor.BusinessLogic.Tests.csproj" dotnet test "TransactionProcessor.ProjectionEngine.Tests\TransactionProcessor.ProjectionEngine.Tests.csproj" dotnet test "TransactionProcessor.Aggregates.Tests\TransactionProcessor.Aggregates.Tests.csproj" - dotnet test "TransactionProcessor.Tests\TransactionProcessor.Tests.csproj" + dotnet test "TransactionProcessor.Tests\TransactionProcessor.Tests.csproj" - name: Publish Images to Docker Hub - Pre Release if: ${{ github.event.release.prerelease == true }} diff --git a/.github/workflows/nightlybuild.yml b/.github/workflows/nightlybuild.yml index 9521488d..7e5b834c 100644 --- a/.github/workflows/nightlybuild.yml +++ b/.github/workflows/nightlybuild.yml @@ -37,12 +37,13 @@ jobs: dotnet test "TransactionProcessor.ProjectionEngine.Tests\TransactionProcessor.ProjectionEngine.Tests.csproj" /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov2.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov" dotnet test "TransactionProcessor.Aggregates.Tests\TransactionProcessor.Aggregates.Tests.csproj" /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov3.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov" dotnet test "TransactionProcessor.Tests\TransactionProcessor.Tests.csproj" /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov4.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov" - + dotnet test "TransactionProcessor.DatabaseTests\TransactionProcessor.DatabaseTests.csproj" /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:ExcludeByAttribute="Obsolete" /p:ExcludeByAttribute="GeneratedCodeAttribute" /p:ExcludeByAttribute="CompilerGeneratedAttribute" /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" /p:CoverletOutput="../lcov5.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov" + - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./lcov1.info,./lcov2.info,./lcov3.info,./lcov4.info + files: ./lcov1.info,./lcov2.info,./lcov3.info,./lcov4.info,./lcov5.info - name: Build Docker Image run: docker build . --file TransactionProcessor/Dockerfile --tag transactionprocessor:latest diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index ea79b47b..20dfc440 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -34,6 +34,7 @@ jobs: dotnet test "TransactionProcessor.ProjectionEngine.Tests\TransactionProcessor.ProjectionEngine.Tests.csproj" dotnet test "TransactionProcessor.Aggregates.Tests\TransactionProcessor.Aggregates.Tests.csproj" dotnet test "TransactionProcessor.Tests\TransactionProcessor.Tests.csproj" + dotnet test "TransactionProcessor.DatabaseTests\TransactionProcessor.DatabaseTests.csproj" - name: Build Docker Image run: docker build . --file TransactionProcessor/Dockerfile --tag transactionprocessor:latest diff --git a/TransactionProcessor.DatabaseTests/BaseTest.cs b/TransactionProcessor.DatabaseTests/BaseTest.cs new file mode 100644 index 00000000..ed6ca128 --- /dev/null +++ b/TransactionProcessor.DatabaseTests/BaseTest.cs @@ -0,0 +1,128 @@ +using Ductus.FluentDocker.Services; +using Ductus.FluentDocker.Services.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NLog; +using Shared.EntityFramework; +using Shared.IntegrationTesting; +using Shared.Logger; +using Shouldly; +using SimpleResults; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using TransactionProcessor.Database.Contexts; +using TransactionProcessor.Repository; +using Xunit.Abstractions; +using Logger = Shared.Logger.Logger; +using NullLogger = Microsoft.Extensions.Logging.Abstractions.NullLogger; + +namespace TransactionProcessor.DatabaseTests +{ + public abstract class BaseTest : IAsyncLifetime + { + protected ITransactionProcessorReadModelRepository Repository; + protected ITestOutputHelper TestOutputHelper; + public virtual async Task InitializeAsync() + { + Logger.Initialise(new Shared.Logger.NullLogger()); + + this.TestId = Guid.NewGuid(); + + await this.StartSqlContainer(); + await this.GetRepository(); + EstateManagementContext context = this.GetContext(); + await context.Database.EnsureCreatedAsync(CancellationToken.None); + } + + public EstateManagementContext GetContext() + { + return new EstateManagementContext(GetLocalConnectionString($"TransactionProcessorReadModel-{this.TestId}")); + } + + public async Task GetRepository() + { + String dbConnString = GetLocalConnectionString($"TransactionProcessorReadModel-{this.TestId}"); + + Mock> resolver = new Mock>(); + resolver.Setup(r => r.Resolve(It.IsAny(), It.IsAny())) + .Returns(() => + { + Mock innerScope = new Mock(); + EstateManagementContext context = new EstateManagementContext(dbConnString); + + innerScope.Setup(s => s.ServiceProvider.GetService(typeof(EstateManagementContext))) + .Returns(context); + + return new ResolvedDbContext(innerScope.Object); + }); + + this.Repository = new TransactionProcessorReadModelRepository(resolver.Object); + } + + public virtual async Task DisposeAsync() + { + } + + //protected abstract Task ClearStandingData(); + //protected abstract Task SetupStandingData(); + + protected Guid TestId; + + public static IContainerService DatabaseServerContainer; + public static INetworkService DatabaseServerNetwork; + public static (String usename, String password) SqlCredentials = ("sa", "thisisalongpassword123!"); + + public static String GetLocalConnectionString(String databaseName) + { + Int32 databaseHostPort = DatabaseServerContainer.ToHostExposedEndpoint("1433/tcp").Port; + + return $"server=localhost,{databaseHostPort};database={databaseName};user id={SqlCredentials.usename};password={SqlCredentials.password};Encrypt=false"; + } + + internal async Task StartSqlContainer() + { + DockerHelper dockerHelper = new TestDockerHelper(); + + NlogLogger logger = new NlogLogger(); + logger.Initialise(LogManager.GetLogger("Specflow"), "Specflow"); + LogManager.AddHiddenAssembly(typeof(NlogLogger).Assembly); + dockerHelper.Logger = logger; + dockerHelper.SqlCredentials = SqlCredentials; + dockerHelper.SqlServerContainerName = "sharedsqlserver_repotests"; + dockerHelper.RequiredDockerServices = DockerServices.SqlServer; + + DatabaseServerNetwork = dockerHelper.SetupTestNetwork("sharednetwork", true); + await Retry.For(async () => { + DatabaseServerContainer = await dockerHelper.SetupSqlServerContainer(DatabaseServerNetwork); + }); + } + + public void Dispose() + { + EstateManagementContext context = new EstateManagementContext(BaseTest.GetLocalConnectionString($"EstateReportingReadModel{this.TestId.ToString()}")); + + Console.WriteLine($"About to delete database EstateReportingReadModel{this.TestId.ToString()}"); + Boolean result = context.Database.EnsureDeleted(); + Console.WriteLine($"Delete result is {result}"); + result.ShouldBeTrue(); + } + } + + + + public class TestDockerHelper : DockerHelper + { + public override async Task CreateSubscriptions() + { + // Nothing here + } + } + +} diff --git a/TransactionProcessor.DatabaseTests/ContractEventTests.cs b/TransactionProcessor.DatabaseTests/ContractEventTests.cs new file mode 100644 index 00000000..c8e58f9d --- /dev/null +++ b/TransactionProcessor.DatabaseTests/ContractEventTests.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Shared.EntityFramework; +using Shouldly; +using SimpleResults; +using System.ComponentModel.Design; +using TransactionProcessor.Database.Contexts; +using TransactionProcessor.Database.Entities; +using TransactionProcessor.Repository; +using TransactionProcessor.Testing; + +namespace TransactionProcessor.DatabaseTests +{ + public class ContractEventTests : BaseTest + { + [Fact] + public async Task AddContract_ContractIsAdded() { + Result result = await this.Repository.AddContract(TestData.DomainEvents.ContractCreatedEvent, CancellationToken.None); + result.IsSuccess.ShouldBeTrue(); + EstateManagementContext context = this.GetContext(); + Contract? contract = await context.Contracts.SingleOrDefaultAsync(c => c.ContractId == TestData.DomainEvents.ContractCreatedEvent.ContractId); + contract.ShouldNotBeNull(); + } + + [Fact] + public async Task AddContract_ContractIsAdded_EventReplayHandled() { + Result result = await this.Repository.AddContract(TestData.DomainEvents.ContractCreatedEvent, CancellationToken.None); + result.IsSuccess.ShouldBeTrue(); + + result = await this.Repository.AddContract(TestData.DomainEvents.ContractCreatedEvent, CancellationToken.None); + result.IsSuccess.ShouldBeTrue(); + } + } +} diff --git a/TransactionProcessor.DatabaseTests/TransactionProcessor.DatabaseTests.csproj b/TransactionProcessor.DatabaseTests/TransactionProcessor.DatabaseTests.csproj new file mode 100644 index 00000000..2970b6be --- /dev/null +++ b/TransactionProcessor.DatabaseTests/TransactionProcessor.DatabaseTests.csproj @@ -0,0 +1,31 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TransactionProcessor.Repository/ITransactionProcessorReadModelRepository.cs b/TransactionProcessor.Repository/ITransactionProcessorReadModelRepository.cs index 10a8249e..c1f329be 100644 --- a/TransactionProcessor.Repository/ITransactionProcessorReadModelRepository.cs +++ b/TransactionProcessor.Repository/ITransactionProcessorReadModelRepository.cs @@ -1659,7 +1659,7 @@ public async Task AddContract(ContractDomainEvents.ContractCreatedEvent await context.Contracts.AddAsync(contract, cancellationToken); - return await context.SaveChangesAsync(cancellationToken); + return await context.SaveChangesWithDuplicateHandling(cancellationToken); } public async Task AddContractProduct(ContractDomainEvents.VariableValueProductAddedToContractEvent domainEvent, diff --git a/TransactionProcessor.sln b/TransactionProcessor.sln index db70f73f..7d35f702 100644 --- a/TransactionProcessor.sln +++ b/TransactionProcessor.sln @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.Reposi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.DomainEvents", "TransactionProcessor.DomainEvents\TransactionProcessor.DomainEvents.csproj", "{45712D2F-07CB-4C28-9DC3-6D3FD686D01F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionProcessor.DatabaseTests", "TransactionProcessor.DatabaseTests\TransactionProcessor.DatabaseTests.csproj", "{33C3AABB-B6A9-40CD-A8E0-3EF8DD38646E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -115,6 +117,10 @@ Global {45712D2F-07CB-4C28-9DC3-6D3FD686D01F}.Debug|Any CPU.Build.0 = Debug|Any CPU {45712D2F-07CB-4C28-9DC3-6D3FD686D01F}.Release|Any CPU.ActiveCfg = Release|Any CPU {45712D2F-07CB-4C28-9DC3-6D3FD686D01F}.Release|Any CPU.Build.0 = Release|Any CPU + {33C3AABB-B6A9-40CD-A8E0-3EF8DD38646E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33C3AABB-B6A9-40CD-A8E0-3EF8DD38646E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33C3AABB-B6A9-40CD-A8E0-3EF8DD38646E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33C3AABB-B6A9-40CD-A8E0-3EF8DD38646E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -137,6 +143,7 @@ Global {812BE19F-0494-43C3-95CE-4684ECAC3CB3} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} {0696B63D-2807-40F9-BEEA-87D0415EC929} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} {45712D2F-07CB-4C28-9DC3-6D3FD686D01F} = {749ADE74-A6F0-4469-A638-8FD7E82A8667} + {33C3AABB-B6A9-40CD-A8E0-3EF8DD38646E} = {71B30DC4-AB27-4D30-8481-B4C326D074CB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {193D13DE-424B-4D50-B674-01F9E4CC2CA9}