using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.Replication; using ZB.MOM.WW.Secrets.Replicator.SqlServer.DependencyInjection; using ZB.MOM.WW.Secrets.Sqlite; namespace ZB.MOM.WW.Secrets.Replicator.SqlServer.Tests.DependencyInjection; /// /// Container-resolution tests for both SQL-Server topologies. /// /// /// These exist because a code review caught that neither mode could resolve at all: the decorators /// ask for the concrete , which the core package only registered /// behind . Every unit test passed and both modes were dead on arrival, /// because nothing built a provider. Resolving the graph is the assertion that matters. /// public sealed class AddZbSecretsSqlServerTests { private static IConfiguration Config(string sqlitePath) => new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Secrets:SqlitePath"] = sqlitePath, ["Secrets:RunMigrationsOnStartup"] = "false", ["Secrets:MasterKey:Source"] = "Environment", ["Secrets:MasterKey:EnvVarName"] = "ZB_SECRETS_TEST_KEY", ["Secrets:SqlServer:ConnectionString"] = "Server=unused;Database=x;", }).Build(); private static string TempDb() => Path.Combine(Path.GetTempPath(), $"zb-di-{Guid.NewGuid():N}.db"); [Fact] public void SharedStore_mode_resolves_the_SqlServer_store_as_ISecretStore() { var services = new ServiceCollection(); services.AddLogging(); services.AddZbSecretsSqlServerStore(Config(TempDb())); using ServiceProvider provider = services.BuildServiceProvider(); // The SQL-Server store displaces SQLite — no decorator, because there is nothing to replicate. Assert.IsType(provider.GetRequiredService()); Assert.IsType(provider.GetRequiredService()); } [Fact] public void HubReplication_mode_resolves_a_decorated_local_store() { var services = new ServiceCollection(); services.AddLogging(); services.AddZbSecretsSqlServerReplication(Config(TempDb())); using ServiceProvider provider = services.BuildServiceProvider(); Assert.IsType(provider.GetRequiredService()); Assert.IsType(provider.GetRequiredService()); // The local store is still provisioned by the core SQLite migrator, not the hub's. Assert.IsType(provider.GetRequiredService()); } [Fact] public void HubReplication_mode_registers_both_the_hub_migration_and_the_sync_service() { var services = new ServiceCollection(); services.AddLogging(); services.AddZbSecretsSqlServerReplication(Config(TempDb())); using ServiceProvider provider = services.BuildServiceProvider(); IHostedService[] hosted = [.. provider.GetServices()]; // The hub schema is nobody's "own" store, so every node must provision it at startup too. Assert.Contains(hosted, h => h is SqlServerSecretSyncService); Assert.Single(hosted, h => h.GetType().Name == "SqlServerHubMigrationHostedService"); } [Fact] public void The_decorator_wraps_the_undecorated_local_store_rather_than_itself() { var services = new ServiceCollection(); services.AddLogging(); services.AddZbSecretsSqlServerReplication(Config(TempDb())); using ServiceProvider provider = services.BuildServiceProvider(); // Guards against the self-referential registration that would stack-overflow on first use. var decorated = (ReplicatingSecretStore)provider.GetRequiredService(); Assert.NotNull(decorated); Assert.NotSame(decorated, provider.GetRequiredService()); } [Fact] public void A_missing_connection_string_fails_at_registration_not_at_first_resolve() { IConfiguration config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Secrets:SqlitePath"] = TempDb(), }) .Build(); var services = new ServiceCollection(); services.AddLogging(); InvalidOperationException ex = Assert.Throws( () => services.AddZbSecretsSqlServerStore(config)); Assert.Contains("ConnectionString", ex.Message, StringComparison.Ordinal); } }