using Akka.Actor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.DependencyInjection; using ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.DependencyInjection; namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.Tests.DependencyInjection; /// /// scadaproj#33 (Secrets-010): the replication actor must exist once the host has started, without /// anything resolving . Before the fix the actor was created only through /// the store → replicator chain, so a node that expanded its references pre-host and never touched /// the store at runtime had no actor and silently missed every rotation. /// /// /// Nothing in these tests may resolve , or /// before the assertion — that would create the actor the old way and /// make the test vacuous. could not catch the defect for /// exactly that reason. /// public sealed class EagerActorStartTests(SingleNodeClusterFixture cluster) : IClassFixture { public enum Order { CoreFirst, ExtensionFirst, } private static Dictionary Settings(string actorName) => new() { ["Secrets:SqlitePath"] = Path.Combine(Path.GetTempPath(), $"zb-eager-{Guid.NewGuid():N}.db"), ["Secrets:RunMigrationsOnStartup"] = "true", ["Secrets:Replication:AnnounceInterval"] = "00:00:30", ["Secrets:Replication:ActorName"] = actorName, }; private async Task ActorExistsAsync(string actorName) { try { await cluster.System.ActorSelection($"/user/{actorName}").ResolveOne(TimeSpan.FromSeconds(3)); return true; } catch (ActorNotFoundException) { return false; } } [Theory] [InlineData(Order.CoreFirst)] [InlineData(Order.ExtensionFirst)] public async Task A_started_generic_host_creates_the_actor_without_resolving_the_store(Order order) { string actorName = $"zb-eager-host-{order}"; HostApplicationBuilder builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { Args = [], DisableDefaults = true, }); builder.Configuration.AddInMemoryCollection(Settings(actorName)); builder.Services.AddSingleton(cluster.System); if (order == Order.CoreFirst) { builder.Services.AddZbSecrets(builder.Configuration, "Secrets"); builder.Services.AddZbSecretsAkkaReplication(builder.Configuration); } else { builder.Services.AddZbSecretsAkkaReplication(builder.Configuration); builder.Services.AddZbSecrets(builder.Configuration, "Secrets"); } using IHost host = builder.Build(); Assert.False(await ActorExistsAsync(actorName), "positive control: no actor before the host starts"); await host.StartAsync(); try { Assert.True(await ActorExistsAsync(actorName), "the replication actor was not created at startup"); } finally { await host.StopAsync(); } } [Fact] public async Task Without_a_host_lifetime_the_starter_creates_the_actor_in_StartAsync() { string actorName = "zb-eager-bare"; IConfigurationRoot config = new ConfigurationBuilder().AddInMemoryCollection(Settings(actorName)).Build(); var services = new ServiceCollection(); services.AddLogging(); services.AddSingleton(cluster.System); services.AddZbSecretsAkkaReplication(config); await using ServiceProvider provider = services.BuildServiceProvider(); foreach (IHostedService hosted in provider.GetServices()) { await hosted.StartAsync(CancellationToken.None); } Assert.True(await ActorExistsAsync(actorName), "the replication actor was not created at startup"); } }