diff --git a/HealthChecks.slnx b/HealthChecks.slnx index 8f244a67..07b5f339 100644 --- a/HealthChecks.slnx +++ b/HealthChecks.slnx @@ -36,6 +36,7 @@ + diff --git a/src/NetEvolve.HealthChecks.Azure.CosmosDB/ClientCreation.cs b/src/NetEvolve.HealthChecks.Azure.CosmosDB/ClientCreation.cs new file mode 100644 index 00000000..c71d7401 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.CosmosDB/ClientCreation.cs @@ -0,0 +1,60 @@ +namespace NetEvolve.HealthChecks.Azure.CosmosDB; + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using Azure.Core; +using Azure.Identity; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.DependencyInjection; + +internal class ClientCreation +{ + private ConcurrentDictionary? _cosmosClients; + + internal CosmosClient GetCosmosClient( + string name, + TOptions options, + IServiceProvider serviceProvider + ) + where TOptions : class, ICosmosDbOptions + { + _cosmosClients ??= new ConcurrentDictionary(); + + return _cosmosClients.GetOrAdd(name, _ => CreateCosmosClient(options, serviceProvider)); + } + + internal static CosmosClient CreateCosmosClient( + TOptions options, + IServiceProvider serviceProvider + ) + where TOptions : class, ICosmosDbOptions + { + CosmosClientOptions? clientOptions = null; + if (options.ConfigureClientOptions is not null) + { + clientOptions = new CosmosClientOptions(); + options.ConfigureClientOptions(clientOptions); + } + + var mode = options.Mode ?? CosmosDbClientCreationMode.ConnectionString; + +#pragma warning disable IDE0010 // Add missing cases + switch (mode) + { + case CosmosDbClientCreationMode.DefaultAzureCredentials: + var tokenCredential = serviceProvider.GetService() ?? new DefaultAzureCredential(); + return new CosmosClient(options.ServiceEndpoint, tokenCredential, clientOptions); + case CosmosDbClientCreationMode.ConnectionString: + return new CosmosClient(options.ConnectionString, clientOptions); + case CosmosDbClientCreationMode.AccountKey: + return new CosmosClient(options.ServiceEndpoint, options.AccountKey, clientOptions); + case CosmosDbClientCreationMode.ServicePrincipal: + var servicePrincipalCredential = serviceProvider.GetService() ?? new DefaultAzureCredential(); + return new CosmosClient(options.ServiceEndpoint, servicePrincipalCredential, clientOptions); + default: + throw new UnreachableException($"Invalid client creation mode `{mode}`."); + } +#pragma warning restore IDE0010 // Add missing cases + } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbClientCreationMode.cs b/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbClientCreationMode.cs new file mode 100644 index 00000000..52c0a43e --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbClientCreationMode.cs @@ -0,0 +1,27 @@ +namespace NetEvolve.HealthChecks.Azure.CosmosDB; + +/// +/// Specifies the mode for creating a CosmosDB client. +/// +public enum CosmosDbClientCreationMode +{ + /// + /// Use connection string to create the client. + /// + ConnectionString, + + /// + /// Use service endpoint with default Azure credentials. + /// + DefaultAzureCredentials, + + /// + /// Use service endpoint with account key authentication. + /// + AccountKey, + + /// + /// Use service endpoint with Azure Active Directory (token credential). + /// + ServicePrincipal +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbConfigure.cs b/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbConfigure.cs new file mode 100644 index 00000000..268c395a --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbConfigure.cs @@ -0,0 +1,115 @@ +namespace NetEvolve.HealthChecks.Azure.CosmosDB; + +using System; +using System.Threading; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using static Microsoft.Extensions.Options.ValidateOptionsResult; + +internal sealed class CosmosDbConfigure + : IConfigureNamedOptions, + IValidateOptions +{ + private readonly IConfiguration _configuration; + private readonly IServiceProvider _serviceProvider; + + public CosmosDbConfigure(IConfiguration configuration, IServiceProvider serviceProvider) + { + _configuration = configuration; + _serviceProvider = serviceProvider; + } + + public void Configure(string? name, CosmosDbOptions options) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + _configuration.Bind($"HealthChecks:CosmosDb:{name}", options); + } + + public void Configure(CosmosDbOptions options) => Configure(Options.DefaultName, options); + + public ValidateOptionsResult Validate(string? name, CosmosDbOptions 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."); + } + + var mode = options.Mode ?? CosmosDbClientCreationMode.ConnectionString; + + return mode switch + { + CosmosDbClientCreationMode.ConnectionString => ValidateModeConnectionString(options), + CosmosDbClientCreationMode.DefaultAzureCredentials => ValidateModeDefaultAzureCredentials(options), + CosmosDbClientCreationMode.AccountKey => ValidateModeAccountKey(options), + CosmosDbClientCreationMode.ServicePrincipal => ValidateModeServicePrincipal(options), + _ => Fail($"The mode `{mode}` is not supported."), + }; + } + + private static ValidateOptionsResult ValidateModeConnectionString(CosmosDbOptions options) + { + if (string.IsNullOrWhiteSpace(options.ConnectionString)) + { + return Fail( + $"The connection string cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.ConnectionString)}` mode." + ); + } + + return Success; + } + + private static ValidateOptionsResult ValidateModeDefaultAzureCredentials(CosmosDbOptions options) + { + if (string.IsNullOrWhiteSpace(options.ServiceEndpoint)) + { + return Fail( + $"The service endpoint cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.DefaultAzureCredentials)}` mode." + ); + } + + return Success; + } + + private static ValidateOptionsResult ValidateModeAccountKey(CosmosDbOptions options) + { + if (string.IsNullOrWhiteSpace(options.ServiceEndpoint)) + { + return Fail( + $"The service endpoint cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.AccountKey)}` mode." + ); + } + + if (string.IsNullOrWhiteSpace(options.AccountKey)) + { + return Fail( + $"The account key cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.AccountKey)}` mode." + ); + } + + return Success; + } + + private static ValidateOptionsResult ValidateModeServicePrincipal(CosmosDbOptions options) + { + if (string.IsNullOrWhiteSpace(options.ServiceEndpoint)) + { + return Fail( + $"The service endpoint cannot be null or whitespace when using `{nameof(CosmosDbClientCreationMode.ServicePrincipal)}` mode." + ); + } + + return Success; + } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbHealthCheck.cs b/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbHealthCheck.cs new file mode 100644 index 00000000..da8d9e26 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbHealthCheck.cs @@ -0,0 +1,76 @@ +namespace NetEvolve.HealthChecks.Azure.CosmosDB; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using NetEvolve.Extensions.Tasks; +using NetEvolve.HealthChecks.Abstractions; + +internal sealed class CosmosDbHealthCheck : ConfigurableHealthCheckBase +{ + private readonly IServiceProvider _serviceProvider; + + public CosmosDbHealthCheck( + IServiceProvider serviceProvider, + IOptionsMonitor optionsMonitor + ) + : base(optionsMonitor) => _serviceProvider = serviceProvider; + + protected override async ValueTask ExecuteHealthCheckAsync( + string name, + HealthStatus failureStatus, + CosmosDbOptions options, + CancellationToken cancellationToken + ) + { + var clientCreation = _serviceProvider.GetRequiredService(); + var cosmosClient = clientCreation.GetCosmosClient(name, options, _serviceProvider); + + // Check if the CosmosDB service is available by reading the account properties + var (serviceAvailable, _) = await cosmosClient + .ReadAccountAsync() + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + if (!serviceAvailable) + { + return HealthCheckState(false, name); + } + + // If database name is specified, check database availability + if (!string.IsNullOrWhiteSpace(options.DatabaseName)) + { + var database = cosmosClient.GetDatabase(options.DatabaseName); + var (databaseAvailable, _) = await database + .ReadAsync(cancellationToken: cancellationToken) + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + if (!databaseAvailable) + { + return HealthCheckResult.Unhealthy($"{name}: Database `{options.DatabaseName}` is not available."); + } + + // If container name is also specified, check container availability + if (!string.IsNullOrWhiteSpace(options.ContainerName)) + { + var container = database.GetContainer(options.ContainerName); + var (containerAvailable, _) = await container + .ReadContainerAsync(cancellationToken: cancellationToken) + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + if (!containerAvailable) + { + return HealthCheckResult.Unhealthy($"{name}: Container `{options.ContainerName}` is not available."); + } + } + } + + return HealthCheckState(true, name); + } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbOptions.cs b/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbOptions.cs new file mode 100644 index 00000000..d5786fb9 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.CosmosDB/CosmosDbOptions.cs @@ -0,0 +1,50 @@ +namespace NetEvolve.HealthChecks.Azure.CosmosDB; + +using System; +using Microsoft.Azure.Cosmos; + +/// +/// Options for the . +/// +public sealed record CosmosDbOptions : ICosmosDbOptions +{ + /// + /// Gets or sets the connection string. + /// + public string? ConnectionString { get; set; } + + /// + /// Gets or sets the mode to create the client. + /// + public CosmosDbClientCreationMode? Mode { get; set; } + + /// + /// Gets or sets the service endpoint. + /// + public string? ServiceEndpoint { get; set; } + + /// + /// Gets or sets the account key. + /// + public string? AccountKey { get; set; } + + /// + /// Gets or sets the lambda to configure the . + /// + public Action? ConfigureClientOptions { get; set; } + + /// + /// The timeout to use when connecting and executing tasks against the database. + /// + public int Timeout { get; set; } = 100; + + /// + /// Gets or sets the database name to check. + /// + public string? DatabaseName { get; set; } + + /// + /// Gets or sets the container name to check. + /// + public string? ContainerName { get; set; } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.CosmosDB/DependencyInjectionExtensions.cs b/src/NetEvolve.HealthChecks.Azure.CosmosDB/DependencyInjectionExtensions.cs new file mode 100644 index 00000000..a813e02a --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.CosmosDB/DependencyInjectionExtensions.cs @@ -0,0 +1,66 @@ +namespace NetEvolve.HealthChecks.Azure.CosmosDB; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using NetEvolve.HealthChecks.Abstractions; + +/// +/// Extensions methods for with custom Health Checks. +/// +public static class DependencyInjectionExtensions +{ + private static readonly string[] _defaultTags = ["azure", "cosmosdb", "database"]; + + /// + /// Add a health check for Azure Cosmos DB. + /// + /// The . + /// The name of the . + /// An optional action to configure. + /// A list of additional tags that can be used to filter sets of health checks. Optional. + /// The is . + /// The is . + /// The is or whitespace. + /// The is already in use. + /// The is . + public static IHealthChecksBuilder AddCosmosDb( + [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.Services.TryAddSingleton(); + } + + 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 CosmosDbCheckMarker; +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.CosmosDB/ICosmosDbOptions.cs b/src/NetEvolve.HealthChecks.Azure.CosmosDB/ICosmosDbOptions.cs new file mode 100644 index 00000000..d950997c --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.CosmosDB/ICosmosDbOptions.cs @@ -0,0 +1,40 @@ +namespace NetEvolve.HealthChecks.Azure.CosmosDB; + +using System; +using Microsoft.Azure.Cosmos; + +/// +/// Common interface for CosmosDB options. +/// +public interface ICosmosDbOptions +{ + /// + /// Gets or sets the connection string. + /// + string? ConnectionString { get; set; } + + /// + /// Gets or sets the mode to create the client. + /// + CosmosDbClientCreationMode? Mode { get; set; } + + /// + /// Gets or sets the service endpoint. + /// + string? ServiceEndpoint { get; set; } + + /// + /// Gets or sets the account key. + /// + string? AccountKey { get; set; } + + /// + /// Gets or sets the lambda to configure the . + /// + Action? ConfigureClientOptions { get; set; } + + /// + /// The timeout to use when connecting and executing tasks against the database. + /// + int Timeout { get; set; } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.CosmosDB/NetEvolve.HealthChecks.Azure.CosmosDB.csproj b/src/NetEvolve.HealthChecks.Azure.CosmosDB/NetEvolve.HealthChecks.Azure.CosmosDB.csproj new file mode 100644 index 00000000..9560f9f4 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.CosmosDB/NetEvolve.HealthChecks.Azure.CosmosDB.csproj @@ -0,0 +1,15 @@ + + + $(_ProjectTargetFrameworks) + Contains HealthChecks for Azure Cosmos DB. + $(PackageTags);azure;cosmosdb;database + + + + + + + + + + \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.CosmosDB/README.md b/src/NetEvolve.HealthChecks.Azure.CosmosDB/README.md new file mode 100644 index 00000000..be49d54b --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.CosmosDB/README.md @@ -0,0 +1,123 @@ +# NetEvolve.HealthChecks.Azure.CosmosDB + +[![NuGet](https://img.shields.io/nuget/v/NetEvolve.HealthChecks.Azure.CosmosDB?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.CosmosDB/) +[![NuGet](https://img.shields.io/nuget/dt/NetEvolve.HealthChecks.Azure.CosmosDB?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.CosmosDB/) + +This package provides a health check for Azure Cosmos DB, based on the [Microsoft.Azure.Cosmos](https://www.nuget.org/packages/Microsoft.Azure.Cosmos/) package. The main purpose is to check that the Azure Cosmos DB service is available and that the client can connect to it. + +: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.Azure.CosmosDB +``` + +## Health Check - Azure Cosmos DB Availability +The health check is a liveness check. It will check that the Azure Cosmos DB service is reachable and that the client can connect to it. If the service needs longer than the configured timeout to respond, the health check will return `Degraded`. If the service is not reachable, the health check will return `Unhealthy`. + +### Usage +After adding the package, you need to import the namespace `NetEvolve.HealthChecks.Azure.CosmosDB` and add the health check to the service collection. +```csharp +using NetEvolve.HealthChecks.Azure.CosmosDB; +``` +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 `azure`, `cosmosdb` and `database` 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. Therefore, you have to add the configuration section `HealthChecks:CosmosDb` to your `appsettings.json` file. +```csharp +var builder = services.AddHealthChecks(); + +builder.AddCosmosDb(""); +``` + +The configuration looks like this: +```json +{ + ..., // other configuration + "HealthChecks": { + "CosmosDb": { + "": { + "ConnectionString": "", // required when using ConnectionString mode + "ServiceEndpoint": "", // required when using other modes + "AccountKey": "", // required when using AccountKey mode + "DatabaseName": "", // optional, checks specific database + "ContainerName": "", // optional, checks specific container (requires DatabaseName) + "Mode": "", // optional, defaults to ConnectionString + "Timeout": "" // optional, default is 100 milliseconds + } + } + } +} +``` + +### Variant 2: Options based +The second one is to use the options based approach. Therefore, you have to create an instance of `CosmosDbOptions` and provide the configuration. +```csharp +var builder = services.AddHealthChecks(); + +builder.AddCosmosDb("", options => +{ + options.ConnectionString = ""; + options.DatabaseName = ""; // optional + options.ContainerName = ""; // optional + options.Timeout = ""; +}); +``` + +### :bulb: You can always provide tags to all health checks, for grouping or filtering. + +```csharp +var builder = services.AddHealthChecks(); + +builder.AddCosmosDb("", options => ..., "cosmosdb"); +``` + +### Client Creation Modes +The health check supports multiple authentication modes: + +#### ConnectionString +Uses a Cosmos DB connection string for authentication. +```csharp +builder.AddCosmosDb("", options => +{ + options.Mode = CosmosDbClientCreationMode.ConnectionString; + options.ConnectionString = ""; +}); +``` + +#### DefaultAzureCredentials +Uses Default Azure Credentials for authentication. +```csharp +builder.AddCosmosDb("", options => +{ + options.Mode = CosmosDbClientCreationMode.DefaultAzureCredentials; + options.ServiceEndpoint = ""; +}); +``` + +#### AccountKey +Uses account key for authentication. +```csharp +builder.AddCosmosDb("", options => +{ + options.Mode = CosmosDbClientCreationMode.AccountKey; + options.ServiceEndpoint = ""; + options.AccountKey = ""; +}); +``` + +#### ServicePrincipal +Uses service principal for authentication (requires token credential to be registered). +```csharp +builder.AddCosmosDb("", options => +{ + options.Mode = CosmosDbClientCreationMode.ServicePrincipal; + options.ServiceEndpoint = ""; +}); +``` \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/Azure.CosmosDB/CosmosDbHealthCheckIntegrationTests.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/Azure.CosmosDB/CosmosDbHealthCheckIntegrationTests.cs new file mode 100644 index 00000000..1a5b6df9 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/Azure.CosmosDB/CosmosDbHealthCheckIntegrationTests.cs @@ -0,0 +1,16 @@ +namespace NetEvolve.HealthChecks.Tests.Integration.Azure.CosmosDB; + +using NetEvolve.Extensions.TUnit; + +[TestGroup($"{nameof(Azure)}.{nameof(CosmosDB)}")] +public class CosmosDbHealthCheckIntegrationTests +{ + [Test] + [Skip("Integration tests require actual CosmosDB instance")] + public async Task ExecuteHealthCheckAsync_WhenCosmosDbAvailable_ShouldReturnHealthy() + { + // This test would require actual CosmosDB setup or emulator + // Skipping for now as it requires infrastructure + await Task.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/CosmosDbConfigureTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/CosmosDbConfigureTests.cs new file mode 100644 index 00000000..1477fa9a --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/CosmosDbConfigureTests.cs @@ -0,0 +1,81 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.Azure.CosmosDB; + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.Azure.CosmosDB; + +[TestGroup($"{nameof(Azure)}.{nameof(CosmosDB)}")] +public class CosmosDbConfigureTests +{ + [Test] + public void Configure_WhenConnectionStringMode_ShouldSucceed() + { + // Arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new[] + { + new KeyValuePair("HealthChecks:CosmosDb:Test:ConnectionString", "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test;"), + new KeyValuePair("HealthChecks:CosmosDb:Test:Mode", "ConnectionString") + }) + .Build(); + + var serviceProvider = new ServiceCollection() + .AddSingleton(configuration) + .BuildServiceProvider(); + + var configure = new CosmosDbConfigure(configuration, serviceProvider); + var options = new CosmosDbOptions(); + + // Act + configure.Configure("Test", options); + var result = configure.Validate("Test", options); + + // Assert + Assert.That(result.Succeeded, Is.True); + Assert.That(options.ConnectionString, Is.EqualTo("AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test;")); + Assert.That(options.Mode, Is.EqualTo(CosmosDbClientCreationMode.ConnectionString)); + } + + [Test] + public void Validate_WhenMissingConnectionString_ShouldFail() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var configure = new CosmosDbConfigure(configuration, serviceProvider); + var options = new CosmosDbOptions + { + Mode = CosmosDbClientCreationMode.ConnectionString + }; + + // Act + var result = configure.Validate("Test", options); + + // Assert + Assert.That(result.Succeeded, Is.False); + Assert.That(result.Failures, Contains.Item("The connection string cannot be null or whitespace when using `ConnectionString` mode.")); + } + + [Test] + public void Validate_WhenAccountKeyModeWithMissingEndpoint_ShouldFail() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var configure = new CosmosDbConfigure(configuration, serviceProvider); + var options = new CosmosDbOptions + { + Mode = CosmosDbClientCreationMode.AccountKey, + AccountKey = "test-key" + }; + + // Act + var result = configure.Validate("Test", options); + + // Assert + Assert.That(result.Succeeded, Is.False); + Assert.That(result.Failures, Contains.Item("The service endpoint cannot be null or whitespace when using `AccountKey` mode.")); + } +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/CosmosDbHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/CosmosDbHealthCheckTests.cs new file mode 100644 index 00000000..c17b0251 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/CosmosDbHealthCheckTests.cs @@ -0,0 +1,47 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.Azure.CosmosDB; + +using System.Threading; +using Microsoft.Extensions.Options; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.Azure.CosmosDB; + +[TestGroup($"{nameof(Azure)}.{nameof(CosmosDB)}")] +public class CosmosDbHealthCheckTests +{ + [Test] + public async Task ExecuteHealthCheckAsync_WhenCalled_ShouldNotThrow() + { + // Arrange + var serviceProvider = new MockServiceProvider(); + var optionsMonitor = new MockOptionsMonitor(); + var healthCheck = new CosmosDbHealthCheck(serviceProvider, optionsMonitor); + + // Act & Assert + // Note: This will fail in real execution due to missing CosmosDB connection, + // but it should not throw during construction + Assert.That(healthCheck, Is.Not.Null); + await Task.CompletedTask; + } + + private class MockServiceProvider : IServiceProvider + { + public object? GetService(Type serviceType) + { + if (serviceType == typeof(ClientCreation)) + { + return new ClientCreation(); + } + return null; + } + } + + private class MockOptionsMonitor : IOptionsMonitor + where T : class + { + public T CurrentValue => throw new NotImplementedException(); + + public T Get(string? name) => throw new NotImplementedException(); + + public IDisposable? OnChange(Action listener) => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/CosmosDbOptionsTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/CosmosDbOptionsTests.cs new file mode 100644 index 00000000..6267d8fe --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/CosmosDbOptionsTests.cs @@ -0,0 +1,67 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.Azure.CosmosDB; + +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.Azure.CosmosDB; + +[TestGroup($"{nameof(Azure)}.{nameof(CosmosDB)}")] +public class CosmosDbOptionsTests +{ + [Test] + public void Constructor_WhenParametersDefault_ExpectedValues() + { + // Arrange / Act + var options = new CosmosDbOptions(); + + // Assert + Assert.Multiple(() => + { + Assert.That(options.ConnectionString, Is.Null); + Assert.That(options.ServiceEndpoint, Is.Null); + Assert.That(options.AccountKey, Is.Null); + Assert.That(options.Mode, Is.Null); + Assert.That(options.DatabaseName, Is.Null); + Assert.That(options.ContainerName, Is.Null); + Assert.That(options.Timeout, Is.EqualTo(100)); + Assert.That(options.ConfigureClientOptions, Is.Null); + }); + } + + [Test] + public void Properties_WhenSet_ExpectedValues() + { + // Arrange + var expectedConnectionString = "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test;"; + var expectedServiceEndpoint = "https://test.documents.azure.com:443/"; + var expectedAccountKey = "test-key"; + var expectedMode = CosmosDbClientCreationMode.AccountKey; + var expectedDatabaseName = "TestDatabase"; + var expectedContainerName = "TestContainer"; + var expectedTimeout = 500; + + // Act + var options = new CosmosDbOptions + { + ConnectionString = expectedConnectionString, + ServiceEndpoint = expectedServiceEndpoint, + AccountKey = expectedAccountKey, + Mode = expectedMode, + DatabaseName = expectedDatabaseName, + ContainerName = expectedContainerName, + Timeout = expectedTimeout, + ConfigureClientOptions = _ => { } + }; + + // Assert + Assert.Multiple(() => + { + Assert.That(options.ConnectionString, Is.EqualTo(expectedConnectionString)); + Assert.That(options.ServiceEndpoint, Is.EqualTo(expectedServiceEndpoint)); + Assert.That(options.AccountKey, Is.EqualTo(expectedAccountKey)); + Assert.That(options.Mode, Is.EqualTo(expectedMode)); + Assert.That(options.DatabaseName, Is.EqualTo(expectedDatabaseName)); + Assert.That(options.ContainerName, Is.EqualTo(expectedContainerName)); + Assert.That(options.Timeout, Is.EqualTo(expectedTimeout)); + Assert.That(options.ConfigureClientOptions, Is.Not.Null); + }); + } +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/DependencyInjectionExtensionsTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/DependencyInjectionExtensionsTests.cs new file mode 100644 index 00000000..556b6a18 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.CosmosDB/DependencyInjectionExtensionsTests.cs @@ -0,0 +1,88 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.Azure.CosmosDB; + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.Azure.CosmosDB; + +[TestGroup($"{nameof(Azure)}.{nameof(CosmosDB)}")] +public class DependencyInjectionExtensionsTests +{ + [Test] + public void AddCosmosDb_WhenArgumentBuilderNull_ThrowArgumentNullException() + { + // Arrange + var builder = default(IHealthChecksBuilder); + + // Act + void Act() => builder.AddCosmosDb("Test"); + + // Assert + _ = Assert.Throws("builder", Act); + } + + [Test] + public void AddCosmosDb_WhenArgumentNameNull_ThrowArgumentNullException() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var services = new ServiceCollection(); + var builder = services.AddSingleton(configuration).AddHealthChecks(); + const string? name = default; + + // Act + void Act() => builder.AddCosmosDb(name!); + + // Assert + _ = Assert.Throws("name", Act); + } + + [Test] + public void AddCosmosDb_WhenArgumentNameEmpty_ThrowArgumentException() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var services = new ServiceCollection(); + var builder = services.AddSingleton(configuration).AddHealthChecks(); + var name = string.Empty; + + // Act + void Act() => builder.AddCosmosDb(name); + + // Assert + _ = Assert.Throws("name", Act); + } + + [Test] + public void AddCosmosDb_WhenArgumentTagsNull_ThrowArgumentNullException() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var services = new ServiceCollection(); + var builder = services.AddSingleton(configuration).AddHealthChecks(); + var tags = default(string[]); + + // Act + void Act() => builder.AddCosmosDb("Test", options => { }, tags!); + + // Assert + _ = Assert.Throws("tags", Act); + } + + [Test] + public void AddCosmosDb_WhenParametersValid_RegisterHealthCheck() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var services = new ServiceCollection(); + var builder = services.AddSingleton(configuration).AddHealthChecks(); + + // Act + var result = builder.AddCosmosDb("Test"); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.SameAs(builder)); + } +} \ No newline at end of file