using Microsoft.Extensions.Configuration; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.Configuration; namespace ZB.MOM.WW.Secrets.Tests.Configuration; public sealed class SecretReferenceExpanderTests { private sealed class FakeSecretResolver : ISecretResolver { private readonly Dictionary _values; public FakeSecretResolver(Dictionary values) => _values = values; public Task GetAsync(SecretName name, CancellationToken ct) => Task.FromResult(_values.TryGetValue(name.Value, out string? v) ? v : null); } private static SecretReferenceExpander Expander(params (string Name, string? Value)[] secrets) { var dict = new Dictionary(); foreach ((string name, string? value) in secrets) { dict[new SecretName(name).Value] = value; } return new SecretReferenceExpander(new FakeSecretResolver(dict)); } [Fact] public async Task ExpandAsync_SingleToken() { SecretReferenceExpander expander = Expander(("sql/foo", "hunter2")); string result = await expander.ExpandAsync("Password=${secret:sql/foo}", CancellationToken.None); Assert.Equal("Password=hunter2", result); } [Fact] public async Task ExpandAsync_MultipleTokens() { SecretReferenceExpander expander = Expander(("sql/foo", "hunter2"), ("sql/bar", "swordfish")); string result = await expander.ExpandAsync( "u=${secret:sql/foo};p=${secret:sql/bar}", CancellationToken.None); Assert.Equal("u=hunter2;p=swordfish", result); } [Fact] public async Task ExpandAsync_NoToken_Unchanged() { SecretReferenceExpander expander = Expander(("sql/foo", "hunter2")); string result = await expander.ExpandAsync("no tokens here", CancellationToken.None); Assert.Equal("no tokens here", result); } [Fact] public async Task ExpandAsync_MissingSecret_Throws() { SecretReferenceExpander expander = Expander(); SecretNotFoundException ex = await Assert.ThrowsAsync( () => expander.ExpandAsync("Password=${secret:sql/absent}", CancellationToken.None)); Assert.Contains("sql/absent", ex.Message); Assert.Contains("${secret:sql/absent}", ex.Message); } [Fact] public async Task ExpandConfigurationAsync_RewritesMatchingKeys() { SecretReferenceExpander expander = Expander(("sql/foo", "hunter2")); IConfigurationRoot config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["ConnectionStrings:Db"] = "Server=.;Password=${secret:sql/foo}", ["Plain:Value"] = "unchanged", }) .Build(); await expander.ExpandConfigurationAsync(config, CancellationToken.None); Assert.Contains("hunter2", config["ConnectionStrings:Db"]); Assert.DoesNotContain("${secret:", config["ConnectionStrings:Db"]); Assert.Equal("unchanged", config["Plain:Value"]); } [Fact] public async Task ExpandConfigurationAsync_MissingSecret_FailsClosed() { SecretReferenceExpander expander = Expander(); IConfigurationRoot config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["ConnectionStrings:Db"] = "Server=.;Password=${secret:sql/absent}", }) .Build(); await Assert.ThrowsAsync( () => expander.ExpandConfigurationAsync(config, CancellationToken.None)); } [Fact] public async Task ExpandConfigurationAsync_SkipsCommentKeys() { // A "_comment" key may document the ${secret:...} syntax with a literal (even malformed or // not-yet-seeded) example token; it must be left untouched — never resolved, never throwing — // while a real sibling key still expands. Regression: an unseeded/ellipsis example in a comment // previously crashed boot (SecretName rejects '...'). SecretReferenceExpander expander = Expander(("sql/foo", "hunter2")); IConfigurationRoot config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["_secretsComment"] = "e.g. Password=${secret:...} or ${secret:sql/not-seeded}", ["Nested:_comment"] = "also skipped: ${secret:sql/also-absent}", ["ConnectionStrings:Db"] = "Server=.;Password=${secret:sql/foo}", }) .Build(); await expander.ExpandConfigurationAsync(config, CancellationToken.None); // Comment keys untouched (literal token preserved, no throw despite absent/malformed names). Assert.Equal("e.g. Password=${secret:...} or ${secret:sql/not-seeded}", config["_secretsComment"]); Assert.Contains("${secret:sql/also-absent}", config["Nested:_comment"]); // Real key still expanded. Assert.Equal("Server=.;Password=hunter2", config["ConnectionStrings:Db"]); } }