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; /// /// Verifies that wires the whole /// core: every seam resolves, a real secret round-trips through the composed graph, and a missing /// master key fails closed. /// 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 { ["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()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); ISecretReplicator? replicator = provider.GetService(); Assert.NotNull(replicator); Assert.IsType(replicator); } finally { Environment.SetEnvironmentVariable(envVar, null); File.Delete(dbPath); } } [Fact] public void CacheInvalidator_And_Resolver_AreSameSingletonInstance() { 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(); var resolver = provider.GetService(); var invalidator = provider.GetService(); Assert.NotNull(resolver); Assert.NotNull(invalidator); // Same object: an invalidation MUST hit the exact cache the resolver reads from. Assert.Same(resolver, invalidator); } 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().MigrateAsync(CancellationToken.None); var name = new SecretName("db/password"); const string plaintext = "super-secret-value"; ISecretCipher cipher = provider.GetRequiredService(); ISecretStore store = provider.GetRequiredService(); StoredSecret row = cipher.Encrypt(name, plaintext, SecretContentType.Text); await store.UpsertAsync(row, CancellationToken.None); ISecretResolver resolver = provider.GetRequiredService(); 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(); Assert.Throws(() => keyProvider.GetMasterKey()); } finally { File.Delete(dbPath); } } }