using Amazon.SecretsManager; using Amazon.SecretsManager.Model; using Api.SeaHavenIndustries.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using SeaHaven.Services.Configuration; using SeaHaven.Services.Interfaces; using Xunit; namespace Api.SeaHavenIndustries.Tests; public sealed class WorkOrderWebhookSecretProviderTests { [Fact] public async Task Known_key_is_cached_and_callers_receive_independent_secret_copies() { var client = new Mock(MockBehavior.Strict); client.Setup(c => c.GetSecretValueAsync( It.Is(r => r.SecretId == "arn:configured"), It.IsAny())) .ReturnsAsync(new GetSecretValueResponse { SecretString = "shared-secret" }); var provider = CreateProvider(client.Object); var first = await provider.GetSecretAsync("current", CancellationToken.None); first.Secret![0] = 0; var second = await provider.GetSecretAsync("current", CancellationToken.None); Assert.Equal(WorkOrderWebhookSecretStatus.Found, second.Status); Assert.Equal("shared-secret", System.Text.Encoding.UTF8.GetString(second.Secret!)); client.VerifyAll(); client.Verify( c => c.GetSecretValueAsync( It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Unknown_key_never_influences_an_aws_request() { var client = new Mock(MockBehavior.Strict); var provider = CreateProvider(client.Object); var result = await provider.GetSecretAsync( "attacker-controlled-key", CancellationToken.None); Assert.Equal(WorkOrderWebhookSecretStatus.UnknownKey, result.Status); client.VerifyNoOtherCalls(); } [Fact] public async Task Aws_failure_returns_unavailable_without_exposing_the_exception() { var client = new Mock(MockBehavior.Strict); client.Setup(c => c.GetSecretValueAsync( It.IsAny(), It.IsAny())) .ThrowsAsync(new InvalidOperationException("sensitive provider detail")); var provider = CreateProvider(client.Object); var result = await provider.GetSecretAsync("current", CancellationToken.None); Assert.Equal(WorkOrderWebhookSecretStatus.Unavailable, result.Status); Assert.Null(result.Secret); } private static AwsWorkOrderWebhookSecretProvider CreateProvider( IAmazonSecretsManager client) => new( client, new TestOptionsMonitor(new WorkOrderWebhookOptions { SecretCacheSeconds = 300, KeySecrets = new Dictionary { ["current"] = "arn:configured" } }), TimeProvider.System, NullLogger.Instance); private sealed class TestOptionsMonitor : IOptionsMonitor { public TestOptionsMonitor(WorkOrderWebhookOptions currentValue) { CurrentValue = currentValue; } public WorkOrderWebhookOptions CurrentValue { get; } public WorkOrderWebhookOptions Get(string? name) => CurrentValue; public IDisposable? OnChange( Action listener) => null; } }