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