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));
}
}