feat(secrets): ${secret:} config expander (fail-closed)
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
||||
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<string, string?> _values;
|
||||
|
||||
public FakeSecretResolver(Dictionary<string, string?> values) => _values = values;
|
||||
|
||||
public Task<string?> 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<string, string?>();
|
||||
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<SecretNotFoundException>(
|
||||
() => 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<string, string?>
|
||||
{
|
||||
["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<string, string?>
|
||||
{
|
||||
["ConnectionStrings:Db"] = "Server=.;Password=${secret:sql/absent}",
|
||||
})
|
||||
.Build();
|
||||
|
||||
await Assert.ThrowsAsync<SecretNotFoundException>(
|
||||
() => expander.ExpandConfigurationAsync(config, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user