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
+
+[](https://www.nuget.org/packages/NetEvolve.HealthChecks.GCP.Firestore/)
+[](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
}
);
},