From 67ded31cba74b21d63956ecb85ac15b074b17abb Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 30 May 2026 08:12:12 +0200 Subject: [PATCH] Increase secrets unit coverage (#7545) --- .../SecretModelMapperTests.cs | 32 +++++++++++ .../SecretStoreTests.cs | 57 +++++++++++++++++++ .../SecretValueProtectorTests.cs | 52 +++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 test/unit/Elsa.Secrets.UnitTests/SecretModelMapperTests.cs create mode 100644 test/unit/Elsa.Secrets.UnitTests/SecretValueProtectorTests.cs diff --git a/test/unit/Elsa.Secrets.UnitTests/SecretModelMapperTests.cs b/test/unit/Elsa.Secrets.UnitTests/SecretModelMapperTests.cs new file mode 100644 index 000000000..da588bfc6 --- /dev/null +++ b/test/unit/Elsa.Secrets.UnitTests/SecretModelMapperTests.cs @@ -0,0 +1,32 @@ +using Elsa.Secrets.Models; +using Elsa.Secrets.Services; +using Xunit; + +namespace Elsa.Secrets.UnitTests; + +public class SecretModelMapperTests +{ + [Fact] + public void ToModel_ReportsExpired_WhenActiveSecretHasOnlyExpiredVersions() + { + var secret = new Secret + { + Name = "smtp:password", + DisplayName = "SMTP password", + Versions = + { + new SecretVersion + { + Version = 1, + ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(-1) + } + } + }; + + var model = secret.ToModel(); + + Assert.Equal(SecretStatus.Expired, model.Status); + Assert.Null(model.CurrentVersion); + Assert.Null(model.ExpiresAt); + } +} diff --git a/test/unit/Elsa.Secrets.UnitTests/SecretStoreTests.cs b/test/unit/Elsa.Secrets.UnitTests/SecretStoreTests.cs index fbcf9b67f..a4a00effe 100644 --- a/test/unit/Elsa.Secrets.UnitTests/SecretStoreTests.cs +++ b/test/unit/Elsa.Secrets.UnitTests/SecretStoreTests.cs @@ -40,6 +40,51 @@ public class SecretStoreTests Assert.Equal("configured-secret", value); } + [Fact] + public async Task ConfigurationStore_FallsBackToRootConfigurationKey() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { ["SmtpPassword"] = "root-configured-secret" }) + .Build(); + var fixture = new SecretTestFixture(configuration); + + var secret = await fixture.Manager.CreateAsync(new CreateSecretRequest + { + Name = "smtp:password", + StoreName = SecretStoreNames.Configuration, + ConfigurationKey = " SmtpPassword " + }); + var value = await fixture.Resolver.ResolveAsync("smtp:password"); + + Assert.Equal("root-configured-secret", value); + Assert.Null(secret.Versions.Single().Payload.Value); + Assert.Equal("SmtpPassword", secret.Versions.Single().Payload.Metadata["configurationKey"]); + } + + [Fact] + public async Task ConfigurationStore_RotateAsync_UsesReplacementConfigurationKey() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Elsa:Secrets:OldPassword"] = "old-configured-secret", + ["Elsa:Secrets:NewPassword"] = "new-configured-secret" + }) + .Build(); + var fixture = new SecretTestFixture(configuration); + + await fixture.Manager.CreateAsync(new CreateSecretRequest + { + Name = "smtp:password", + StoreName = SecretStoreNames.Configuration, + ConfigurationKey = "OldPassword" + }); + await fixture.Manager.RotateAsync("smtp:password", new RotateSecretRequest { ConfigurationKey = "NewPassword" }); + var value = await fixture.Resolver.ResolveAsync("smtp:password"); + + Assert.Equal("new-configured-secret", value); + } + [Fact] public async Task ConfigurationStore_TestAsync_ReturnsFalseWhenConfiguredValueIsMissing() { @@ -57,6 +102,18 @@ public class SecretStoreTests Assert.Equal("Secret value is unavailable.", result.Error); } + [Fact] + public void Registries_Throw_WhenTypeOrStoreIsMissing() + { + var fixture = new SecretTestFixture(); + + var missingType = Assert.Throws(() => fixture.TypeRegistry.Get("missing-type")); + var missingStore = Assert.Throws(() => fixture.StoreRegistry.Get("missing-store")); + + Assert.Contains("missing-type", missingType.Message); + Assert.Contains("missing-store", missingStore.Message); + } + [Fact] public async Task FileRepository_PersistsSecretAggregate() { diff --git a/test/unit/Elsa.Secrets.UnitTests/SecretValueProtectorTests.cs b/test/unit/Elsa.Secrets.UnitTests/SecretValueProtectorTests.cs new file mode 100644 index 000000000..47ec88f96 --- /dev/null +++ b/test/unit/Elsa.Secrets.UnitTests/SecretValueProtectorTests.cs @@ -0,0 +1,52 @@ +using Elsa.Secrets.Options; +using Elsa.Secrets.Services; +using Xunit; + +namespace Elsa.Secrets.UnitTests; + +public class SecretValueProtectorTests +{ + [Fact] + public void Protect_AndUnprotect_RoundTripsValue() + { + var protector = CreateProtector("0123456789abcdef0123456789abcdef"u8.ToArray()); + + var protectedValue = protector.Protect("plain-text-secret"); + var value = protector.Unprotect(protectedValue); + + Assert.NotEqual("plain-text-secret", protectedValue); + Assert.Equal("plain-text-secret", value); + } + + [Theory] + [InlineData(null)] + [InlineData(0)] + [InlineData(15)] + [InlineData(17)] + public void Protect_RejectsMissingOrInvalidEncryptionKey(int? keyLength) + { + var protector = CreateProtector(keyLength == null ? null : new byte[keyLength.Value]); + + var exception = Assert.Throws(() => protector.Protect("secret")); + + Assert.Contains("encryption key", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData("")] + [InlineData("v2.payload")] + [InlineData("v1.payload")] + public void Unprotect_RejectsUnsupportedPayloadFormat(string protectedValue) + { + var protector = CreateProtector("0123456789abcdef0123456789abcdef"u8.ToArray()); + + var exception = Assert.Throws(() => protector.Unprotect(protectedValue)); + + Assert.Contains("not supported", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + private static DefaultSecretValueProtector CreateProtector(byte[]? key) + { + return new DefaultSecretValueProtector(Microsoft.Extensions.Options.Options.Create(new SecretsOptions { EncryptionKey = key })); + } +}