feat(secrets): SecretsOptions + AddZbSecrets DI + migration hosted service
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.Secrets.Abstractions;
|
||||
using ZB.MOM.WW.Secrets.DependencyInjection;
|
||||
using ZB.MOM.WW.Secrets.Sqlite;
|
||||
|
||||
namespace ZB.MOM.WW.Secrets.Tests.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="SecretsServiceCollectionExtensions.AddZbSecrets"/> wires the whole
|
||||
/// core: every seam resolves, a real secret round-trips through the composed graph, and a missing
|
||||
/// master key fails closed.
|
||||
/// </summary>
|
||||
public sealed class AddZbSecretsTests
|
||||
{
|
||||
private static string NewTempDbPath() =>
|
||||
Path.Combine(Path.GetTempPath(), $"zb-secrets-di-{Guid.NewGuid():N}.db");
|
||||
|
||||
private static string Base64Key32() => Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
private static IConfiguration BuildConfig(string sqlitePath, string envVarName) =>
|
||||
new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Secrets:SqlitePath"] = sqlitePath,
|
||||
["Secrets:MasterKey:Source"] = "Environment",
|
||||
["Secrets:MasterKey:EnvVarName"] = envVarName,
|
||||
})
|
||||
.Build();
|
||||
|
||||
[Fact]
|
||||
public void Registers_All_CoreServices()
|
||||
{
|
||||
string dbPath = NewTempDbPath();
|
||||
string envVar = $"ZB_TEST_KEY_{Guid.NewGuid():N}";
|
||||
Environment.SetEnvironmentVariable(envVar, Base64Key32());
|
||||
try
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddZbSecrets(BuildConfig(dbPath, envVar), "Secrets");
|
||||
using ServiceProvider provider = services.BuildServiceProvider();
|
||||
|
||||
Assert.NotNull(provider.GetService<ISecretResolver>());
|
||||
Assert.NotNull(provider.GetService<ISecretStore>());
|
||||
Assert.NotNull(provider.GetService<IMasterKeyProvider>());
|
||||
Assert.NotNull(provider.GetService<ISecretCipher>());
|
||||
|
||||
ISecretReplicator? replicator = provider.GetService<ISecretReplicator>();
|
||||
Assert.NotNull(replicator);
|
||||
Assert.IsType<NoOpSecretReplicator>(replicator);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable(envVar, null);
|
||||
File.Delete(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Resolver_Works_EndToEnd_ThroughDi()
|
||||
{
|
||||
string dbPath = NewTempDbPath();
|
||||
string envVar = $"ZB_TEST_KEY_{Guid.NewGuid():N}";
|
||||
Environment.SetEnvironmentVariable(envVar, Base64Key32());
|
||||
try
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddZbSecrets(BuildConfig(dbPath, envVar), "Secrets");
|
||||
using ServiceProvider provider = services.BuildServiceProvider();
|
||||
|
||||
// Run the migration first so the schema exists (as the hosted service would on startup).
|
||||
await provider.GetRequiredService<SqliteSecretsStoreMigrator>().MigrateAsync(CancellationToken.None);
|
||||
|
||||
var name = new SecretName("db/password");
|
||||
const string plaintext = "super-secret-value";
|
||||
|
||||
ISecretCipher cipher = provider.GetRequiredService<ISecretCipher>();
|
||||
ISecretStore store = provider.GetRequiredService<ISecretStore>();
|
||||
StoredSecret row = cipher.Encrypt(name, plaintext, SecretContentType.Text);
|
||||
await store.UpsertAsync(row, CancellationToken.None);
|
||||
|
||||
ISecretResolver resolver = provider.GetRequiredService<ISecretResolver>();
|
||||
string? resolved = await resolver.GetAsync(name, CancellationToken.None);
|
||||
|
||||
Assert.Equal(plaintext, resolved);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable(envVar, null);
|
||||
File.Delete(dbPath);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MasterKey_Unavailable_FailsClosed()
|
||||
{
|
||||
string dbPath = NewTempDbPath();
|
||||
string envVar = $"ZB_TEST_KEY_{Guid.NewGuid():N}";
|
||||
// Deliberately do NOT set the env var — the master key cannot be resolved.
|
||||
try
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddZbSecrets(BuildConfig(dbPath, envVar), "Secrets");
|
||||
using ServiceProvider provider = services.BuildServiceProvider();
|
||||
|
||||
IMasterKeyProvider keyProvider = provider.GetRequiredService<IMasterKeyProvider>();
|
||||
|
||||
Assert.Throws<MasterKeyUnavailableException>(() => keyProvider.GetMasterKey());
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(dbPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Xunit.SkippableFact" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user