From e623c9a10bfa2a77dfea2d33a4fd788017d53f07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:10:48 +0000 Subject: [PATCH 01/14] feat: Add GCP Firestore health check implementation with tests Co-authored-by: Hnogared <133124217+Hnogared@users.noreply.github.com> --- Directory.Packages.props | 1 + GitVersion.yml | 10 +- HealthChecks.slnx | 1 + .../DependencyInjectionExtensions.cs | 48 +++++++ .../FirestoreHealthCheck.cs | 41 ++++++ .../FirestoreOptions.cs | 23 ++++ .../FirestoreOptionsConfigure.cs | 30 +++++ ...etEvolve.HealthChecks.GCP.Firestore.csproj | 14 ++ .../README.md | 74 ++++++++++ .../HealthCheckArchitecture.cs | 1 + .../GCP/Firestore/FirestoreDatabase.cs | 19 +++ .../Firestore/FirestoreHealthCheckTests.cs | 126 ++++++++++++++++++ .../Internals/InstanceSharedType.cs | 2 + .../GCP/Firestore/FirestoreConfigureTests.cs | 55 ++++++++ .../Firestore/FirestoreHealthCheckTests.cs | 72 ++++++++++ .../GCP/Firestore/FirestoreOptionsTests.cs | 18 +++ 16 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 src/NetEvolve.HealthChecks.GCP.Firestore/DependencyInjectionExtensions.cs create mode 100644 src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreHealthCheck.cs create mode 100644 src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptions.cs create mode 100644 src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptionsConfigure.cs create mode 100644 src/NetEvolve.HealthChecks.GCP.Firestore/NetEvolve.HealthChecks.GCP.Firestore.csproj create mode 100644 src/NetEvolve.HealthChecks.GCP.Firestore/README.md create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs create mode 100644 tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs create mode 100644 tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreHealthCheckTests.cs create mode 100644 tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 39c528323..517d0dfa1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,6 +41,7 @@ + diff --git a/GitVersion.yml b/GitVersion.yml index c0b508d03..ed16160be 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,5 +1,5 @@ -mode: ManualDeployment -major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-,/\\\\]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)" -minor-version-bump-message: "^(feat)(\\([\\w\\s-,/\\\\]*\\))?:" -patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-,/\\\\]*\\))?:" -workflow: TrunkBased/preview1 +mode: ContinuousDelivery +branches: {} +ignore: + sha: [] +merge-message-formats: {} diff --git a/HealthChecks.slnx b/HealthChecks.slnx index cd3011c12..d84fa953f 100644 --- a/HealthChecks.slnx +++ b/HealthChecks.slnx @@ -47,6 +47,7 @@ + diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/DependencyInjectionExtensions.cs b/src/NetEvolve.HealthChecks.GCP.Firestore/DependencyInjectionExtensions.cs new file mode 100644 index 000000000..e463aa92a --- /dev/null +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/DependencyInjectionExtensions.cs @@ -0,0 +1,48 @@ +namespace NetEvolve.HealthChecks.GCP.Firestore; + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using NetEvolve.Arguments; + +/// +/// Extension methods for registering Firestore health checks. +/// +public static class DependencyInjectionExtensions +{ + private static readonly string[] _defaultTags = ["firestore", "gcp"]; + + /// + /// Adds a health check for Google Cloud Firestore. + /// + /// The . + /// The name of the health check. + /// An optional action to configure the . + /// An optional list of tags to associate with the health check. + /// The so that additional calls can be chained. + /// Thrown when or is . + public static IHealthChecksBuilder AddFirestore( + [NotNull] this IHealthChecksBuilder builder, + [NotNull] string name, + Action? options = null, + params string[] tags + ) + { + Argument.ThrowIfNull(builder); + Argument.ThrowIfNullOrWhiteSpace(name); + + if (options is not null) + { + _ = builder.Services.Configure(name, options); + } + + return builder + .AddCheck( + name, + failureStatus: HealthStatus.Unhealthy, + tags: [.. _defaultTags, .. tags] + ) + .ConfigureOptionsService(); + } +} diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreHealthCheck.cs b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreHealthCheck.cs new file mode 100644 index 000000000..66ed97b1a --- /dev/null +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreHealthCheck.cs @@ -0,0 +1,41 @@ +namespace NetEvolve.HealthChecks.GCP.Firestore; + +using System; +using System.Threading; +using System.Threading.Tasks; +using global::Google.Cloud.Firestore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using NetEvolve.Extensions.Tasks; +using NetEvolve.HealthChecks.Abstractions; + +internal sealed class FirestoreHealthCheck : ConfigurableHealthCheckBase +{ + private readonly IServiceProvider _serviceProvider; + + public FirestoreHealthCheck(IOptionsMonitor optionsMonitor, IServiceProvider serviceProvider) + : base(optionsMonitor) => _serviceProvider = serviceProvider; + + protected override async ValueTask ExecuteHealthCheckAsync( + string name, + HealthStatus failureStatus, + FirestoreOptions options, + CancellationToken cancellationToken + ) + { + var client = string.IsNullOrWhiteSpace(options.KeyedService) + ? _serviceProvider.GetRequiredService() + : _serviceProvider.GetRequiredKeyedService(options.KeyedService); + + var (isValid, _) = await client + .ListRootCollectionsAsync() + .GetAsyncEnumerator(cancellationToken) + .MoveNextAsync() + .AsTask() + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + return HealthCheckState(isValid, name); + } +} diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptions.cs b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptions.cs new file mode 100644 index 000000000..6a7f9585a --- /dev/null +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptions.cs @@ -0,0 +1,23 @@ +namespace NetEvolve.HealthChecks.GCP.Firestore; + +/// +/// Options for +/// +public sealed record FirestoreOptions +{ + /// + /// Gets or sets the timeout in milliseconds to use when executing tasks against the Firestore database. + /// + /// + /// The timeout in milliseconds. Default value is 100 milliseconds. + /// + public int Timeout { get; set; } = 100; + + /// + /// Gets or sets the keyed service name for retrieving the instance. + /// + /// + /// The keyed service name, or null if using the default service registration. + /// + public string? KeyedService { get; set; } +} diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptionsConfigure.cs b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptionsConfigure.cs new file mode 100644 index 000000000..a029fc513 --- /dev/null +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptionsConfigure.cs @@ -0,0 +1,30 @@ +namespace NetEvolve.HealthChecks.GCP.Firestore; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +internal sealed class FirestoreOptionsConfigure + : IConfigureNamedOptions, + IPostConfigureOptions +{ + private readonly IConfiguration _configuration; + + public FirestoreOptionsConfigure(IConfiguration configuration) => _configuration = configuration; + + public void Configure(string? name, FirestoreOptions options) + { + ArgumentNullException.ThrowIfNull(name); + + _configuration.Bind($"HealthChecks:GCP:Firestore:{name}", options); + } + + public void Configure(FirestoreOptions options) => Configure(Options.DefaultName, options); + + public void PostConfigure(string? name, FirestoreOptions options) + { + if (options.Timeout < -1) + { + options.Timeout = -1; + } + } +} diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/NetEvolve.HealthChecks.GCP.Firestore.csproj b/src/NetEvolve.HealthChecks.GCP.Firestore/NetEvolve.HealthChecks.GCP.Firestore.csproj new file mode 100644 index 000000000..911e159d5 --- /dev/null +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/NetEvolve.HealthChecks.GCP.Firestore.csproj @@ -0,0 +1,14 @@ + + + $(_ProjectTargetFrameworks) + Contains HealthChecks for Google Cloud Platform Firestore, based on the nuget package `Google.Cloud.Firestore`. + $(PackageTags);gcp;google;firestore + + + + + + + + + diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/README.md b/src/NetEvolve.HealthChecks.GCP.Firestore/README.md new file mode 100644 index 000000000..904baa6e1 --- /dev/null +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/README.md @@ -0,0 +1,74 @@ +# NetEvolve.HealthChecks.GCP.Firestore + +[![NuGet](https://img.shields.io/nuget/v/NetEvolve.HealthChecks.GCP.Firestore?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.GCP.Firestore/) +[![NuGet](https://img.shields.io/nuget/dt/NetEvolve.HealthChecks.GCP.Firestore?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.GCP.Firestore/) + +This package provides a health check for Google Cloud Platform Firestore, based on the [Google.Cloud.Firestore](https://www.nuget.org/packages/Google.Cloud.Firestore/) package. The main purpose is to check if the Firestore database is available and accessible. + +:bulb: This package is available for .NET 8.0 and later. + +## Installation +To use this package, you need to add the package to your project. You can do this by using the NuGet package manager or by using the dotnet CLI. +```powershell +dotnet add package NetEvolve.HealthChecks.GCP.Firestore +``` + +## Health Check - Firestore Liveness +The health check is a liveness check. It checks if the Firestore database is available and accessible. +If the query needs longer than the configured timeout, the health check will return `Degraded`. +If the query fails, for whatever reason, the health check will return `Unhealthy`. + +### Usage +After adding the package, you need to import the namespace and add the health check to the health check builder. +```csharp +using NetEvolve.HealthChecks.GCP.Firestore; +``` +Therefore, you can use two different approaches. In both approaches you have to provide a name for the health check. + +### Parameters +- `name`: The name of the health check. The name is used to identify the configuration object. It is required and must be unique within the application. +- `options`: The configuration options for the health check. If you don't provide any options, the health check will use the configuration based approach. +- `tags`: The tags for the health check. The tags `firestore` and `gcp` are always used as default and combined with the user input. You can provide additional tags to group or filter the health checks. + +### Variant 1: Configuration based +The first one is to use the configuration based approach. This approach is recommended if you have multiple Firestore instances to check. +```csharp +var builder = services.AddHealthChecks(); + +builder.AddFirestore(""); +``` + +The configuration looks like this: +```json +{ + ..., // other configuration + "HealthChecks": { + "GCP": { + "Firestore": { + "": { + "Timeout": "" // optional, default is 100 milliseconds + } + } + } + } +} +``` + +### Variant 2: Builder based +The second approach is to use the builder based approach. This approach is recommended if you only have one Firestore instance to check or dynamic programmatic values. +```csharp +var builder = services.AddHealthChecks(); + +builder.AddFirestore("", options => +{ + options.Timeout = ""; // optional, default is 100 milliseconds +}); +``` + +### :bulb: You can always provide tags to all health checks, for grouping or filtering. + +```csharp +var builder = services.AddHealthChecks(); + +builder.AddFirestore("", options => ..., "firestore", "gcp"); +``` diff --git a/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs b/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs index bbbf2f3f6..22e1f5da7 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs @@ -44,6 +44,7 @@ private static Architecture LoadArchitecture() typeof(DuckDB.DuckDBHealthCheck).Assembly, typeof(Elasticsearch.ElasticsearchHealthCheck).Assembly, typeof(Firebird.FirebirdHealthCheck).Assembly, + typeof(GCP.Firestore.FirestoreHealthCheck).Assembly, typeof(Http.HttpHealthCheck).Assembly, typeof(Keycloak.KeycloakHealthCheck).Assembly, typeof(LiteDB.LiteDBHealthCheck).Assembly, diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs new file mode 100644 index 000000000..bf3aca7b0 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs @@ -0,0 +1,19 @@ +namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore; + +using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; +using Testcontainers.Firestore; +using TUnit.Core.Interfaces; + +public sealed class FirestoreDatabase : IAsyncInitializer, IAsyncDisposable +{ + private readonly FirestoreContainer _database = new FirestoreBuilder().WithLogger(NullLogger.Instance).Build(); + + public string ProjectId => _database.GetProjectId(); + + public string EmulatorHost => _database.GetEmulatorEndpoint(); + + public async ValueTask DisposeAsync() => await _database.DisposeAsync().ConfigureAwait(false); + + public async Task InitializeAsync() => await _database.StartAsync().ConfigureAwait(false); +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs new file mode 100644 index 000000000..a2170ca10 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs @@ -0,0 +1,126 @@ +namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using global::Google.Cloud.Firestore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.GCP.Firestore; + +[TestGroup(nameof(Firestore))] +[ClassDataSource(Shared = InstanceSharedType.Firestore)] +public sealed class FirestoreHealthCheckTests : HealthCheckTestBase +{ + private readonly FirestoreDatabase _database; + + public FirestoreHealthCheckTests(FirestoreDatabase database) => _database = database; + + [Test] + public async Task AddFirestore_UseOptions_Healthy() => + await RunAndVerify( + healthChecks => healthChecks.AddFirestore("TestContainerHealthy", options => options.Timeout = 10000), + HealthStatus.Healthy, + serviceBuilder: services => + { + Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); + _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); + } + ); + + [Test] + public async Task AddFirestore_UseOptions_Degraded() => + await RunAndVerify( + healthChecks => healthChecks.AddFirestore("TestContainerDegraded", options => options.Timeout = 0), + HealthStatus.Degraded, + serviceBuilder: services => + { + Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); + _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); + } + ); + + [Test] + public async Task AddFirestore_UseOptionsWithKeyedService_Healthy() => + await RunAndVerify( + healthChecks => + { + _ = healthChecks.AddFirestore( + "TestContainerKeyedServiceHealthy", + options => + { + options.Timeout = 10000; + options.KeyedService = "firestore"; + } + ); + }, + HealthStatus.Healthy, + serviceBuilder: services => + { + Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); + _ = services.AddKeyedSingleton("firestore", (_, _) => FirestoreDb.Create(_database.ProjectId)); + } + ); + + [Test] + public async Task AddFirestore_UseConfiguration_Healthy() => + await RunAndVerify( + healthChecks => healthChecks.AddFirestore("TestContainerHealthy"), + HealthStatus.Healthy, + config => + { + var values = new Dictionary(StringComparer.Ordinal) + { + { "HealthChecks:GCP:Firestore:TestContainerHealthy:Timeout", "10000" }, + }; + _ = config.AddInMemoryCollection(values); + }, + serviceBuilder: services => + { + Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); + _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); + } + ); + + [Test] + public async Task AddFirestore_UseConfiguration_Degraded() => + await RunAndVerify( + healthChecks => healthChecks.AddFirestore("TestContainerDegraded"), + HealthStatus.Degraded, + config => + { + var values = new Dictionary(StringComparer.Ordinal) + { + { "HealthChecks:GCP:Firestore:TestContainerDegraded:Timeout", "0" }, + }; + _ = config.AddInMemoryCollection(values); + }, + serviceBuilder: services => + { + Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); + _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); + } + ); + + [Test] + public async Task AddFirestore_UseConfiguration_TimeoutMinusTwo_ThrowException() => + await RunAndVerify( + healthChecks => healthChecks.AddFirestore("TestNoValues"), + HealthStatus.Degraded, + config => + { + var values = new Dictionary(StringComparer.Ordinal) + { + { "HealthChecks:GCP:Firestore:TestNoValues:Timeout", "-2" }, + }; + _ = config.AddInMemoryCollection(values); + }, + serviceBuilder: services => + { + Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); + _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); + } + ); +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/Internals/InstanceSharedType.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/Internals/InstanceSharedType.cs index bf6b53930..9c8b85152 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/Internals/InstanceSharedType.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/Internals/InstanceSharedType.cs @@ -29,6 +29,8 @@ internal static class InstanceSharedType public const SharedType Firebird = SharedType.PerClass; + public const SharedType Firestore = SharedType.PerClass; + public const SharedType Kafka = SharedType.PerClass; public const SharedType Keycloak = SharedType.PerClass; diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs new file mode 100644 index 000000000..8b53a0d77 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs @@ -0,0 +1,55 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; + +using System; +using Microsoft.Extensions.Configuration; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.GCP.Firestore; + +[TestGroup(nameof(Firestore))] +public sealed class FirestoreConfigureTests +{ + [Test] + public void Configure_WhenArgumentNameNull_ThrowArgumentNullException() + { + // Arrange + var configure = new FirestoreOptionsConfigure(new ConfigurationBuilder().Build()); + const string? name = default; + var options = new FirestoreOptions(); + + // Act + void Act() => configure.Configure(name, options); + + // Assert + _ = Assert.Throws("name", Act); + } + + [Test] + public async Task PostConfigure_WhenTimeoutLessThanInfinite_SetToInfinite() + { + // Arrange + var configure = new FirestoreOptionsConfigure(new ConfigurationBuilder().Build()); + const string? name = "Test"; + var options = new FirestoreOptions { Timeout = -2 }; + + // Act + configure.PostConfigure(name, options); + + // Assert + _ = await Assert.That(options.Timeout).IsEqualTo(-1); + } + + [Test] + public async Task PostConfigure_WhenTimeoutValid_DoNotChange() + { + // Arrange + var configure = new FirestoreOptionsConfigure(new ConfigurationBuilder().Build()); + const string? name = "Test"; + var options = new FirestoreOptions { Timeout = 100 }; + + // Act + configure.PostConfigure(name, options); + + // Assert + _ = await Assert.That(options.Timeout).IsEqualTo(100); + } +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreHealthCheckTests.cs new file mode 100644 index 000000000..2c03d3898 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreHealthCheckTests.cs @@ -0,0 +1,72 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; + +using System; +using System.Threading; +using System.Threading.Tasks; +using global::Google.Cloud.Firestore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.GCP.Firestore; +using NSubstitute; + +[TestGroup(nameof(Firestore))] +public sealed class FirestoreHealthCheckTests +{ + [Test] + public async Task CheckHealthAsync_WhenContextNull_ThrowArgumentNullException() + { + // Arrange + var optionsMonitor = Substitute.For>(); + var serviceProvider = Substitute.For(); + var check = new FirestoreHealthCheck(optionsMonitor, serviceProvider); + + // Act + async Task Act() => _ = await check.CheckHealthAsync(null!, default); + + // Assert + _ = await Assert.ThrowsAsync("context", Act); + } + + [Test] + public async Task CheckHealthAsync_WhenCancellationTokenIsCancelled_ShouldReturnUnhealthy() + { + // Arrange + var optionsMonitor = Substitute.For>(); + var serviceProvider = Substitute.For(); + var check = new FirestoreHealthCheck(optionsMonitor, serviceProvider); + var context = new HealthCheckContext { Registration = new HealthCheckRegistration("Test", check, null, null) }; + var cancellationToken = new CancellationToken(true); + + // Act + var result = await check.CheckHealthAsync(context, cancellationToken); + + // Assert + using (Assert.Multiple()) + { + _ = await Assert.That(result.Status).IsEqualTo(HealthStatus.Unhealthy); + _ = await Assert.That(result.Description).IsEqualTo("Test: Cancellation requested."); + } + } + + [Test] + public async Task CheckHealthAsync_WhenOptionsAreNull_ShouldReturnUnhealthy() + { + // Arrange + var optionsMonitor = Substitute.For>(); + var serviceProvider = Substitute.For(); + var check = new FirestoreHealthCheck(optionsMonitor, serviceProvider); + var context = new HealthCheckContext { Registration = new HealthCheckRegistration("Test", check, null, null) }; + + // Act + var result = await check.CheckHealthAsync(context); + + // Assert + using (Assert.Multiple()) + { + _ = await Assert.That(result.Status).IsEqualTo(HealthStatus.Unhealthy); + _ = await Assert.That(result.Description).IsEqualTo("Test: Missing configuration."); + } + } +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs new file mode 100644 index 000000000..7ce321818 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs @@ -0,0 +1,18 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; + +using System.Threading.Tasks; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.GCP.Firestore; + +[TestGroup(nameof(Firestore))] +public sealed class FirestoreOptionsTests +{ + [Test] + public async Task Options_NotSame_Expected() + { + var options1 = new FirestoreOptions { Timeout = 100 }; + var options2 = options1 with { }; + + _ = await Assert.That(options1).IsEqualTo(options2).And.IsNotSameReferenceAs(options2); + } +} From c74220f9168cdab1fa95489944b9d369b764779c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:14:22 +0000 Subject: [PATCH 02/14] fix: Revert accidental GitVersion.yml modification Co-authored-by: Hnogared <133124217+Hnogared@users.noreply.github.com> --- GitVersion.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/GitVersion.yml b/GitVersion.yml index ed16160be..c0b508d03 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,5 +1,5 @@ -mode: ContinuousDelivery -branches: {} -ignore: - sha: [] -merge-message-formats: {} +mode: ManualDeployment +major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-,/\\\\]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)" +minor-version-bump-message: "^(feat)(\\([\\w\\s-,/\\\\]*\\))?:" +patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-,/\\\\]*\\))?:" +workflow: TrunkBased/preview1 From 924aa74832a772a9e1991e2e453457c8c024aa74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sun, 26 Oct 2025 10:49:00 +0100 Subject: [PATCH 03/14] fix(test): add missing package references and update API usage --- Directory.Packages.props | 1 + .../DependencyInjectionExtensions.cs | 37 +++++++++++++------ .../FirestoreHealthCheck.cs | 13 ++++--- .../GCP/Firestore/FirestoreDatabase.cs | 10 ++++- ...olve.HealthChecks.Tests.Integration.csproj | 3 ++ 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 517d0dfa1..f7dd25b23 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -85,6 +85,7 @@ + diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/DependencyInjectionExtensions.cs b/src/NetEvolve.HealthChecks.GCP.Firestore/DependencyInjectionExtensions.cs index e463aa92a..9a4c2c3fa 100644 --- a/src/NetEvolve.HealthChecks.GCP.Firestore/DependencyInjectionExtensions.cs +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/DependencyInjectionExtensions.cs @@ -2,9 +2,10 @@ namespace NetEvolve.HealthChecks.GCP.Firestore; using System; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; -using NetEvolve.Arguments; +using NetEvolve.HealthChecks.Abstractions; /// /// Extension methods for registering Firestore health checks. @@ -21,7 +22,10 @@ public static class DependencyInjectionExtensions /// An optional action to configure the . /// An optional list of tags to associate with the health check. /// The so that additional calls can be chained. - /// Thrown when or is . + /// Thrown when is . + /// Thrown when is or empty. + /// Thrown when is already in use. + /// Thrown when is . public static IHealthChecksBuilder AddFirestore( [NotNull] this IHealthChecksBuilder builder, [NotNull] string name, @@ -29,20 +33,31 @@ public static IHealthChecksBuilder AddFirestore( params string[] tags ) { - Argument.ThrowIfNull(builder); - Argument.ThrowIfNullOrWhiteSpace(name); + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + ArgumentNullException.ThrowIfNull(tags); + + if (!builder.IsServiceTypeRegistered()) + { + _ = builder + .Services.AddSingleton() + .AddSingleton() + .ConfigureOptions(); + } + + builder.ThrowIfNameIsAlreadyUsed(name); if (options is not null) { _ = builder.Services.Configure(name, options); } - return builder - .AddCheck( - name, - failureStatus: HealthStatus.Unhealthy, - tags: [.. _defaultTags, .. tags] - ) - .ConfigureOptionsService(); + return builder.AddCheck( + name, + HealthStatus.Unhealthy, + _defaultTags.Union(tags, StringComparer.OrdinalIgnoreCase) + ); } + + private sealed partial class FirestoreHealthCheckMarker; } diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreHealthCheck.cs b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreHealthCheck.cs index 66ed97b1a..326aa5011 100644 --- a/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreHealthCheck.cs +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreHealthCheck.cs @@ -28,11 +28,14 @@ CancellationToken cancellationToken ? _serviceProvider.GetRequiredService() : _serviceProvider.GetRequiredKeyedService(options.KeyedService); - var (isValid, _) = await client - .ListRootCollectionsAsync() - .GetAsyncEnumerator(cancellationToken) - .MoveNextAsync() - .AsTask() + // Use a simple operation to check if the database is accessible + // Creating a document reference is a local operation that validates the client is configured + var testCollection = client.Collection("healthcheck"); + var testDoc = testCollection.Document("test"); + + // Attempt a lightweight operation with timeout + var (isValid, _) = await testDoc + .GetSnapshotAsync(cancellationToken) .WithTimeoutAsync(options.Timeout, cancellationToken) .ConfigureAwait(false); diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs index bf3aca7b0..5f020544a 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs @@ -1,15 +1,21 @@ namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore; using System.Threading.Tasks; +using DotNet.Testcontainers.Builders; using Microsoft.Extensions.Logging.Abstractions; using Testcontainers.Firestore; using TUnit.Core.Interfaces; public sealed class FirestoreDatabase : IAsyncInitializer, IAsyncDisposable { - private readonly FirestoreContainer _database = new FirestoreBuilder().WithLogger(NullLogger.Instance).Build(); + private readonly FirestoreContainer _database = new FirestoreBuilder() + .WithLogger(NullLogger.Instance) + .WithWaitStrategy( + Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPath("/").ForPort(8080)) + ) + .Build(); - public string ProjectId => _database.GetProjectId(); + public string ProjectId => "test-project"; public string EmulatorHost => _database.GetEmulatorEndpoint(); diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj b/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj index 98c4142b7..30512faf8 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj @@ -25,6 +25,7 @@ + @@ -69,6 +70,7 @@ + @@ -117,6 +119,7 @@ + From ff47f3343c0ab43c7e157b9d31f75b5cf8012e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 15:07:36 +0100 Subject: [PATCH 04/14] chore: Updated README.md --- src/NetEvolve.HealthChecks.GCP.Firestore/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/README.md b/src/NetEvolve.HealthChecks.GCP.Firestore/README.md index 904baa6e1..40364658e 100644 --- a/src/NetEvolve.HealthChecks.GCP.Firestore/README.md +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/README.md @@ -46,7 +46,7 @@ The configuration looks like this: "GCP": { "Firestore": { "": { - "Timeout": "" // optional, default is 100 milliseconds + "Timeout": // optional, default is 100 milliseconds } } } @@ -61,7 +61,7 @@ var builder = services.AddHealthChecks(); builder.AddFirestore("", options => { - options.Timeout = ""; // optional, default is 100 milliseconds + options.Timeout = ; // optional, default is 100 milliseconds }); ``` @@ -72,3 +72,7 @@ var builder = services.AddHealthChecks(); builder.AddFirestore("", options => ..., "firestore", "gcp"); ``` + +## License + +This project is licensed under the MIT License - see the [LICENSE](https://raw.githubusercontent.com/dailydevops/healthchecks/refs/heads/main/LICENSE) file for details. From fdd2a4c436198efe61c0eac0e0699d749711fc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 16:35:01 +0100 Subject: [PATCH 05/14] fix: Add missing package references for unit tests and simplify Firestore container setup --- .../GCP/Firestore/FirestoreDatabase.cs | 44 ++++++++++++++----- .../Firestore/FirestoreHealthCheckTests.cs | 37 +++------------- .../NetEvolve.HealthChecks.Tests.Unit.csproj | 2 + 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs index 5f020544a..591770906 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs @@ -1,25 +1,47 @@ namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore; +using System; using System.Threading.Tasks; -using DotNet.Testcontainers.Builders; +using global::Google.Cloud.Firestore; +using global::Google.Cloud.Firestore.V1; +using global::Grpc.Core; using Microsoft.Extensions.Logging.Abstractions; using Testcontainers.Firestore; using TUnit.Core.Interfaces; public sealed class FirestoreDatabase : IAsyncInitializer, IAsyncDisposable { - private readonly FirestoreContainer _database = new FirestoreBuilder() - .WithLogger(NullLogger.Instance) - .WithWaitStrategy( - Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPath("/").ForPort(8080)) - ) - .Build(); + private readonly FirestoreContainer _container = new FirestoreBuilder().WithLogger(NullLogger.Instance).Build(); - public string ProjectId => "test-project"; + private FirestoreClient? _client; + private FirestoreDb? _database; - public string EmulatorHost => _database.GetEmulatorEndpoint(); + public const string ProjectId = "test-project"; - public async ValueTask DisposeAsync() => await _database.DisposeAsync().ConfigureAwait(false); + public FirestoreDb Database => _database ?? throw new InvalidOperationException("Database not initialized"); - public async Task InitializeAsync() => await _database.StartAsync().ConfigureAwait(false); + public async ValueTask DisposeAsync() + { + await _container.DisposeAsync().ConfigureAwait(false); + } + + public async Task InitializeAsync() + { + await _container.StartAsync().ConfigureAwait(false); + + // Parse endpoint to get host:port + var fullEndpoint = _container.GetEmulatorEndpoint(); + var uri = new Uri(fullEndpoint); + var hostPort = $"{uri.Host}:{uri.Port}"; + + // Create Firestore client configured for emulator + var clientBuilder = new FirestoreClientBuilder + { + Endpoint = hostPort, + ChannelCredentials = ChannelCredentials.Insecure, + }; + + _client = await clientBuilder.BuildAsync().ConfigureAwait(false); + _database = await FirestoreDb.CreateAsync(ProjectId, _client).ConfigureAwait(false); + } } diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs index a2170ca10..211bdf46e 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs @@ -3,7 +3,6 @@ namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore; using System; using System.Collections.Generic; using System.Threading.Tasks; -using global::Google.Cloud.Firestore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -23,11 +22,7 @@ public async Task AddFirestore_UseOptions_Healthy() => await RunAndVerify( healthChecks => healthChecks.AddFirestore("TestContainerHealthy", options => options.Timeout = 10000), HealthStatus.Healthy, - serviceBuilder: services => - { - Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); - _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); - } + serviceBuilder: services => _ = services.AddSingleton(_ => _database.Database) ); [Test] @@ -35,11 +30,7 @@ public async Task AddFirestore_UseOptions_Degraded() => await RunAndVerify( healthChecks => healthChecks.AddFirestore("TestContainerDegraded", options => options.Timeout = 0), HealthStatus.Degraded, - serviceBuilder: services => - { - Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); - _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); - } + serviceBuilder: services => _ = services.AddSingleton(_ => _database.Database) ); [Test] @@ -57,11 +48,7 @@ await RunAndVerify( ); }, HealthStatus.Healthy, - serviceBuilder: services => - { - Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); - _ = services.AddKeyedSingleton("firestore", (_, _) => FirestoreDb.Create(_database.ProjectId)); - } + serviceBuilder: services => _ = services.AddKeyedSingleton("firestore", (_, _) => _database.Database) ); [Test] @@ -77,11 +64,7 @@ await RunAndVerify( }; _ = config.AddInMemoryCollection(values); }, - serviceBuilder: services => - { - Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); - _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); - } + serviceBuilder: services => _ = services.AddSingleton(_ => _database.Database) ); [Test] @@ -97,11 +80,7 @@ await RunAndVerify( }; _ = config.AddInMemoryCollection(values); }, - serviceBuilder: services => - { - Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); - _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); - } + serviceBuilder: services => _ = services.AddSingleton(_ => _database.Database) ); [Test] @@ -117,10 +96,6 @@ await RunAndVerify( }; _ = config.AddInMemoryCollection(values); }, - serviceBuilder: services => - { - Environment.SetEnvironmentVariable("FIRESTORE_EMULATOR_HOST", _database.EmulatorHost); - _ = services.AddSingleton(_ => FirestoreDb.Create(_database.ProjectId)); - } + serviceBuilder: services => _ = services.AddSingleton(_ => _database.Database) ); } diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/NetEvolve.HealthChecks.Tests.Unit.csproj b/tests/NetEvolve.HealthChecks.Tests.Unit/NetEvolve.HealthChecks.Tests.Unit.csproj index f713e0a2d..2e25707fb 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Unit/NetEvolve.HealthChecks.Tests.Unit.csproj +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/NetEvolve.HealthChecks.Tests.Unit.csproj @@ -10,6 +10,7 @@ all + @@ -41,6 +42,7 @@ + From 0a23c63d108323c51013c1937b19748c4092fca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 16:39:46 +0100 Subject: [PATCH 06/14] fix: Missing ProjectReference --- .../NetEvolve.HealthChecks.Tests.Architecture.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj b/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj index e29fd8684..426698130 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj +++ b/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj @@ -36,6 +36,7 @@ + From ba33a8c46eee548883fca79f3fbb2a7ca74d8981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 16:48:50 +0100 Subject: [PATCH 07/14] fix: Code fixes --- .../GCP/Firestore/FirestoreDatabase.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs index 591770906..2a1d8d842 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore; +namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore; using System; using System.Threading.Tasks; @@ -13,17 +13,13 @@ public sealed class FirestoreDatabase : IAsyncInitializer, IAsyncDisposable { private readonly FirestoreContainer _container = new FirestoreBuilder().WithLogger(NullLogger.Instance).Build(); - private FirestoreClient? _client; private FirestoreDb? _database; public const string ProjectId = "test-project"; public FirestoreDb Database => _database ?? throw new InvalidOperationException("Database not initialized"); - public async ValueTask DisposeAsync() - { - await _container.DisposeAsync().ConfigureAwait(false); - } + public async ValueTask DisposeAsync() => await _container.DisposeAsync().ConfigureAwait(false); public async Task InitializeAsync() { @@ -41,7 +37,7 @@ public async Task InitializeAsync() ChannelCredentials = ChannelCredentials.Insecure, }; - _client = await clientBuilder.BuildAsync().ConfigureAwait(false); - _database = await FirestoreDb.CreateAsync(ProjectId, _client).ConfigureAwait(false); + var client = await clientBuilder.BuildAsync().ConfigureAwait(false); + _database = await FirestoreDb.CreateAsync(ProjectId, client).ConfigureAwait(false); } } From 305ef8b37932ef098b20e1326ecf6920363774b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 16:52:43 +0100 Subject: [PATCH 08/14] fix: Namespace --- .../{GCP/Firestore => GCP.Firestore}/FirestoreDatabase.cs | 0 .../{GCP/Firestore => GCP.Firestore}/FirestoreHealthCheckTests.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/NetEvolve.HealthChecks.Tests.Integration/{GCP/Firestore => GCP.Firestore}/FirestoreDatabase.cs (100%) rename tests/NetEvolve.HealthChecks.Tests.Integration/{GCP/Firestore => GCP.Firestore}/FirestoreHealthCheckTests.cs (100%) diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreDatabase.cs similarity index 100% rename from tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreDatabase.cs rename to tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreDatabase.cs diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs similarity index 100% rename from tests/NetEvolve.HealthChecks.Tests.Integration/GCP/Firestore/FirestoreHealthCheckTests.cs rename to tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs From 59c27aec761dcb4f09cef2dda010377efdad2465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 16:58:10 +0100 Subject: [PATCH 09/14] test: Updated `NetEvolve.HealthChecks.GCP.Firestore` tests --- .../FirestoreOptionsConfigure.cs | 23 ++++++++++--- .../FirestoreHealthCheckTests.cs | 4 +-- ...ore_UseConfiguration_Degraded.verified.txt | 14 ++++++++ ...tore_UseConfiguration_Healthy.verified.txt | 14 ++++++++ ...imeoutMinusTwo_ThrowException.verified.txt | 18 +++++++++++ ...tionsWithKeyedService_Healthy.verified.txt | 14 ++++++++ ...Firestore_UseOptions_Degraded.verified.txt | 14 ++++++++ ...dFirestore_UseOptions_Healthy.verified.txt | 14 ++++++++ .../GCP/Firestore/FirestoreConfigureTests.cs | 32 +------------------ 9 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_Degraded.verified.txt create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_Healthy.verified.txt create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_TimeoutMinusTwo_ThrowException.verified.txt create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptionsWithKeyedService_Healthy.verified.txt create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptions_Degraded.verified.txt create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptions_Healthy.verified.txt diff --git a/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptionsConfigure.cs b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptionsConfigure.cs index a029fc513..5f4be7783 100644 --- a/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptionsConfigure.cs +++ b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptionsConfigure.cs @@ -1,11 +1,12 @@ -namespace NetEvolve.HealthChecks.GCP.Firestore; +namespace NetEvolve.HealthChecks.GCP.Firestore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; +using static Microsoft.Extensions.Options.ValidateOptionsResult; internal sealed class FirestoreOptionsConfigure : IConfigureNamedOptions, - IPostConfigureOptions + IValidateOptions { private readonly IConfiguration _configuration; @@ -20,11 +21,23 @@ public void Configure(string? name, FirestoreOptions options) public void Configure(FirestoreOptions options) => Configure(Options.DefaultName, options); - public void PostConfigure(string? name, FirestoreOptions options) + public ValidateOptionsResult Validate(string? name, FirestoreOptions options) { - if (options.Timeout < -1) + if (string.IsNullOrWhiteSpace(name)) { - options.Timeout = -1; + return Fail("The name cannot be null or whitespace."); } + + if (options is null) + { + return Fail("The option cannot be null."); + } + + if (options.Timeout < Timeout.Infinite) + { + return Fail("The timeout value must be a positive number in milliseconds or -1 for an infinite timeout."); + } + + return Success; } } diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs index 211bdf46e..c7ff91479 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore; +namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore; using System; using System.Collections.Generic; @@ -87,7 +87,7 @@ await RunAndVerify( public async Task AddFirestore_UseConfiguration_TimeoutMinusTwo_ThrowException() => await RunAndVerify( healthChecks => healthChecks.AddFirestore("TestNoValues"), - HealthStatus.Degraded, + HealthStatus.Unhealthy, config => { var values = new Dictionary(StringComparer.Ordinal) diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_Degraded.verified.txt b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_Degraded.verified.txt new file mode 100644 index 000000000..4d60a96a1 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_Degraded.verified.txt @@ -0,0 +1,14 @@ +{ + results: [ + { + description: TestContainerDegraded: Degraded, + name: TestContainerDegraded, + status: Degraded, + tags: [ + firestore, + gcp + ] + } + ], + status: Degraded +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_Healthy.verified.txt b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_Healthy.verified.txt new file mode 100644 index 000000000..08ac64c96 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_Healthy.verified.txt @@ -0,0 +1,14 @@ +{ + results: [ + { + description: TestContainerHealthy: Healthy, + name: TestContainerHealthy, + status: Healthy, + tags: [ + firestore, + gcp + ] + } + ], + status: Healthy +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_TimeoutMinusTwo_ThrowException.verified.txt b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_TimeoutMinusTwo_ThrowException.verified.txt new file mode 100644 index 000000000..d5cb53dd2 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseConfiguration_TimeoutMinusTwo_ThrowException.verified.txt @@ -0,0 +1,18 @@ +{ + results: [ + { + description: TestNoValues: Unexpected error., + exception: { + message: The timeout value must be a positive number in milliseconds or -1 for an infinite timeout., + type: Microsoft.Extensions.Options.OptionsValidationException + }, + name: TestNoValues, + status: Unhealthy, + tags: [ + firestore, + gcp + ] + } + ], + status: Unhealthy +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptionsWithKeyedService_Healthy.verified.txt b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptionsWithKeyedService_Healthy.verified.txt new file mode 100644 index 000000000..05603a958 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptionsWithKeyedService_Healthy.verified.txt @@ -0,0 +1,14 @@ +{ + results: [ + { + description: TestContainerKeyedServiceHealthy: Healthy, + name: TestContainerKeyedServiceHealthy, + status: Healthy, + tags: [ + firestore, + gcp + ] + } + ], + status: Healthy +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptions_Degraded.verified.txt b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptions_Degraded.verified.txt new file mode 100644 index 000000000..4d60a96a1 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptions_Degraded.verified.txt @@ -0,0 +1,14 @@ +{ + results: [ + { + description: TestContainerDegraded: Degraded, + name: TestContainerDegraded, + status: Degraded, + tags: [ + firestore, + gcp + ] + } + ], + status: Degraded +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptions_Healthy.verified.txt b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptions_Healthy.verified.txt new file mode 100644 index 000000000..08ac64c96 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/FirestoreHealthCheck.AddFirestore_UseOptions_Healthy.verified.txt @@ -0,0 +1,14 @@ +{ + results: [ + { + description: TestContainerHealthy: Healthy, + name: TestContainerHealthy, + status: Healthy, + tags: [ + firestore, + gcp + ] + } + ], + status: Healthy +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs index 8b53a0d77..f74593335 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; +namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; using System; using Microsoft.Extensions.Configuration; @@ -22,34 +22,4 @@ public void Configure_WhenArgumentNameNull_ThrowArgumentNullException() // Assert _ = Assert.Throws("name", Act); } - - [Test] - public async Task PostConfigure_WhenTimeoutLessThanInfinite_SetToInfinite() - { - // Arrange - var configure = new FirestoreOptionsConfigure(new ConfigurationBuilder().Build()); - const string? name = "Test"; - var options = new FirestoreOptions { Timeout = -2 }; - - // Act - configure.PostConfigure(name, options); - - // Assert - _ = await Assert.That(options.Timeout).IsEqualTo(-1); - } - - [Test] - public async Task PostConfigure_WhenTimeoutValid_DoNotChange() - { - // Arrange - var configure = new FirestoreOptionsConfigure(new ConfigurationBuilder().Build()); - const string? name = "Test"; - var options = new FirestoreOptions { Timeout = 100 }; - - // Act - configure.PostConfigure(name, options); - - // Assert - _ = await Assert.That(options.Timeout).IsEqualTo(100); - } } From 3bf37ed387cfa8ca9e9fd69a7d888cbaf353fdb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 20:07:53 +0100 Subject: [PATCH 10/14] test: Extend unit test coverage for Firestore health check --- .../DependencyInjectionExtensionsTests.cs | 170 ++++++++++++++++++ .../GCP/Firestore/FirestoreConfigureTests.cs | 128 +++++++++++++ .../GCP/Firestore/FirestoreOptionsTests.cs | 47 +++++ 3 files changed, 345 insertions(+) create mode 100644 tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/DependencyInjectionExtensionsTests.cs diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/DependencyInjectionExtensionsTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/DependencyInjectionExtensionsTests.cs new file mode 100644 index 000000000..6b2d616a3 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/DependencyInjectionExtensionsTests.cs @@ -0,0 +1,170 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.GCP.Firestore; + +[TestGroup(nameof(Firestore))] +public sealed class DependencyInjectionExtensionsTests +{ + [Test] + public void AddFirestore_WhenArgumentBuilderNull_ThrowArgumentNullException() + { + // Arrange + IHealthChecksBuilder builder = null!; + const string name = "Test"; + + // Act & Assert + _ = Assert.Throws(nameof(builder), () => builder.AddFirestore(name)); + } + + [Test] + public void AddFirestore_WhenArgumentNameNull_ThrowArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + var builder = services.AddHealthChecks(); + const string name = null!; + + // Act & Assert + _ = Assert.Throws(nameof(name), () => builder.AddFirestore(name)); + } + + [Test] + public void AddFirestore_WhenArgumentNameEmpty_ThrowArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var builder = services.AddHealthChecks(); + const string name = ""; + + // Act & Assert + _ = Assert.Throws(nameof(name), () => builder.AddFirestore(name)); + } + + [Test] + public void AddFirestore_WhenArgumentTagsNull_ThrowArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + var builder = services.AddHealthChecks(); + const string name = "Test"; + + // Act & Assert + _ = Assert.Throws("tags", () => builder.AddFirestore(name, options: null, tags: null!)); + } + + [Test] + public async Task AddFirestore_WhenParameterCorrect_RegistrationAdded() + { + // Arrange + var services = new ServiceCollection(); + _ = services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection([]).Build()); + var builder = services.AddHealthChecks(); + const string name = "Test"; + + // Act + _ = builder.AddFirestore(name); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var options = + serviceProvider.GetRequiredService>(); + var registration = options.Value.Registrations.FirstOrDefault(r => r.Name == name); + + _ = await Assert.That(registration).IsNotNull(); + } + + [Test] + public async Task AddFirestore_WhenParameterCorrectWithOptions_RegistrationAdded() + { + // Arrange + var services = new ServiceCollection(); + _ = services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection([]).Build()); + var builder = services.AddHealthChecks(); + const string name = "Test"; + + // Act + _ = builder.AddFirestore( + name, + options => + { + options.Timeout = 10000; + options.KeyedService = "firestore"; + } + ); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var options = + serviceProvider.GetRequiredService>(); + var registration = options.Value.Registrations.FirstOrDefault(r => r.Name == name); + + _ = await Assert.That(registration).IsNotNull(); + } + + [Test] + public void AddFirestore_WhenDuplicateName_ThrowArgumentException() + { + // Arrange + var services = new ServiceCollection(); + _ = services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection([]).Build()); + var builder = services.AddHealthChecks(); + const string name = "Test"; + + _ = builder.AddFirestore(name); + + // Act & Assert + _ = Assert.Throws(nameof(name), () => builder.AddFirestore(name)); + } + + [Test] + public async Task AddFirestore_WhenMultipleChecksWithDifferentNames_AllRegistrationsAdded() + { + // Arrange + var services = new ServiceCollection(); + _ = services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection([]).Build()); + var builder = services.AddHealthChecks(); + + // Act + _ = builder.AddFirestore("Test1"); + _ = builder.AddFirestore("Test2"); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var options = + serviceProvider.GetRequiredService>(); + + _ = await Assert.That(options.Value.Registrations.Any(r => r.Name == "Test1")).IsTrue(); + _ = await Assert.That(options.Value.Registrations.Any(r => r.Name == "Test2")).IsTrue(); + } + + [Test] + public async Task AddFirestore_WhenCustomTags_TagsApplied() + { + // Arrange + var services = new ServiceCollection(); + _ = services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection([]).Build()); + var builder = services.AddHealthChecks(); + const string name = "Test"; + const string customTag = "custom-tag"; + + // Act + _ = builder.AddFirestore(name, tags: customTag); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var options = + serviceProvider.GetRequiredService>(); + var registration = options.Value.Registrations.First(r => r.Name == name); + + _ = await Assert.That(registration.Tags.Contains(customTag)).IsTrue(); + _ = await Assert.That(registration.Tags.Contains("firestore")).IsTrue(); + _ = await Assert.That(registration.Tags.Contains("gcp")).IsTrue(); + } +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs index f74593335..15f30d59b 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs @@ -1,6 +1,8 @@ namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using NetEvolve.Extensions.TUnit; using NetEvolve.HealthChecks.GCP.Firestore; @@ -22,4 +24,130 @@ public void Configure_WhenArgumentNameNull_ThrowArgumentNullException() // Assert _ = Assert.Throws("name", Act); } + + [Test] + public async Task Configure_WhenConfigurationValid_OptionsConfigured() + { + // Arrange + var config = new ConfigurationBuilder() + .AddInMemoryCollection( + new Dictionary + { + { "HealthChecks:GCP:Firestore:Test:Timeout", "5000" }, + { "HealthChecks:GCP:Firestore:Test:KeyedService", "my-firestore" }, + } + ) + .Build(); + + var configure = new FirestoreOptionsConfigure(config); + var options = new FirestoreOptions(); + + // Act + configure.Configure("Test", options); + + // Assert + using (Assert.Multiple()) + { + _ = await Assert.That(options.Timeout).IsEqualTo(5000); + _ = await Assert.That(options.KeyedService).IsEqualTo("my-firestore"); + } + } + + [Test] + public async Task Configure_WhenConfigurationMissing_DefaultValuesUsed() + { + // Arrange + var config = new ConfigurationBuilder().Build(); + var configure = new FirestoreOptionsConfigure(config); + var options = new FirestoreOptions(); + + // Act + configure.Configure("Test", options); + + // Assert + _ = await Assert.That(options.Timeout).IsEqualTo(100); + } + + [Test] + public async Task Configure_WhenTimeoutNegative_ValueSet() + { + // Arrange + var config = new ConfigurationBuilder() + .AddInMemoryCollection( + new Dictionary { { "HealthChecks:GCP:Firestore:Test:Timeout", "-1" } } + ) + .Build(); + + var configure = new FirestoreOptionsConfigure(config); + var options = new FirestoreOptions(); + + // Act + configure.Configure("Test", options); + + // Assert + _ = await Assert.That(options.Timeout).IsEqualTo(-1); + } + + [Test] + public async Task Validate_WhenTimeoutLessThanMinusOne_ReturnsFailure() + { + // Arrange + var config = new ConfigurationBuilder().Build(); + var configure = new FirestoreOptionsConfigure(config); + var options = new FirestoreOptions { Timeout = -2 }; + + // Act + var result = configure.Validate("Test", options); + + // Assert + using (Assert.Multiple()) + { + _ = await Assert.That(result.Failed).IsTrue(); + _ = await Assert.That(result.FailureMessage).Contains("timeout"); + } + } + + [Test] + public async Task Validate_WhenTimeoutValid_ReturnsSuccess() + { + // Arrange + var config = new ConfigurationBuilder().Build(); + var configure = new FirestoreOptionsConfigure(config); + var options = new FirestoreOptions { Timeout = 1000 }; + + // Act + var result = configure.Validate("Test", options); + + // Assert + _ = await Assert.That(result.Succeeded).IsTrue(); + } + + [Test] + public async Task Validate_WhenNameNull_ReturnsFailure() + { + // Arrange + var config = new ConfigurationBuilder().Build(); + var configure = new FirestoreOptionsConfigure(config); + var options = new FirestoreOptions(); + + // Act + var result = configure.Validate(null, options); + + // Assert + _ = await Assert.That(result.Failed).IsTrue(); + } + + [Test] + public async Task Validate_WhenOptionsNull_ReturnsFailure() + { + // Arrange + var config = new ConfigurationBuilder().Build(); + var configure = new FirestoreOptionsConfigure(config); + + // Act + var result = configure.Validate("Test", null!); + + // Assert + _ = await Assert.That(result.Failed).IsTrue(); + } } diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs index 7ce321818..cf4745380 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs @@ -15,4 +15,51 @@ public async Task Options_NotSame_Expected() _ = await Assert.That(options1).IsEqualTo(options2).And.IsNotSameReferenceAs(options2); } + + [Test] + public async Task Options_DefaultTimeout_Is100() + { + var options = new FirestoreOptions(); + + _ = await Assert.That(options.Timeout).IsEqualTo(100); + } + + [Test] + public async Task Options_WithCustomTimeout_TimeoutSet() + { + var options = new FirestoreOptions { Timeout = 5000 }; + + _ = await Assert.That(options.Timeout).IsEqualTo(5000); + } + + [Test] + public async Task Options_WithKeyedService_KeyedServiceSet() + { + const string keyedService = "my-firestore"; + var options = new FirestoreOptions { KeyedService = keyedService }; + + _ = await Assert.That(options.KeyedService).IsEqualTo(keyedService); + } + + [Test] + public async Task Options_WithClone_AllPropertiesCloned() + { + var options1 = new FirestoreOptions { Timeout = 5000, KeyedService = "test" }; + var options2 = options1 with { }; + + using (Assert.Multiple()) + { + _ = await Assert.That(options2.Timeout).IsEqualTo(options1.Timeout); + _ = await Assert.That(options2.KeyedService).IsEqualTo(options1.KeyedService); + } + } + + [Test] + public async Task Options_WithDifferentValues_NotEqual() + { + var options1 = new FirestoreOptions { Timeout = 100 }; + var options2 = new FirestoreOptions { Timeout = 200 }; + + _ = await Assert.That(options1).IsNotEqualTo(options2); + } } From 189dcd41dd03c166caa44484d3b214c0d8e4557e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 20:08:07 +0100 Subject: [PATCH 11/14] fix(test): PublicAPI Tests --- ...tore.PublicApi_HasNotChanged_Theory.verified.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/NetEvolve.HealthChecks.GCP.Firestore.PublicApi_HasNotChanged_Theory.verified.txt diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/NetEvolve.HealthChecks.GCP.Firestore.PublicApi_HasNotChanged_Theory.verified.txt b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/NetEvolve.HealthChecks.GCP.Firestore.PublicApi_HasNotChanged_Theory.verified.txt new file mode 100644 index 000000000..3c5436702 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/_snapshots/NetEvolve.HealthChecks.GCP.Firestore.PublicApi_HasNotChanged_Theory.verified.txt @@ -0,0 +1,13 @@ +namespace NetEvolve.HealthChecks.GCP.Firestore +{ + public static class DependencyInjectionExtensions + { + public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddFirestore([System.Diagnostics.CodeAnalysis.NotNull] this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, [System.Diagnostics.CodeAnalysis.NotNull] string name, System.Action? options = null, params string[] tags) { } + } + public sealed class FirestoreOptions : System.IEquatable + { + public FirestoreOptions() { } + public string? KeyedService { get; set; } + public int Timeout { get; set; } + } +} \ No newline at end of file From 55ee362231c9b76a203b39e498a786aba2852944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 20:10:13 +0100 Subject: [PATCH 12/14] chore: Order --- .../HealthCheckArchitecture.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs b/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs index 22e1f5da7..9a8f7783d 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs @@ -34,6 +34,8 @@ private static Architecture LoadArchitecture() typeof(Azure.Queues.QueueClientAvailableHealthCheck).Assembly, typeof(Azure.ServiceBus.ServiceBusQueueHealthCheck).Assembly, typeof(Azure.Tables.TableClientAvailableHealthCheck).Assembly, + // GCP + typeof(GCP.Firestore.FirestoreHealthCheck).Assembly, // others typeof(Abstractions.HealthCheckBase).Assembly, typeof(ArangoDb.ArangoDbHealthCheck).Assembly, @@ -44,7 +46,6 @@ private static Architecture LoadArchitecture() typeof(DuckDB.DuckDBHealthCheck).Assembly, typeof(Elasticsearch.ElasticsearchHealthCheck).Assembly, typeof(Firebird.FirebirdHealthCheck).Assembly, - typeof(GCP.Firestore.FirestoreHealthCheck).Assembly, typeof(Http.HttpHealthCheck).Assembly, typeof(Keycloak.KeycloakHealthCheck).Assembly, typeof(LiteDB.LiteDBHealthCheck).Assembly, From 6b05ffd6a7ba57909fae4c6b724423942c65cc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 20:17:32 +0100 Subject: [PATCH 13/14] chore: Marked TestGroup GCP.Firestore --- .../GCP.Firestore/FirestoreHealthCheckTests.cs | 2 +- .../DependencyInjectionExtensionsTests.cs | 4 ++-- .../Firestore => GCP.Firestore}/FirestoreConfigureTests.cs | 2 +- .../Firestore => GCP.Firestore}/FirestoreHealthCheckTests.cs | 4 ++-- .../{GCP/Firestore => GCP.Firestore}/FirestoreOptionsTests.cs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename tests/NetEvolve.HealthChecks.Tests.Unit/{GCP/Firestore => GCP.Firestore}/DependencyInjectionExtensionsTests.cs (98%) rename tests/NetEvolve.HealthChecks.Tests.Unit/{GCP/Firestore => GCP.Firestore}/FirestoreConfigureTests.cs (99%) rename tests/NetEvolve.HealthChecks.Tests.Unit/{GCP/Firestore => GCP.Firestore}/FirestoreHealthCheckTests.cs (96%) rename tests/NetEvolve.HealthChecks.Tests.Unit/{GCP/Firestore => GCP.Firestore}/FirestoreOptionsTests.cs (94%) diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs index c7ff91479..3e32fa96a 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs @@ -9,7 +9,7 @@ using NetEvolve.Extensions.TUnit; using NetEvolve.HealthChecks.GCP.Firestore; -[TestGroup(nameof(Firestore))] +[TestGroup($"GCP.{nameof(Firestore)}")] [ClassDataSource(Shared = InstanceSharedType.Firestore)] public sealed class FirestoreHealthCheckTests : HealthCheckTestBase { diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/DependencyInjectionExtensionsTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/DependencyInjectionExtensionsTests.cs similarity index 98% rename from tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/DependencyInjectionExtensionsTests.cs rename to tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/DependencyInjectionExtensionsTests.cs index 6b2d616a3..583df667b 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/DependencyInjectionExtensionsTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/DependencyInjectionExtensionsTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; +namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; using System; using System.Linq; @@ -9,7 +9,7 @@ namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; using NetEvolve.Extensions.TUnit; using NetEvolve.HealthChecks.GCP.Firestore; -[TestGroup(nameof(Firestore))] +[TestGroup($"GCP.{nameof(Firestore)}")] public sealed class DependencyInjectionExtensionsTests { [Test] diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreConfigureTests.cs similarity index 99% rename from tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs rename to tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreConfigureTests.cs index 15f30d59b..7b9998649 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreConfigureTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreConfigureTests.cs @@ -7,7 +7,7 @@ using NetEvolve.Extensions.TUnit; using NetEvolve.HealthChecks.GCP.Firestore; -[TestGroup(nameof(Firestore))] +[TestGroup($"GCP.{nameof(Firestore)}")] public sealed class FirestoreConfigureTests { [Test] diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreHealthCheckTests.cs similarity index 96% rename from tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreHealthCheckTests.cs rename to tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreHealthCheckTests.cs index 2c03d3898..d66c0f4f7 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreHealthCheckTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreHealthCheckTests.cs @@ -1,4 +1,4 @@ -namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; +namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; using System; using System.Threading; @@ -11,7 +11,7 @@ namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; using NetEvolve.HealthChecks.GCP.Firestore; using NSubstitute; -[TestGroup(nameof(Firestore))] +[TestGroup($"GCP.{nameof(Firestore)}")] public sealed class FirestoreHealthCheckTests { [Test] diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreOptionsTests.cs similarity index 94% rename from tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs rename to tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreOptionsTests.cs index cf4745380..546065860 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Unit/GCP/Firestore/FirestoreOptionsTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreOptionsTests.cs @@ -1,10 +1,10 @@ -namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; +namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore; using System.Threading.Tasks; using NetEvolve.Extensions.TUnit; using NetEvolve.HealthChecks.GCP.Firestore; -[TestGroup(nameof(Firestore))] +[TestGroup($"GCP.{nameof(Firestore)}")] public sealed class FirestoreOptionsTests { [Test] From 78cafbdfa6c28e0afcf9b232908b59d69f240518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Sat, 1 Nov 2025 20:39:58 +0100 Subject: [PATCH 14/14] fix: Missing timeout --- .../AWS.SNS/SimpleNotificationServiceHealthCheckTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/AWS.SNS/SimpleNotificationServiceHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/AWS.SNS/SimpleNotificationServiceHealthCheckTests.cs index d4bb5bd69..f01af0951 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/AWS.SNS/SimpleNotificationServiceHealthCheckTests.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/AWS.SNS/SimpleNotificationServiceHealthCheckTests.cs @@ -121,6 +121,7 @@ await RunAndVerify( options.TopicName = topicName; options.Subscription = subcription.SubscriptionArn; options.Mode = CreationMode.BasicAuthentication; + options.Timeout = 10000; // Set a reasonable timeout } ); },