using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.DependencyInjection;
using ZB.MOM.WW.Secrets.Replicator.SqlServer.DependencyInjection;
namespace ZB.MOM.WW.Secrets.Replicator.SqlServer.Tests.DependencyInjection;
///
/// scadaproj#31 (Secrets-002): in hub-replication mode the node's local SQLite store is migrated by
/// a pre-host ${secret:} expander, so the under-content-root guard must be reachable — either
/// through the extension's own contentRootPath or through a 4-arg AddZbSecrets in
/// either order. Before the fix no registration order applied it: the extension's internal 3-arg
/// call captured a null root first, and the core-first order lost the replicator (scadaproj#30).
///
///
/// Every container here is pre-host — no IHostEnvironment — which is the case the guard
/// exists for. The store file is asserted absent BEFORE the exception is asserted, so a regression
/// that stops the throw but still creates the database fails on the artifact.
///
public sealed class ContentRootTests : IDisposable
{
private readonly string _contentRoot =
Path.Combine(Path.GetTempPath(), $"zb-sqlsrv-root-{Guid.NewGuid():N}");
public ContentRootTests() => Directory.CreateDirectory(_contentRoot);
public enum Composition
{
ExtensionArgument,
ExtensionThenCore,
CoreThenExtension,
}
private IConfiguration Config(string sqlitePath) =>
new ConfigurationBuilder().AddInMemoryCollection(new Dictionary
{
["Secrets:SqlitePath"] = sqlitePath,
["Secrets:RunMigrationsOnStartup"] = "false",
["Secrets:SqlServer:ConnectionString"] = "Server=unused;Database=x;",
}).Build();
private ServiceProvider Compose(Composition composition, IConfiguration config)
{
var services = new ServiceCollection();
services.AddLogging();
switch (composition)
{
case Composition.ExtensionArgument:
services.AddZbSecretsSqlServerReplication(config, contentRootPath: _contentRoot);
break;
case Composition.ExtensionThenCore:
services.AddZbSecretsSqlServerReplication(config);
services.AddZbSecrets(config, "Secrets", _contentRoot);
break;
case Composition.CoreThenExtension:
services.AddZbSecrets(config, "Secrets", _contentRoot);
services.AddZbSecretsSqlServerReplication(config);
break;
}
return services.BuildServiceProvider();
}
[Theory]
[InlineData(Composition.ExtensionArgument)]
[InlineData(Composition.ExtensionThenCore)]
[InlineData(Composition.CoreThenExtension)]
public async Task Hub_mode_refuses_a_local_store_inside_the_content_root(Composition composition)
{
string dbPath = Path.Combine(_contentRoot, "data", "s.db");
using ServiceProvider provider = Compose(composition, Config(dbPath));
Exception? thrown = await Record.ExceptionAsync(
() => provider.GetRequiredService().MigrateAsync(default));
Assert.False(File.Exists(dbPath), "a rejected path must not be left holding a store file");
Assert.IsType(thrown);
}
[Theory]
[InlineData(Composition.ExtensionArgument)]
[InlineData(Composition.ExtensionThenCore)]
[InlineData(Composition.CoreThenExtension)]
public void Hub_mode_accepts_a_store_outside_the_content_root(Composition composition)
{
// Positive control: the same compositions with a legal path resolve cleanly, so the refusal
// above is the content-root rule and not some other failure of the composition.
string dbPath = Path.Combine(Path.GetTempPath(), $"zb-sqlsrv-outside-{Guid.NewGuid():N}.db");
using ServiceProvider provider = Compose(composition, Config(dbPath));
Assert.Equal(dbPath, provider.GetRequiredService>().Value.SqlitePath);
}
[Fact]
public void Shared_store_mode_forwards_the_content_root_too()
{
string dbPath = Path.Combine(_contentRoot, "s.db");
var services = new ServiceCollection();
services.AddLogging();
services.AddZbSecretsSqlServerStore(Config(dbPath), contentRootPath: _contentRoot);
using ServiceProvider provider = services.BuildServiceProvider();
Assert.Throws(
() => provider.GetRequiredService>().Value);
}
public void Dispose()
{
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
Directory.Delete(_contentRoot, recursive: true);
}
}