diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e0e28c4942..d28a7f9ceb 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -75,6 +75,8 @@ + + diff --git a/src/ServiceControl.Persistence.EFCore.PostgreSql/ServiceControl.Persistence.EFCore.PostgreSql.csproj b/src/ServiceControl.Persistence.EFCore.PostgreSql/ServiceControl.Persistence.EFCore.PostgreSql.csproj index f638c069d0..a20e270c74 100644 --- a/src/ServiceControl.Persistence.EFCore.PostgreSql/ServiceControl.Persistence.EFCore.PostgreSql.csproj +++ b/src/ServiceControl.Persistence.EFCore.PostgreSql/ServiceControl.Persistence.EFCore.PostgreSql.csproj @@ -33,5 +33,8 @@ + + + diff --git a/src/ServiceControl.Persistence.EFCore.SqlServer/ServiceControl.Persistence.EFCore.SqlServer.csproj b/src/ServiceControl.Persistence.EFCore.SqlServer/ServiceControl.Persistence.EFCore.SqlServer.csproj index 862beed816..9346aa13f2 100644 --- a/src/ServiceControl.Persistence.EFCore.SqlServer/ServiceControl.Persistence.EFCore.SqlServer.csproj +++ b/src/ServiceControl.Persistence.EFCore.SqlServer/ServiceControl.Persistence.EFCore.SqlServer.csproj @@ -33,5 +33,9 @@ + + + + diff --git a/src/ServiceControl.Persistence.Tests.InMemory/PersistenceTestsContext.cs b/src/ServiceControl.Persistence.Tests.InMemory/PersistenceTestsContext.cs index 4e2f2e3d23..8e66c39775 100644 --- a/src/ServiceControl.Persistence.Tests.InMemory/PersistenceTestsContext.cs +++ b/src/ServiceControl.Persistence.Tests.InMemory/PersistenceTestsContext.cs @@ -8,6 +8,7 @@ public class PersistenceTestsContext : IPersistenceTestsContext { public PersistenceSettings PersistenceSettings { get; private set; } + public string GenerateFailedMessageRecordId(string messageId) => throw new System.NotImplementedException(); public Task Setup(IHostApplicationBuilder hostBuilder) { diff --git a/src/ServiceControl.Persistence.Tests.PostgreSql/.editorconfig b/src/ServiceControl.Persistence.Tests.PostgreSql/.editorconfig new file mode 100644 index 0000000000..20102eaa39 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.PostgreSql/.editorconfig @@ -0,0 +1,5 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none + diff --git a/src/ServiceControl.Persistence.Tests.PostgreSql/PersistenceTestsContext.cs b/src/ServiceControl.Persistence.Tests.PostgreSql/PersistenceTestsContext.cs new file mode 100644 index 0000000000..091e910ece --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.PostgreSql/PersistenceTestsContext.cs @@ -0,0 +1,38 @@ +// ReSharper disable once CheckNamespace +namespace ServiceControl.Persistence.Tests; + +using System.Threading.Tasks; +using EFCore.PostgreSql; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +public class PersistenceTestsContext : IPersistenceTestsContext +{ + public async Task Setup(IHostApplicationBuilder hostBuilder) + { + PersistenceSettings = new PostgreSqlPersisterSettings + { + ConnectionString = await PostgreSqlSharedContainer.GetConnectionStringAsync() + }; + + var persistence = new PostgreSqlPersistenceConfiguration().Create(PersistenceSettings); + + persistence.AddPersistence(hostBuilder.Services); + persistence.AddInstaller(hostBuilder.Services); + } + + public async Task PostSetup(IHost host) + { + using var scope = host.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.EnsureCreatedAsync(); + } + + public Task TearDown() => Task.CompletedTask; + + public void CompleteDatabaseOperation() { } + + public PersistenceSettings PersistenceSettings { get; set; } + + public string GenerateFailedMessageRecordId(string messageId) => messageId; +} diff --git a/src/ServiceControl.Persistence.Tests.PostgreSql/PostgreSqlSharedContainer.cs b/src/ServiceControl.Persistence.Tests.PostgreSql/PostgreSqlSharedContainer.cs new file mode 100644 index 0000000000..b3c5a9b223 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.PostgreSql/PostgreSqlSharedContainer.cs @@ -0,0 +1,47 @@ +namespace ServiceControl.Persistence.Tests; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Testcontainers.PostgreSql; + +static class PostgreSqlSharedContainer +{ + public static async Task GetConnectionStringAsync(CancellationToken ct = default) + { + var envConnStr = Environment.GetEnvironmentVariable("ServiceControl_Persistence_PostgreSql_ConnectionString"); + if (!string.IsNullOrEmpty(envConnStr)) + { + return envConnStr; + } + + if (container != null) + { + return container.GetConnectionString(); + } + + await semaphore.WaitAsync(ct); + try + { + container ??= await StartContainerAsync(ct); + return container.GetConnectionString(); + } + finally + { + semaphore.Release(); + } + } + + public static async Task Stop() => await (container?.DisposeAsync() ?? ValueTask.CompletedTask); + + static async Task StartContainerAsync(CancellationToken ct) + { + var c = new PostgreSqlBuilder("postgres:16-alpine") + .Build(); + await c.StartAsync(ct); + return c; + } + + static PostgreSqlContainer container; + static readonly SemaphoreSlim semaphore = new(1, 1); +} diff --git a/src/ServiceControl.Persistence.Tests.PostgreSql/ServiceControl.Persistence.Tests.PostgreSql.csproj b/src/ServiceControl.Persistence.Tests.PostgreSql/ServiceControl.Persistence.Tests.PostgreSql.csproj new file mode 100644 index 0000000000..4394b112a4 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.PostgreSql/ServiceControl.Persistence.Tests.PostgreSql.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ServiceControl.Persistence.Tests.PostgreSql/StopSharedPostgreSql.cs b/src/ServiceControl.Persistence.Tests.PostgreSql/StopSharedPostgreSql.cs new file mode 100644 index 0000000000..d5b0e8515e --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.PostgreSql/StopSharedPostgreSql.cs @@ -0,0 +1,12 @@ +namespace ServiceControl; + +using System.Threading.Tasks; +using NUnit.Framework; +using Persistence.Tests; + +[SetUpFixture] +public class StopSharedPostgreSql +{ + [OneTimeTearDown] + public async Task Teardown() => await PostgreSqlSharedContainer.Stop(); +} diff --git a/src/ServiceControl.Persistence.Tests.PostgreSql/TestsFilter.cs b/src/ServiceControl.Persistence.Tests.PostgreSql/TestsFilter.cs new file mode 100644 index 0000000000..c2971486f6 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.PostgreSql/TestsFilter.cs @@ -0,0 +1 @@ +[assembly: IncludeInTestCategory("PostgreSqlPersistence")] diff --git a/src/ServiceControl.Persistence.Tests.RavenDB/PersistenceTestsContext.cs b/src/ServiceControl.Persistence.Tests.RavenDB/PersistenceTestsContext.cs index 1bf7b44424..e8ba73939c 100644 --- a/src/ServiceControl.Persistence.Tests.RavenDB/PersistenceTestsContext.cs +++ b/src/ServiceControl.Persistence.Tests.RavenDB/PersistenceTestsContext.cs @@ -53,6 +53,7 @@ public async Task PostSetup(IHost host) public async Task TearDown() => await embeddedServer.DeleteDatabase(databaseName); public PersistenceSettings PersistenceSettings { get; private set; } + public string GenerateFailedMessageRecordId(string messageId) => FailedMessageIdGenerator.MakeDocumentId(messageId); public IDocumentStore DocumentStore { get; private set; } diff --git a/src/ServiceControl.Persistence.Tests.SqlServer/.editorconfig b/src/ServiceControl.Persistence.Tests.SqlServer/.editorconfig new file mode 100644 index 0000000000..20102eaa39 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.SqlServer/.editorconfig @@ -0,0 +1,5 @@ +[*.cs] + +# Justification: ServiceControl app has no synchronization context +dotnet_diagnostic.CA2007.severity = none + diff --git a/src/ServiceControl.Persistence.Tests.SqlServer/PersistenceTestsContext.cs b/src/ServiceControl.Persistence.Tests.SqlServer/PersistenceTestsContext.cs new file mode 100644 index 0000000000..2d3a6b49d3 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.SqlServer/PersistenceTestsContext.cs @@ -0,0 +1,38 @@ +// ReSharper disable once CheckNamespace +namespace ServiceControl.Persistence.Tests; + +using System.Threading.Tasks; +using EFCore.SqlServer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +public class PersistenceTestsContext : IPersistenceTestsContext +{ + public async Task Setup(IHostApplicationBuilder hostBuilder) + { + PersistenceSettings = new SqlServerPersisterSettings + { + ConnectionString = await SqlServerSharedContainer.GetConnectionStringAsync() + }; + + var persistence = new SqlServerPersistenceConfiguration().Create(PersistenceSettings); + + persistence.AddPersistence(hostBuilder.Services); + persistence.AddInstaller(hostBuilder.Services); + } + + public async Task PostSetup(IHost host) + { + using var scope = host.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.EnsureCreatedAsync(); + } + + public Task TearDown() => Task.CompletedTask; + + public void CompleteDatabaseOperation() { } + + public PersistenceSettings PersistenceSettings { get; set; } + + public string GenerateFailedMessageRecordId(string messageId) => messageId; +} \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests.SqlServer/ServiceControl.Persistence.Tests.SqlServer.csproj b/src/ServiceControl.Persistence.Tests.SqlServer/ServiceControl.Persistence.Tests.SqlServer.csproj new file mode 100644 index 0000000000..acbe985b53 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.SqlServer/ServiceControl.Persistence.Tests.SqlServer.csproj @@ -0,0 +1,31 @@ + + + + net10.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ServiceControl.Persistence.Tests.SqlServer/SqlServerSharedContainer.cs b/src/ServiceControl.Persistence.Tests.SqlServer/SqlServerSharedContainer.cs new file mode 100644 index 0000000000..066e823329 --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.SqlServer/SqlServerSharedContainer.cs @@ -0,0 +1,47 @@ +namespace ServiceControl.Persistence.Tests; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Testcontainers.MsSql; + +static class SqlServerSharedContainer +{ + public static async Task GetConnectionStringAsync(CancellationToken ct = default) + { + var envConnStr = Environment.GetEnvironmentVariable("ServiceControl_Persistence_SqlServer_ConnectionString"); + if (!string.IsNullOrEmpty(envConnStr)) + { + return envConnStr; + } + + if (container != null) + { + return container.GetConnectionString(); + } + + await semaphore.WaitAsync(ct); + try + { + container ??= await StartContainerAsync(ct); + return container.GetConnectionString(); + } + finally + { + semaphore.Release(); + } + } + + public static async Task Stop() => await (container?.DisposeAsync() ?? ValueTask.CompletedTask); + + static async Task StartContainerAsync(CancellationToken ct) + { + var c = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2025-latest") + .Build(); + await c.StartAsync(ct); + return c; + } + + static MsSqlContainer container; + static readonly SemaphoreSlim semaphore = new(1, 1); +} diff --git a/src/ServiceControl.Persistence.Tests.SqlServer/StopSharedSqlServer.cs b/src/ServiceControl.Persistence.Tests.SqlServer/StopSharedSqlServer.cs new file mode 100644 index 0000000000..61b506cceb --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.SqlServer/StopSharedSqlServer.cs @@ -0,0 +1,12 @@ +namespace ServiceControl; + +using System.Threading.Tasks; +using NUnit.Framework; +using Persistence.Tests; + +[SetUpFixture] +public class StopSharedSqlServer +{ + [OneTimeTearDown] + public async Task Teardown() => await SqlServerSharedContainer.Stop(); +} diff --git a/src/ServiceControl.Persistence.Tests.SqlServer/TestsFilter.cs b/src/ServiceControl.Persistence.Tests.SqlServer/TestsFilter.cs new file mode 100644 index 0000000000..d37d903ffb --- /dev/null +++ b/src/ServiceControl.Persistence.Tests.SqlServer/TestsFilter.cs @@ -0,0 +1 @@ +[assembly: IncludeInTestCategory("SqlServerPersistence")] diff --git a/src/ServiceControl.Persistence.Tests/IPersistenceTestsContext.cs b/src/ServiceControl.Persistence.Tests/IPersistenceTestsContext.cs index bebd0aec54..5009a117a8 100644 --- a/src/ServiceControl.Persistence.Tests/IPersistenceTestsContext.cs +++ b/src/ServiceControl.Persistence.Tests/IPersistenceTestsContext.cs @@ -14,4 +14,6 @@ public interface IPersistenceTestsContext void CompleteDatabaseOperation(); PersistenceSettings PersistenceSettings { get; } + + string GenerateFailedMessageRecordId(string messageId); } \ No newline at end of file diff --git a/src/ServiceControl.Persistence.Tests/Recoverability/EditHandlerAuditTests.cs b/src/ServiceControl.Persistence.Tests/Recoverability/EditHandlerAuditTests.cs index a22a45e2fd..df02f36532 100644 --- a/src/ServiceControl.Persistence.Tests/Recoverability/EditHandlerAuditTests.cs +++ b/src/ServiceControl.Persistence.Tests/Recoverability/EditHandlerAuditTests.cs @@ -91,7 +91,7 @@ async Task CreateAndStoreFailedMessage(string failedMessageId = n var failedMessage = new FailedMessage { UniqueMessageId = failedMessageId, - Id = FailedMessageIdGenerator.MakeDocumentId(failedMessageId), + Id = PersistenceTestsContext.GenerateFailedMessageRecordId(failedMessageId), Status = FailedMessageStatus.Unresolved, ProcessingAttempts = [ diff --git a/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs b/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs index 74f62ef85f..7836a00e0f 100644 --- a/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs +++ b/src/ServiceControl.Persistence.Tests/Recoverability/EditMessageTests.cs @@ -248,7 +248,7 @@ async Task CreateAndStoreFailedMessage(string failedMessageId = n var failedMessage = new FailedMessage { UniqueMessageId = failedMessageId, - Id = FailedMessageIdGenerator.MakeDocumentId(failedMessageId), + Id = PersistenceTestsContext.GenerateFailedMessageRecordId(failedMessageId), Status = status, ProcessingAttempts = [ diff --git a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs index 8f22784e67..bc87e50e34 100644 --- a/src/ServiceControl.Persistence.Tests/RetryStateTests.cs +++ b/src/ServiceControl.Persistence.Tests/RetryStateTests.cs @@ -239,7 +239,7 @@ public async Task When_a_selection_is_staged_each_message_is_audited_as_a_batch( var messages = ids.Select(id => new FailedMessage { - Id = FailedMessageIdGenerator.MakeDocumentId(id), + Id = PersistenceTestsContext.GenerateFailedMessageRecordId(id), UniqueMessageId = id, Status = FailedMessageStatus.Unresolved, ProcessingAttempts = @@ -318,7 +318,7 @@ async Task CreateAFailedMessageAndMarkAsPartOfRetryBatch(RetryingManager retryMa { var messages = messageIds.Select(id => new FailedMessage { - Id = FailedMessageIdGenerator.MakeDocumentId(id), + Id = PersistenceTestsContext.GenerateFailedMessageRecordId(id), UniqueMessageId = id, FailureGroups = [ diff --git a/src/ServiceControl.slnx b/src/ServiceControl.slnx index 76d72c12f1..b59518a697 100644 --- a/src/ServiceControl.slnx +++ b/src/ServiceControl.slnx @@ -51,6 +51,8 @@ + + diff --git a/src/ServiceControl/ServiceControl.csproj b/src/ServiceControl/ServiceControl.csproj index 6c857f62d8..2223cb11af 100644 --- a/src/ServiceControl/ServiceControl.csproj +++ b/src/ServiceControl/ServiceControl.csproj @@ -60,7 +60,9 @@ + +