From 418bc36d361d940e5353c6d384767b52898eaf62 Mon Sep 17 00:00:00 2001 From: Mono <81423605+monofunc@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:52:57 +0400 Subject: [PATCH 1/3] Migrate HealthChecks.UI tests to Testcontainers --- .github/workflows/healthchecks_ui_ci.yml | 21 ---------- .../Fixtures/MySqlContainerFixture.cs | 39 +++++++++++++++++++ .../Fixtures/PostgreSqlContainerFixture.cs | 39 +++++++++++++++++++ .../Fixtures/SqlServerContainerFixture.cs | 39 +++++++++++++++++++ .../UIHttpMessageHandlerTests.cs | 2 +- .../DockerImageStorageProviderTests.cs | 13 ++++--- .../InMemoryStorageProviderTests.cs | 4 +- .../MySqlStorageProviderTests.cs | 11 ++++-- .../PostgreSqlStorageProviderTests.cs | 7 ++-- .../SqlServerStorageProviderTests.cs | 7 ++-- .../SqliteStorageProviderTests.cs | 2 +- .../HealthChecks.UI.Tests.csproj | 6 +++ .../Seedwork/ProviderTestHelper.cs | 5 --- 13 files changed, 151 insertions(+), 44 deletions(-) create mode 100644 test/HealthChecks.UI.Tests/Fixtures/MySqlContainerFixture.cs create mode 100644 test/HealthChecks.UI.Tests/Fixtures/PostgreSqlContainerFixture.cs create mode 100644 test/HealthChecks.UI.Tests/Fixtures/SqlServerContainerFixture.cs diff --git a/.github/workflows/healthchecks_ui_ci.yml b/.github/workflows/healthchecks_ui_ci.yml index bb895564b4..d505938095 100644 --- a/.github/workflows/healthchecks_ui_ci.yml +++ b/.github/workflows/healthchecks_ui_ci.yml @@ -36,27 +36,6 @@ on: jobs: build: runs-on: ubuntu-latest - services: - sqlserver: - image: mcr.microsoft.com/mssql/server - ports: - - 5433:1433 - env: - ACCEPT_EULA: Y - SA_PASSWORD: Password12! - npgsql: - image: postgres - ports: - - 8010:5432 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: Password12! - mysql: - image: mysql - ports: - - 3306:3306 - env: - MYSQL_ROOT_PASSWORD: Password12! steps: - uses: actions/checkout@v3 - name: Setup .NET diff --git a/test/HealthChecks.UI.Tests/Fixtures/MySqlContainerFixture.cs b/test/HealthChecks.UI.Tests/Fixtures/MySqlContainerFixture.cs new file mode 100644 index 0000000000..b2fd80593c --- /dev/null +++ b/test/HealthChecks.UI.Tests/Fixtures/MySqlContainerFixture.cs @@ -0,0 +1,39 @@ +using Testcontainers.MySql; + +namespace HealthChecks.UI.Tests.Fixtures; + +public class MySqlContainerFixture : IAsyncLifetime +{ + private const string Registry = "docker.io"; + + private const string Image = "library/mysql"; + + private const string Tag = "9.4.0"; + + public MySqlContainer? Container { get; private set; } + + public string GetConnectionString() + { + if (Container is null) + { + throw new InvalidOperationException("The test container was not initialized."); + } + + return Container.GetConnectionString(); + } + + public async Task InitializeAsync() => Container = await CreateContainerAsync(); + + public Task DisposeAsync() => Container?.DisposeAsync().AsTask() ?? Task.CompletedTask; + + private static async Task CreateContainerAsync() + { + var container = new MySqlBuilder() + .WithImage($"{Registry}/{Image}:{Tag}") + .Build(); + + await container.StartAsync(); + + return container; + } +} diff --git a/test/HealthChecks.UI.Tests/Fixtures/PostgreSqlContainerFixture.cs b/test/HealthChecks.UI.Tests/Fixtures/PostgreSqlContainerFixture.cs new file mode 100644 index 0000000000..c13f3084ed --- /dev/null +++ b/test/HealthChecks.UI.Tests/Fixtures/PostgreSqlContainerFixture.cs @@ -0,0 +1,39 @@ +using Testcontainers.PostgreSql; + +namespace HealthChecks.UI.Tests.Fixtures; + +public class PostgreSqlContainerFixture : IAsyncLifetime +{ + private const string Registry = "docker.io"; + + private const string Image = "library/postgres"; + + private const string Tag = "17.6-alpine3.22"; + + public PostgreSqlContainer? Container { get; private set; } + + public string GetConnectionString() + { + if (Container is null) + { + throw new InvalidOperationException("The test container was not initialized."); + } + + return Container.GetConnectionString(); + } + + public async Task InitializeAsync() => Container = await CreateContainerAsync(); + + public Task DisposeAsync() => Container?.DisposeAsync().AsTask() ?? Task.CompletedTask; + + private static async Task CreateContainerAsync() + { + var container = new PostgreSqlBuilder() + .WithImage($"{Registry}/{Image}:{Tag}") + .Build(); + + await container.StartAsync(); + + return container; + } +} diff --git a/test/HealthChecks.UI.Tests/Fixtures/SqlServerContainerFixture.cs b/test/HealthChecks.UI.Tests/Fixtures/SqlServerContainerFixture.cs new file mode 100644 index 0000000000..ab29a5fbae --- /dev/null +++ b/test/HealthChecks.UI.Tests/Fixtures/SqlServerContainerFixture.cs @@ -0,0 +1,39 @@ +using Testcontainers.MsSql; + +namespace HealthChecks.UI.Tests.Fixtures; + +public class SqlServerContainerFixture : IAsyncLifetime +{ + private const string Registry = "mcr.microsoft.com"; + + private const string Image = "mssql/server"; + + private const string Tag = "2022-CU20-GDR1-ubuntu-22.04"; + + public MsSqlContainer? Container { get; private set; } + + public string GetConnectionString() + { + if (Container is null) + { + throw new InvalidOperationException("The test container was not initialized."); + } + + return Container.GetConnectionString(); + } + + public async Task InitializeAsync() => Container = await CreateContainerAsync(); + + public Task DisposeAsync() => Container?.DisposeAsync().AsTask() ?? Task.CompletedTask; + + private static async Task CreateContainerAsync() + { + var container = new MsSqlBuilder() + .WithImage($"{Registry}/{Image}:{Tag}") + .Build(); + + await container.StartAsync(); + + return container; + } +} diff --git a/test/HealthChecks.UI.Tests/Functional/Configuration/UIHttpMessageHandlerTests.cs b/test/HealthChecks.UI.Tests/Functional/Configuration/UIHttpMessageHandlerTests.cs index 26ffb13e6b..fbda94d169 100644 --- a/test/HealthChecks.UI.Tests/Functional/Configuration/UIHttpMessageHandlerTests.cs +++ b/test/HealthChecks.UI.Tests/Functional/Configuration/UIHttpMessageHandlerTests.cs @@ -80,7 +80,7 @@ public Task configure_api_endpoint_custom_delegating_handlers() return Task.CompletedTask; } - [Fact(Skip = "Temporarily skipping in https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/pull/1807")] + [Fact] public Task configure_webhooks_endpoint_custom_delegating_handlers() { var hostReset = new ManualResetEventSlim(false); diff --git a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/DockerImageStorageProviderTests.cs b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/DockerImageStorageProviderTests.cs index 10bed25346..1ab146bd51 100644 --- a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/DockerImageStorageProviderTests.cs +++ b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/DockerImageStorageProviderTests.cs @@ -1,6 +1,7 @@ using HealthChecks.UI.Data; using HealthChecks.UI.Image; using HealthChecks.UI.Image.Configuration; +using HealthChecks.UI.Tests.Fixtures; using Microsoft.Extensions.Configuration; namespace HealthChecks.UI.Tests; @@ -11,7 +12,6 @@ public class docker_image_storage_provider_configuration_should private const string SqliteProviderName = "Microsoft.EntityFrameworkCore.Sqlite"; private const string PostgreProviderName = "Npgsql.EntityFrameworkCore.PostgreSQL"; private const string InMemoryProviderName = "Microsoft.EntityFrameworkCore.InMemory"; - private const string MySqlProviderName = "Pomelo.EntityFrameworkCore.MySql"; [Fact] public void fail_with_invalid_storage_provider_value() @@ -183,11 +183,16 @@ public void register_inmemory_as_default_provider_when_no_option_is_configured() var context = host.Services.GetRequiredService(); context.Database.ProviderName.ShouldBe(InMemoryProviderName); } +} + +[Collection("execution")] +public class docker_image_storage_provider_mysql_configuration_should(MySqlContainerFixture mySqlFixture) : IClassFixture +{ + private const string MySqlProviderName = "Pomelo.EntityFrameworkCore.MySql"; [Fact] public void register_mysql() { - // var hostBuilder = new WebHostBuilder() .ConfigureAppConfiguration(config => { @@ -196,8 +201,7 @@ public void register_mysql() config.AddInMemoryCollection(new List> { new KeyValuePair("storage_provider", StorageProviderEnum.MySql.ToString()), - new KeyValuePair("storage_connection", "Host=localhost;User Id=root;Password=Password12!;Database=UI"), - + new KeyValuePair("storage_connection", mySqlFixture.GetConnectionString()), }); }) .UseStartup(); @@ -225,5 +229,4 @@ public void fail_to_register_mysql_with_no_connection_string() Should.Throw(() => hostBuilder.Build()); } - } diff --git a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/InMemoryStorageProviderTests.cs b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/InMemoryStorageProviderTests.cs index 0df030f029..b637451b9e 100644 --- a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/InMemoryStorageProviderTests.cs +++ b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/InMemoryStorageProviderTests.cs @@ -29,7 +29,7 @@ public void register_healthchecksdb_context() customOptionsInvoked.ShouldBeTrue(); } - [Fact(Skip = "conflicts with other tests that use inmemory storage too")] + [Fact] public async Task seed_database_and_serve_stored_executions() { var hostReset = new ManualResetEventSlim(false); @@ -38,7 +38,7 @@ public async Task seed_database_and_serve_stored_executions() var webHostBuilder = HostBuilderHelper.Create( hostReset, collectorReset, - configureUI: config => config.AddInMemoryStorage()); + configureUI: config => config.AddInMemoryStorage(databaseName: Guid.NewGuid().ToString())); using var host = new TestServer(webHostBuilder); diff --git a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/MySqlStorageProviderTests.cs b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/MySqlStorageProviderTests.cs index 631f52061c..79f4953102 100644 --- a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/MySqlStorageProviderTests.cs +++ b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/MySqlStorageProviderTests.cs @@ -1,9 +1,14 @@ using HealthChecks.UI.Data; +using HealthChecks.UI.Image; +using HealthChecks.UI.Image.Configuration; +using HealthChecks.UI.Tests.Fixtures; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; namespace HealthChecks.UI.Tests; -public class mysql_storage_should +[Collection("execution")] +public class mysql_storage_should(MySqlContainerFixture mySqlFixture) : IClassFixture { private const string PROVIDER_NAME = "Pomelo.EntityFrameworkCore.MySql"; @@ -17,7 +22,7 @@ public void register_healthchecksdb_context_with_migrations() .ConfigureServices(services => { services.AddHealthChecksUI() - .AddMySqlStorage("Host=localhost;User Id=root;Password=Password12!;Database=UI", options => customOptionsInvoked = true); + .AddMySqlStorage(mySqlFixture.GetConnectionString(), options => customOptionsInvoked = true); }); var services = hostBuilder.Build().Services; @@ -38,7 +43,7 @@ public async Task seed_database_and_serve_stored_executions() var webHostBuilder = HostBuilderHelper.Create( hostReset, collectorReset, - configureUI: config => config.AddMySqlStorage(ProviderTestHelper.MySqlConnectionString())); + configureUI: config => config.AddMySqlStorage(mySqlFixture.GetConnectionString())); using var host = new TestServer(webHostBuilder); diff --git a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/PostgreSqlStorageProviderTests.cs b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/PostgreSqlStorageProviderTests.cs index 67ad42f68b..fb78a21831 100644 --- a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/PostgreSqlStorageProviderTests.cs +++ b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/PostgreSqlStorageProviderTests.cs @@ -1,10 +1,11 @@ using HealthChecks.UI.Data; +using HealthChecks.UI.Tests.Fixtures; using Microsoft.EntityFrameworkCore; namespace HealthChecks.UI.Tests; [Collection("execution")] -public class postgre_storage_should +public class postgre_storage_should(PostgreSqlContainerFixture postgreSqlFixture) : IClassFixture { private const string ProviderName = "Npgsql.EntityFrameworkCore.PostgreSQL"; @@ -18,7 +19,7 @@ public void register_healthchecksdb_context_with_migrations() .ConfigureServices(services => { services.AddHealthChecksUI() - .AddPostgreSqlStorage("connectionString", options => customOptionsInvoked = true); + .AddPostgreSqlStorage(postgreSqlFixture.GetConnectionString(), _ => customOptionsInvoked = true); }); var services = hostBuilder.Build().Services; @@ -39,7 +40,7 @@ public async Task seed_database_and_serve_stored_executions() var webHostBuilder = HostBuilderHelper.Create( hostReset, collectorReset, - configureUI: config => config.AddPostgreSqlStorage(ProviderTestHelper.PostgresConnectionString())); + configureUI: config => config.AddPostgreSqlStorage(postgreSqlFixture.GetConnectionString())); using var host = new TestServer(webHostBuilder); diff --git a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/SqlServerStorageProviderTests.cs b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/SqlServerStorageProviderTests.cs index b7c01c060c..5ce2a31ad8 100644 --- a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/SqlServerStorageProviderTests.cs +++ b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/SqlServerStorageProviderTests.cs @@ -1,9 +1,10 @@ using HealthChecks.UI.Data; +using HealthChecks.UI.Tests.Fixtures; using Microsoft.EntityFrameworkCore; namespace HealthChecks.UI.Tests; -public class sqlserver_storage_should +public class sqlserver_storage_should(SqlServerContainerFixture sqlServerFixture) : IClassFixture { private const string ProviderName = "Microsoft.EntityFrameworkCore.SqlServer"; @@ -17,7 +18,7 @@ public void register_healthchecksdb_context_with_migrations() .ConfigureServices(services => { services.AddHealthChecksUI() - .AddSqlServerStorage("connectionString", opt => customOptionsInvoked = true); + .AddSqlServerStorage(sqlServerFixture.GetConnectionString(), opt => customOptionsInvoked = true); }); var services = hostBuilder.Build().Services; @@ -38,7 +39,7 @@ public async Task seed_database_and_serve_stored_executions() var webHostBuilder = HostBuilderHelper.Create( hostReset, collectorReset, - configureUI: config => config.AddSqlServerStorage(ProviderTestHelper.SqlServerConnectionString())); + configureUI: config => config.AddSqlServerStorage(sqlServerFixture.GetConnectionString())); using var host = new TestServer(webHostBuilder); diff --git a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/SqliteStorageProviderTests.cs b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/SqliteStorageProviderTests.cs index b96abae896..4c91f943c0 100644 --- a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/SqliteStorageProviderTests.cs +++ b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/SqliteStorageProviderTests.cs @@ -38,7 +38,7 @@ public async Task seed_database_and_serve_stored_executions() var webHostBuilder = HostBuilderHelper.Create( hostReset, collectorReset, - configureUI: setup => setup.AddSqliteStorage(ProviderTestHelper.SqliteConnectionString())); + configureUI: setup => setup.AddSqliteStorage("Data Source = sqlite.db")); using var host = new TestServer(webHostBuilder); diff --git a/test/HealthChecks.UI.Tests/HealthChecks.UI.Tests.csproj b/test/HealthChecks.UI.Tests/HealthChecks.UI.Tests.csproj index fe1de35d2d..9ff9690824 100644 --- a/test/HealthChecks.UI.Tests/HealthChecks.UI.Tests.csproj +++ b/test/HealthChecks.UI.Tests/HealthChecks.UI.Tests.csproj @@ -28,4 +28,10 @@ + + + + + + diff --git a/test/HealthChecks.UI.Tests/Seedwork/ProviderTestHelper.cs b/test/HealthChecks.UI.Tests/Seedwork/ProviderTestHelper.cs index d07dbf5caa..229bb260a2 100644 --- a/test/HealthChecks.UI.Tests/Seedwork/ProviderTestHelper.cs +++ b/test/HealthChecks.UI.Tests/Seedwork/ProviderTestHelper.cs @@ -10,9 +10,4 @@ public class ProviderTestHelper ("host1", "http://localhost/health"), ("host2", "http://localhost/health") }; - - public static string SqlServerConnectionString() => "Server=tcp:localhost,5433;Initial Catalog=master;User Id=sa;Password=Password12!;TrustServerCertificate=true"; - public static string PostgresConnectionString() => "Server=127.0.0.1;Port=8010;User ID=postgres;Password=Password12!;database=ui"; - public static string MySqlConnectionString() => "Host=localhost;User Id=root;Password=Password12!;Database=UI"; - public static string SqliteConnectionString() => "Data Source = sqlite.db"; } From bb0df90628a305e8cf33dd20134c23994101c335 Mon Sep 17 00:00:00 2001 From: Mono <81423605+monofunc@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:01:02 +0400 Subject: [PATCH 2/3] Fix formatting --- .../Functional/DatabaseProviders/MySqlStorageProviderTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/MySqlStorageProviderTests.cs b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/MySqlStorageProviderTests.cs index 79f4953102..39131368df 100644 --- a/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/MySqlStorageProviderTests.cs +++ b/test/HealthChecks.UI.Tests/Functional/DatabaseProviders/MySqlStorageProviderTests.cs @@ -1,9 +1,6 @@ using HealthChecks.UI.Data; -using HealthChecks.UI.Image; -using HealthChecks.UI.Image.Configuration; using HealthChecks.UI.Tests.Fixtures; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; namespace HealthChecks.UI.Tests; From 2ef0569281aa892c993fe9b72b5b77d8eaed58d7 Mon Sep 17 00:00:00 2001 From: Mono <81423605+monofunc@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:16:14 +0400 Subject: [PATCH 3/3] Fix flaky test --- .../Functional/Configuration/UIHttpMessageHandlerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/HealthChecks.UI.Tests/Functional/Configuration/UIHttpMessageHandlerTests.cs b/test/HealthChecks.UI.Tests/Functional/Configuration/UIHttpMessageHandlerTests.cs index fbda94d169..d5c2db3e4c 100644 --- a/test/HealthChecks.UI.Tests/Functional/Configuration/UIHttpMessageHandlerTests.cs +++ b/test/HealthChecks.UI.Tests/Functional/Configuration/UIHttpMessageHandlerTests.cs @@ -101,7 +101,7 @@ public Task configure_webhooks_endpoint_custom_delegating_handlers() setup.UseWebHooksEndpointDelegatingHandler(); setup.UseWebHooksEndpointDelegatingHandler(); - }).AddInMemoryStorage(); + }).AddInMemoryStorage(databaseName: Guid.NewGuid().ToString()); }) .Configure(app => {