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.Sns/SnsOptions.cs b/src/HealthChecks.Aws.Sns/SnsOptions.cs index 793988566b..22767672a0 100644 --- a/src/HealthChecks.Aws.Sns/SnsOptions.cs +++ b/src/HealthChecks.Aws.Sns/SnsOptions.cs @@ -8,6 +8,7 @@ namespace HealthChecks.Aws.Sns; /// public class SnsOptions { + public string? ServiceURL { get; set; } public AWSCredentials? Credentials { get; set; } public RegionEndpoint? RegionEndpoint { get; set; } diff --git a/src/HealthChecks.Aws.Sns/SnsTopicAndSubscriptionHealthCheck.cs b/src/HealthChecks.Aws.Sns/SnsTopicAndSubscriptionHealthCheck.cs index 8b60fa9a1e..945c20527e 100644 --- a/src/HealthChecks.Aws.Sns/SnsTopicAndSubscriptionHealthCheck.cs +++ b/src/HealthChecks.Aws.Sns/SnsTopicAndSubscriptionHealthCheck.cs @@ -6,11 +6,12 @@ namespace HealthChecks.Aws.Sns; public class SnsTopicAndSubscriptionHealthCheck : IHealthCheck { - private readonly SnsOptions _snsOptions; + private readonly SnsOptions _options; + public SnsTopicAndSubscriptionHealthCheck(SnsOptions snsOptions) { - _snsOptions = Guard.ThrowIfNull(snsOptions); + _options = Guard.ThrowIfNull(snsOptions); } /// @@ -20,7 +21,7 @@ public async Task CheckHealthAsync(HealthCheckContext context { using var client = CreateSnsClient(); - foreach (var (topicName, subscriptions) in _snsOptions.TopicsAndSubscriptions.Select(x => (x.Key, x.Value))) + foreach (var (topicName, subscriptions) in _options.TopicsAndSubscriptions.Select(x => (x.Key, x.Value))) { var topic = await client.FindTopicAsync(topicName).ConfigureAwait(false) ?? throw new NotFoundException($"Topic {topicName} does not exist."); @@ -53,14 +54,22 @@ public async Task CheckHealthAsync(HealthCheckContext context private AmazonSimpleNotificationServiceClient CreateSnsClient() { - bool credentialsProvided = _snsOptions.Credentials is not null; - bool regionProvided = _snsOptions.RegionEndpoint is not null; - return (credentialsProvided, regionProvided) switch + bool credentialsProvided = _options.Credentials is not null; + bool regionProvided = _options.RegionEndpoint is not null; + + var config = new AmazonSimpleNotificationServiceConfig(); + + if (_options.ServiceURL is not null) { - (false, false) => new AmazonSimpleNotificationServiceClient(), - (false, true) => new AmazonSimpleNotificationServiceClient(_snsOptions.RegionEndpoint), - (true, false) => new AmazonSimpleNotificationServiceClient(_snsOptions.Credentials), - (true, true) => new AmazonSimpleNotificationServiceClient(_snsOptions.Credentials, _snsOptions.RegionEndpoint) - }; + config.ServiceURL = _options.ServiceURL; + } + if (_options.RegionEndpoint is not null) + { + config.RegionEndpoint = _options.RegionEndpoint; + } + + return _options.Credentials is not null + ? new AmazonSimpleNotificationServiceClient(_options.Credentials, config) + : new AmazonSimpleNotificationServiceClient(config); } } diff --git a/test/HealthChecks.Aws.Sns.Tests/Functional/SnsHealthCheckTests.cs b/test/HealthChecks.Aws.Sns.Tests/Functional/SnsHealthCheckTests.cs new file mode 100644 index 0000000000..884c49fb56 --- /dev/null +++ b/test/HealthChecks.Aws.Sns.Tests/Functional/SnsHealthCheckTests.cs @@ -0,0 +1,176 @@ +using System.Net; +using Amazon.Runtime; +using HealthChecks.Aws.Sns.Tests; + +namespace HealthChecks.Aws.Sqs.Tests.Functional; + +public class aws_sqs_healthcheck_should(LocalStackContainerFixture localStackFixture) : IClassFixture +{ + [Fact] + public async Task be_healthy_if_aws_sns_topic_is_available() + { + string connectionString = localStackFixture.GetConnectionString(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSnsTopicsAndSubscriptions( + options => + { + options.Credentials = new BasicAWSCredentials("test", "test"); + options.ServiceURL = connectionString; + + options.AddTopicAndSubscriptions("healthchecks"); + }, + tags: ["sns"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sns") + }); + }); + + 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_sns_multiple_queues_are_available() + { + string connectionString = localStackFixture.GetConnectionString(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSnsTopicsAndSubscriptions( + options => + { + options.Credentials = new BasicAWSCredentials("test", "test"); + options.ServiceURL = connectionString; + + options.AddTopicAndSubscriptions("healthchecks"); + options.AddTopicAndSubscriptions("healthchecks"); + }, + tags: ["sns"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sns") + }); + }); + + 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_sns_is_unavailable() + { + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSnsTopicsAndSubscriptions( + options => + { + options.Credentials = new BasicAWSCredentials("test", "test"); + options.ServiceURL = "invalid"; + + options.AddTopicAndSubscriptions("healthchecks"); + }, + tags: ["sns"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sns") + }); + }); + + 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_sns_credentials_not_provided() + { + string connectionString = localStackFixture.GetConnectionString(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSnsTopicsAndSubscriptions( + options => + { + options.ServiceURL = connectionString; + + options.AddTopicAndSubscriptions("healthchecks"); + }, + tags: ["sns"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sns") + }); + }); + + 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_sns_topic_does_not_exist() + { + string connectionString = localStackFixture.GetConnectionString(); + + var webHostBuilder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddHealthChecks() + .AddSnsTopicsAndSubscriptions( + options => + { + options.Credentials = new BasicAWSCredentials("test", "test"); + options.ServiceURL = connectionString; + + options.AddTopicAndSubscriptions("invalid"); + }, + tags: ["sns"]); + }) + .Configure(app => + { + app.UseHealthChecks("/health", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("sns") + }); + }); + + 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.Sns.Tests/HealthChecks.Aws.Sns.Tests.csproj b/test/HealthChecks.Aws.Sns.Tests/HealthChecks.Aws.Sns.Tests.csproj index 3f66b2f33d..aeb5865e0e 100644 --- a/test/HealthChecks.Aws.Sns.Tests/HealthChecks.Aws.Sns.Tests.csproj +++ b/test/HealthChecks.Aws.Sns.Tests/HealthChecks.Aws.Sns.Tests.csproj @@ -1,5 +1,8 @@ + + + diff --git a/test/HealthChecks.Aws.Sns.Tests/HealthChecks.Aws.Sns.approved.txt b/test/HealthChecks.Aws.Sns.Tests/HealthChecks.Aws.Sns.approved.txt index f2b8e04fd7..67841ab7c5 100644 --- a/test/HealthChecks.Aws.Sns.Tests/HealthChecks.Aws.Sns.approved.txt +++ b/test/HealthChecks.Aws.Sns.Tests/HealthChecks.Aws.Sns.approved.txt @@ -5,6 +5,7 @@ namespace HealthChecks.Aws.Sns public SnsOptions() { } public Amazon.Runtime.AWSCredentials? Credentials { get; set; } public Amazon.RegionEndpoint? RegionEndpoint { get; set; } + public string? ServiceURL { get; set; } public HealthChecks.Aws.Sns.SnsOptions AddTopicAndSubscriptions(string topicName, System.Collections.Generic.IEnumerable? subscriptions = null) { } } public class SnsTopicAndSubscriptionHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck diff --git a/test/HealthChecks.Aws.Sns.Tests/LocalStackContainerFixture.cs b/test/HealthChecks.Aws.Sns.Tests/LocalStackContainerFixture.cs new file mode 100644 index 0000000000..ffc261b652 --- /dev/null +++ b/test/HealthChecks.Aws.Sns.Tests/LocalStackContainerFixture.cs @@ -0,0 +1,45 @@ +using Testcontainers.LocalStack; + + +namespace HealthChecks.Aws.Sns.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", "sns", "create-topic", "--topic-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; + } +}