diff --git a/Directory.Packages.props b/Directory.Packages.props
index 39c528323..f7dd25b23 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -41,6 +41,7 @@
+
@@ -84,6 +85,7 @@
+
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..9a4c2c3fa
--- /dev/null
+++ b/src/NetEvolve.HealthChecks.GCP.Firestore/DependencyInjectionExtensions.cs
@@ -0,0 +1,63 @@
+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.HealthChecks.Abstractions;
+
+///
+/// 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 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,
+ Action? options = null,
+ params string[] tags
+ )
+ {
+ 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,
+ 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
new file mode 100644
index 000000000..326aa5011
--- /dev/null
+++ b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreHealthCheck.cs
@@ -0,0 +1,44 @@
+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);
+
+ // 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);
+
+ 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..5f4be7783
--- /dev/null
+++ b/src/NetEvolve.HealthChecks.GCP.Firestore/FirestoreOptionsConfigure.cs
@@ -0,0 +1,43 @@
+namespace NetEvolve.HealthChecks.GCP.Firestore;
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+using static Microsoft.Extensions.Options.ValidateOptionsResult;
+
+internal sealed class FirestoreOptionsConfigure
+ : IConfigureNamedOptions,
+ IValidateOptions
+{
+ 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 ValidateOptionsResult Validate(string? name, FirestoreOptions options)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ 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/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..40364658e
--- /dev/null
+++ b/src/NetEvolve.HealthChecks.GCP.Firestore/README.md
@@ -0,0 +1,78 @@
+# 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");
+```
+
+## 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.
diff --git a/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs b/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs
index bbbf2f3f6..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,
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 @@
+
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
}
);
},
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..2a1d8d842
--- /dev/null
+++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreDatabase.cs
@@ -0,0 +1,43 @@
+namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore;
+
+using System;
+using System.Threading.Tasks;
+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 _container = new FirestoreBuilder().WithLogger(NullLogger.Instance).Build();
+
+ 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 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,
+ };
+
+ var 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
new file mode 100644
index 000000000..3e32fa96a
--- /dev/null
+++ b/tests/NetEvolve.HealthChecks.Tests.Integration/GCP.Firestore/FirestoreHealthCheckTests.cs
@@ -0,0 +1,101 @@
+namespace NetEvolve.HealthChecks.Tests.Integration.GCP.Firestore;
+
+using System;
+using System.Collections.Generic;
+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($"GCP.{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 => _ = services.AddSingleton(_ => _database.Database)
+ );
+
+ [Test]
+ public async Task AddFirestore_UseOptions_Degraded() =>
+ await RunAndVerify(
+ healthChecks => healthChecks.AddFirestore("TestContainerDegraded", options => options.Timeout = 0),
+ HealthStatus.Degraded,
+ serviceBuilder: services => _ = services.AddSingleton(_ => _database.Database)
+ );
+
+ [Test]
+ public async Task AddFirestore_UseOptionsWithKeyedService_Healthy() =>
+ await RunAndVerify(
+ healthChecks =>
+ {
+ _ = healthChecks.AddFirestore(
+ "TestContainerKeyedServiceHealthy",
+ options =>
+ {
+ options.Timeout = 10000;
+ options.KeyedService = "firestore";
+ }
+ );
+ },
+ HealthStatus.Healthy,
+ serviceBuilder: services => _ = services.AddKeyedSingleton("firestore", (_, _) => _database.Database)
+ );
+
+ [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 => _ = services.AddSingleton(_ => _database.Database)
+ );
+
+ [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 => _ = services.AddSingleton(_ => _database.Database)
+ );
+
+ [Test]
+ public async Task AddFirestore_UseConfiguration_TimeoutMinusTwo_ThrowException() =>
+ await RunAndVerify(
+ healthChecks => healthChecks.AddFirestore("TestNoValues"),
+ HealthStatus.Unhealthy,
+ config =>
+ {
+ var values = new Dictionary(StringComparer.Ordinal)
+ {
+ { "HealthChecks:GCP:Firestore:TestNoValues:Timeout", "-2" },
+ };
+ _ = config.AddInMemoryCollection(values);
+ },
+ serviceBuilder: services => _ = services.AddSingleton(_ => _database.Database)
+ );
+}
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.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 @@
+
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.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
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..583df667b
--- /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($"GCP.{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
new file mode 100644
index 000000000..7b9998649
--- /dev/null
+++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreConfigureTests.cs
@@ -0,0 +1,153 @@
+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;
+
+[TestGroup($"GCP.{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 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/FirestoreHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreHealthCheckTests.cs
new file mode 100644
index 000000000..d66c0f4f7
--- /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($"GCP.{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..546065860
--- /dev/null
+++ b/tests/NetEvolve.HealthChecks.Tests.Unit/GCP.Firestore/FirestoreOptionsTests.cs
@@ -0,0 +1,65 @@
+namespace NetEvolve.HealthChecks.Tests.Unit.GCP.Firestore;
+
+using System.Threading.Tasks;
+using NetEvolve.Extensions.TUnit;
+using NetEvolve.HealthChecks.GCP.Firestore;
+
+[TestGroup($"GCP.{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);
+ }
+
+ [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);
+ }
+}
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 @@
+