diff --git a/Directory.Packages.props b/Directory.Packages.props index 2c2207765a..827e471330 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -105,6 +105,7 @@ + diff --git a/src/HealthChecks.Aws.Sqs/README.md b/src/HealthChecks.Aws.Sqs/README.md index e0b87da02f..7d80ab5e30 100644 --- a/src/HealthChecks.Aws.Sqs/README.md +++ b/src/HealthChecks.Aws.Sqs/README.md @@ -13,7 +13,7 @@ With all of the following examples, you can additionally add the following param ### Basic -### Check existence of a queue and load credentials from the application's default configuration +### Check the existence of a queue and load credentials from the application's default configuration ```csharp public void ConfigureServices(IServiceCollection services) @@ -27,7 +27,7 @@ public void ConfigureServices(IServiceCollection services) } ``` -### Check existence of a queue and directly pass credentials +### Check the existence of a queue and directly pass credentials ```csharp public void ConfigureServices(IServiceCollection services) @@ -42,7 +42,7 @@ public void ConfigureServices(IServiceCollection services) } ``` -### Check existence of a queue and specify region endpoint +### Check the existence of a queue and specify region endpoint ```csharp public void ConfigureServices(IServiceCollection services) @@ -57,7 +57,7 @@ public void ConfigureServices(IServiceCollection services) } ``` -### Check existence of a queue and specify credentials with region endpoint +### Check the existence of a queue and specify credentials with region endpoint ```csharp public void ConfigureServices(IServiceCollection services) @@ -72,3 +72,18 @@ public void ConfigureServices(IServiceCollection services) }); } ``` + +### Check the existence of a queue and specify service URL + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services + .AddHealthChecks() + .AddSqs(options => + { + options.AddQueue("queueName"); + options.ServiceURL = "http://localhost:4566"; + }); +} +``` diff --git a/src/HealthChecks.Aws.Sqs/SqsHealthCheck.cs b/src/HealthChecks.Aws.Sqs/SqsHealthCheck.cs index 59889524e5..c960b626e6 100644 --- a/src/HealthChecks.Aws.Sqs/SqsHealthCheck.cs +++ b/src/HealthChecks.Aws.Sqs/SqsHealthCheck.cs @@ -5,11 +5,11 @@ namespace HealthChecks.Aws.Sqs; public class SqsHealthCheck : IHealthCheck { - private readonly SqsOptions _sqsOptions; + private readonly SqsOptions _options; public SqsHealthCheck(SqsOptions sqsOptions) { - _sqsOptions = Guard.ThrowIfNull(sqsOptions); + _options = Guard.ThrowIfNull(sqsOptions); } /// @@ -18,29 +18,36 @@ public async Task CheckHealthAsync(HealthCheckContext context try { using var client = CreateSqsClient(); - foreach (var queueName in _sqsOptions.Queues) + + foreach (string queueName in _options.Queues) { - _ = await client.GetQueueUrlAsync(queueName).ConfigureAwait(false); + await client.GetQueueUrlAsync(queueName, cancellationToken).ConfigureAwait(false); } return HealthCheckResult.Healthy(); } - catch (Exception ex) + catch (Exception e) { - return new HealthCheckResult(context.Registration.FailureStatus, exception: ex); + return new HealthCheckResult(context.Registration.FailureStatus, exception: e); } } - private IAmazonSQS CreateSqsClient() + private AmazonSQSClient CreateSqsClient() { - var credentialsProvided = _sqsOptions.Credentials is not null; - var regionProvided = _sqsOptions.RegionEndpoint is not null; - return (credentialsProvided, regionProvided) switch + var config = new AmazonSQSConfig(); + + if (_options.ServiceURL is not null) { - (false, false) => new AmazonSQSClient(), - (false, true) => new AmazonSQSClient(_sqsOptions.RegionEndpoint), - (true, false) => new AmazonSQSClient(_sqsOptions.Credentials), - (true, true) => new AmazonSQSClient(_sqsOptions.Credentials, _sqsOptions.RegionEndpoint) - }; + config.ServiceURL = _options.ServiceURL; + } + + if (_options.RegionEndpoint is not null) + { + config.RegionEndpoint = _options.RegionEndpoint; + } + + return _options.Credentials is not null + ? new AmazonSQSClient(_options.Credentials, config) + : new AmazonSQSClient(config); } } diff --git a/src/HealthChecks.Aws.Sqs/SqsOptions.cs b/src/HealthChecks.Aws.Sqs/SqsOptions.cs index b2549ad381..33b01df3cd 100644 --- a/src/HealthChecks.Aws.Sqs/SqsOptions.cs +++ b/src/HealthChecks.Aws.Sqs/SqsOptions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Amazon; using Amazon.Runtime; @@ -8,11 +9,14 @@ namespace HealthChecks.Aws.Sqs; /// public class SqsOptions { + [SuppressMessage("ReSharper", "InconsistentNaming")] + public string? ServiceURL { get; set; } + public AWSCredentials? Credentials { get; set; } public RegionEndpoint? RegionEndpoint { get; set; } - internal HashSet Queues { get; } = new HashSet(); + internal HashSet Queues { get; } = []; /// /// Add an AWS SQS queue to be checked. diff --git a/test/HealthChecks.Aws.Sqs.Tests/Functional/SqsHealthCheckTests.cs b/test/HealthChecks.Aws.Sqs.Tests/Functional/SqsHealthCheckTests.cs new file mode 100644 index 0000000000..e602ed0e29 --- /dev/null +++ b/test/HealthChecks.Aws.Sqs.Tests/Functional/SqsHealthCheckTests.cs @@ -0,0 +1,175 @@ +using System.Net; +using Amazon.Runtime; + +namespace HealthChecks.Aws.Sqs.Tests.Functional; + +public class aws_sqs_healthcheck_should(LocalStackContainerFixture localStackFixture) : IClassFixture +{ + [Fact] + public async Task be_healthy_if_aws_sqs_queue_is_available() + { + string connectionString = localStackFixture.GetConnectionString(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSqs( + options => + { + options.Credentials = new BasicAWSCredentials("test", "test"); + options.ServiceURL = connectionString; + + options.AddQueue("healthchecks"); + }, + tags: ["sqs"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sqs") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_healthy_if_aws_sqs_multiple_queues_are_available() + { + string connectionString = localStackFixture.GetConnectionString(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSqs( + options => + { + options.Credentials = new BasicAWSCredentials("test", "test"); + options.ServiceURL = connectionString; + + options.AddQueue("healthchecks"); + options.AddQueue("healthchecks"); + }, + tags: ["sqs"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sqs") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + [Fact] + public async Task be_unhealthy_if_aws_sqs_is_unavailable() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSqs( + options => + { + options.Credentials = new BasicAWSCredentials("test", "test"); + options.ServiceURL = "invalid"; + + options.AddQueue("healthchecks"); + }, + tags: ["sqs"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sqs") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } + + [Fact] + public async Task be_unhealthy_if_aws_sqs_credentials_not_provided() + { + string connectionString = localStackFixture.GetConnectionString(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSqs( + options => + { + options.ServiceURL = connectionString; + + options.AddQueue("healthchecks"); + }, + tags: ["sqs"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sqs") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } + + [Fact] + public async Task be_unhealthy_if_aws_sqs_queue_does_not_exist() + { + string connectionString = localStackFixture.GetConnectionString(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSqs( + options => + { + options.Credentials = new BasicAWSCredentials("test", "test"); + options.ServiceURL = connectionString; + + options.AddQueue("invalid"); + }, + tags: ["sqs"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sqs") + }); + }); + + using var server = new TestServer(webHostBuilder); + + using var response = await server.CreateRequest("/health").GetAsync(); + + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + } +} diff --git a/test/HealthChecks.Aws.Sqs.Tests/HealthChecks.Aws.Sqs.Tests.csproj b/test/HealthChecks.Aws.Sqs.Tests/HealthChecks.Aws.Sqs.Tests.csproj index 92e6e96ddd..c16df9aec1 100644 --- a/test/HealthChecks.Aws.Sqs.Tests/HealthChecks.Aws.Sqs.Tests.csproj +++ b/test/HealthChecks.Aws.Sqs.Tests/HealthChecks.Aws.Sqs.Tests.csproj @@ -4,4 +4,8 @@ + + + + diff --git a/test/HealthChecks.Aws.Sqs.Tests/HealthChecks.Aws.Sqs.approved.txt b/test/HealthChecks.Aws.Sqs.Tests/HealthChecks.Aws.Sqs.approved.txt index 6b3f47151d..5b99739720 100644 --- a/test/HealthChecks.Aws.Sqs.Tests/HealthChecks.Aws.Sqs.approved.txt +++ b/test/HealthChecks.Aws.Sqs.Tests/HealthChecks.Aws.Sqs.approved.txt @@ -10,6 +10,7 @@ namespace HealthChecks.Aws.Sqs public SqsOptions() { } public Amazon.Runtime.AWSCredentials? Credentials { get; set; } public Amazon.RegionEndpoint? RegionEndpoint { get; set; } + public string? ServiceURL { get; set; } public HealthChecks.Aws.Sqs.SqsOptions AddQueue(string queueName) { } } } diff --git a/test/HealthChecks.Aws.Sqs.Tests/LocalStackContainerFixture.cs b/test/HealthChecks.Aws.Sqs.Tests/LocalStackContainerFixture.cs new file mode 100644 index 0000000000..adcd6d67c0 --- /dev/null +++ b/test/HealthChecks.Aws.Sqs.Tests/LocalStackContainerFixture.cs @@ -0,0 +1,44 @@ +using Testcontainers.LocalStack; + +namespace HealthChecks.Aws.Sqs.Tests; + +public class LocalStackContainerFixture : IAsyncLifetime +{ + private const string Registry = "docker.io"; + + private const string Image = "localstack/localstack"; + + private const string Tag = "4.7.0"; + + public LocalStackContainer? Container { get; private set; } + + public string GetConnectionString() + { + if (Container is null) + { + throw new InvalidOperationException("The test container was not initialized."); + } + + return Container.GetConnectionString(); + } + + public async Task InitializeAsync() + { + Container = await CreateContainerAsync(); + + await Container.ExecAsync(["awslocal", "sqs", "create-queue", "--queue-name", "healthchecks"]); + } + + public Task DisposeAsync() => Container?.DisposeAsync().AsTask() ?? Task.CompletedTask; + + private static async Task CreateContainerAsync() + { + var container = new LocalStackBuilder() + .WithImage($"{Registry}/{Image}:{Tag}") + .Build(); + + await container.StartAsync(); + + return container; + } +}