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.Sqlite; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// DI-resolution tests for . /// /// These deliberately BUILD a container and RESOLVE out of it rather than asserting over /// s. A decorator registration can look perfectly correct as a /// descriptor list and still throw on the first resolve because the undecorated concrete store it /// depends on is missing — that exact gap shipped once in this library, with every descriptor-level /// unit test green. Resolution is the only assertion that catches it. /// /// /// Registration must not need a reachable SQL Server: the SQL-Server package validates its options /// eagerly, but validation only checks that the connection string is non-empty and the schema name /// is a legal identifier. Nothing connects until a sweep or a write runs, and these tests do /// neither — so the dummy connection string below never resolves to a real host. /// /// public class SecretsReplicationWiringTests { // Syntactically valid, deliberately unreachable. Never connected to by these tests. private const string DummyConnectionString = "Server=unused;Database=x;Integrated Security=true;"; private static IConfiguration BuildConfig(params (string Key, string Value)[] overrides) { var settings = new Dictionary { ["Secrets:SqlitePath"] = Path.Combine(Path.GetTempPath(), "sb-secrets-wiring-test.db"), ["Secrets:MasterKey:Source"] = "Environment", ["Secrets:MasterKey:EnvVarName"] = "ZB_SECRETS_MASTER_KEY", ["Secrets:RunMigrationsOnStartup"] = "false", }; foreach ((string key, string value) in overrides) { settings[key] = value; } return new ConfigurationBuilder().AddInMemoryCollection(settings).Build(); } private static ServiceProvider BuildProvider(IConfiguration config) { var services = new ServiceCollection(); services.AddLogging(); services.AddScadaBridgeSecrets(config); return services.BuildServiceProvider(validateScopes: true); } /// /// Any hosted service contributed by the SQL-Server replicator package. Asserted by assembly /// rather than by type because SqlServerHubMigrationHostedService is internal to it. /// private static bool IsReplicatorHostedService(ServiceDescriptor descriptor) => descriptor.ServiceType == typeof(IHostedService) && descriptor.ImplementationType?.Assembly.GetName().Name == "ZB.MOM.WW.Secrets.Replicator.SqlServer"; [Fact] public void ReplicationDisabledByDefault_ResolvesPlainSqliteStore() { using ServiceProvider provider = BuildProvider(BuildConfig()); var store = provider.GetRequiredService(); Assert.IsType(store); Assert.IsNotType(store); } [Fact] public void ReplicationDisabledByDefault_RegistersNoReplicatorHostedServices() { var services = new ServiceCollection(); services.AddLogging(); services.AddScadaBridgeSecrets(BuildConfig()); Assert.DoesNotContain(services, IsReplicatorHostedService); } [Fact] public void ReplicationEnabledWithoutConnectionString_StaysOnPlainSqliteStore() { // The gate requires BOTH the flag and a connection string. Enabling the flag alone must not // reach the SQL-Server package, whose options validation would throw at registration and // take the whole node down at startup. var services = new ServiceCollection(); services.AddLogging(); IConfiguration config = BuildConfig(("Secrets:Replication:Enabled", "true")); services.AddScadaBridgeSecrets(config); using ServiceProvider provider = services.BuildServiceProvider(validateScopes: true); Assert.IsType(provider.GetRequiredService()); Assert.DoesNotContain(services, IsReplicatorHostedService); } [Fact] public void ReplicationEnabledWithConnectionString_ResolvesReplicatingStore() { IConfiguration config = BuildConfig( ("Secrets:Replication:Enabled", "true"), ("Secrets:SqlServer:ConnectionString", DummyConnectionString)); using ServiceProvider provider = BuildProvider(config); Assert.IsType(provider.GetRequiredService()); } [Fact] public void ReplicationEnabledWithConnectionString_UndecoratedLocalStoreAlsoResolves() { // ReplicatingSecretStore is constructed from the CONCRETE SqliteSecretStore, not from // ISecretStore (which is the decorator itself — that would recurse). If the concrete // registration were ever dropped, the decorator would fail on first resolve. This is the // exact defect that shipped once, so it gets its own test. IConfiguration config = BuildConfig( ("Secrets:Replication:Enabled", "true"), ("Secrets:SqlServer:ConnectionString", DummyConnectionString)); using ServiceProvider provider = BuildProvider(config); Assert.NotNull(provider.GetRequiredService()); } [Fact] public void ReplicationEnabledWithConnectionString_RegistersReplicatorHostedServices() { var services = new ServiceCollection(); services.AddLogging(); IConfiguration config = BuildConfig( ("Secrets:Replication:Enabled", "true"), ("Secrets:SqlServer:ConnectionString", DummyConnectionString)); services.AddScadaBridgeSecrets(config); // Both the hub-schema migration service and the bidirectional sweep. Assert.Equal(2, services.Count(IsReplicatorHostedService)); } }