c3b07d6766
The SQL-Server extensions called the 3-arg AddZbSecrets, and the store-path validator captured the content root of whichever call registered it first. In hub mode no registration order applied the pre-host under-content-root guard: extension-first dropped the app's 4-arg root, core-first lost the replicator (#30). That is the 0.6.0 store-path hole, reopened. - AddZbSecretsSqlServerStore / AddZbSecretsSqlServerReplication / AddZbSecretsAkkaReplication gain a trailing optional contentRootPath, forwarded to the 4-arg AddZbSecrets - the root now lives in one SecretsContentRoot instance holder per service collection; the last explicit root wins, null never erases one; the IHostEnvironment fallback is unchanged - tests: ContentRootTests (hub mode, three compositions, artifact asserted before the throw, positive control), last-explicit-wins theory in SecretsStorePathRulesTests, Akka forwarding test - code-reviews: Secrets-002 Resolved; README regenerated Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
135 lines
6.0 KiB
C#
135 lines
6.0 KiB
C#
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.Replication;
|
|
using ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.DependencyInjection;
|
|
using ZB.MOM.WW.Secrets.Sqlite;
|
|
|
|
namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Tests.DependencyInjection;
|
|
|
|
/// <summary>
|
|
/// Registration tests for <c>AddZbSecretsAkkaReplication</c>.
|
|
/// <para>
|
|
/// These exist because 0.2.0 shipped with Akka replication silently inert and every other test in
|
|
/// this assembly still passed. The bug was pure registration ORDER: the extension called
|
|
/// <c>AddZbSecrets</c> first, which does
|
|
/// <c>TryAddSingleton<ISecretReplicator, NoOpSecretReplicator>()</c>, so the package's own
|
|
/// <c>TryAddSingleton<ISecretReplicator></c> found a descriptor already present and was
|
|
/// silently discarded. Writes then published into a no-op sink and no replication actor was ever
|
|
/// spawned — with no exception and no log line anywhere.
|
|
/// </para>
|
|
/// <para>
|
|
/// The equivalent SQL-Server package always had a test asserting its replicator type, and always
|
|
/// had the correct order. That asymmetry is exactly why the defect landed in one package and not
|
|
/// the other, so the gap is closed here.
|
|
/// </para>
|
|
/// <para>
|
|
/// These assert against the <see cref="IServiceCollection"/> because the 0.2.0 defect was entirely
|
|
/// about which descriptor wins, and the descriptor is where it is visible. Resolution through a
|
|
/// BUILT provider — where the scadaproj#1 singleton-cycle deadlock lived — is covered by
|
|
/// <see cref="HostedProcessResolutionTests"/>, and end-to-end actor behaviour by
|
|
/// <c>TwoNodeClusterReplicationTests</c>. (An earlier revision of this comment claimed a
|
|
/// provider-based test was impossible because <c>DistributedPubSub.Get</c> needs a joined
|
|
/// cluster; the hang it was avoiding was actually that DI deadlock, and a single self-joined
|
|
/// node is all the cluster the actor needs.)
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class AddZbSecretsAkkaReplicationTests
|
|
{
|
|
private static IServiceCollection Register()
|
|
{
|
|
IConfigurationRoot config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["Secrets:SqlitePath"] = Path.Combine(Path.GetTempPath(), $"zb-akkadi-{Guid.NewGuid():N}.db"),
|
|
["Secrets:Replication:AnnounceInterval"] = "00:00:30",
|
|
})
|
|
.Build();
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddZbSecretsAkkaReplication(config, "Secrets", "Secrets:Replication");
|
|
return services;
|
|
}
|
|
|
|
private static ServiceDescriptor Descriptor<T>(IServiceCollection services) =>
|
|
services.Single(d => d.ServiceType == typeof(T));
|
|
|
|
[Fact]
|
|
public void ISecretReplicator_is_not_bound_to_the_no_op_sink()
|
|
{
|
|
// THE REGRESSION TEST. Before the fix this descriptor was NoOpSecretReplicator, so every
|
|
// replicated write went nowhere. A no-op secret replicator is the worst failure mode
|
|
// available: the cluster reports healthy and silently never converges.
|
|
ServiceDescriptor descriptor = Descriptor<ISecretReplicator>(Register());
|
|
|
|
Assert.NotEqual(typeof(NoOpSecretReplicator), descriptor.ImplementationType);
|
|
}
|
|
|
|
[Fact]
|
|
public void ISecretReplicator_is_registered_by_this_package_via_a_factory()
|
|
{
|
|
// The package registers through a factory (it needs the lazily-created actor ref), whereas
|
|
// the core no-op is registered by concrete type. A factory registration is therefore proof
|
|
// that this package's descriptor won the TryAdd race rather than core's.
|
|
ServiceDescriptor descriptor = Descriptor<ISecretReplicator>(Register());
|
|
|
|
Assert.NotNull(descriptor.ImplementationFactory);
|
|
Assert.Null(descriptor.ImplementationType);
|
|
}
|
|
|
|
[Fact]
|
|
public void ISecretStore_is_decorated_so_writes_actually_publish()
|
|
{
|
|
// AddSingleton (not TryAdd) means the decorator is appended, and the LAST registration for
|
|
// a service type is what the container resolves. If this regressed to the bare SQLite
|
|
// store, writes would never publish.
|
|
ServiceDescriptor last = Register()
|
|
.Last(d => d.ServiceType == typeof(ISecretStore));
|
|
|
|
Assert.NotNull(last.ImplementationFactory);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_undecorated_concrete_store_is_registered()
|
|
{
|
|
// ReplicatingSecretStore resolves SqliteSecretStore by concrete type. Core registering it
|
|
// only behind ISecretStore was the OTHER defect this library shipped, so pin it here too.
|
|
IServiceCollection services = Register();
|
|
|
|
Assert.Contains(services, d => d.ServiceType == typeof(SqliteSecretStore));
|
|
}
|
|
|
|
[Fact]
|
|
public void The_store_migrator_seam_is_registered()
|
|
{
|
|
IServiceCollection services = Register();
|
|
|
|
Assert.Contains(services, d => d.ServiceType == typeof(ISecretsStoreMigrator));
|
|
}
|
|
|
|
[Fact]
|
|
public void The_content_root_argument_reaches_the_store_path_guard()
|
|
{
|
|
// scadaproj#31: the Akka node's local SQLite store is migrated by a pre-host expander too,
|
|
// so the extension must be able to hand the guard a content root (no IHostEnvironment here).
|
|
string contentRoot = Path.Combine(Path.GetTempPath(), $"zb-akka-root-{Guid.NewGuid():N}");
|
|
IConfigurationRoot config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["Secrets:SqlitePath"] = Path.Combine(contentRoot, "s.db"),
|
|
})
|
|
.Build();
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddZbSecretsAkkaReplication(config, contentRootPath: contentRoot);
|
|
using ServiceProvider provider = services.BuildServiceProvider();
|
|
|
|
Assert.Throws<OptionsValidationException>(
|
|
() => provider.GetRequiredService<IOptions<SecretsOptions>>().Value);
|
|
}
|
|
}
|