using Akka.Actor; using Akka.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs; using ZB.MOM.WW.OtOpcUa.Cluster; using ZB.MOM.WW.OtOpcUa.Configuration; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Hubs; /// /// Verifies the Phase 5 dark switch in : /// TelemetryDial:Mode = Dps (default) spawns the four telemetry DPS bridge actors and NOT the /// gRPC dial supervisor; Grpc spawns the dial supervisor and NONE of the four telemetry /// bridges. The fleet-status bridge (deferred / out of Phase 5 scope) is spawned in BOTH modes. /// The gate is proven on a real from a started Akka host. /// public sealed class TelemetryModeWiringTests { /// Dps mode: the four telemetry bridges + fleet bridge are registered; the dial supervisor is not. [Fact] public async Task Dps_mode_spawns_the_four_telemetry_bridges_and_not_the_dial_supervisor() { using var host = BuildBridgeHost(TelemetryDialOptions.ModeDps); await host.StartAsync(); try { var registry = host.Services.GetRequiredService(); registry.TryGet(out _).ShouldBeTrue(); registry.TryGet(out _).ShouldBeTrue(); registry.TryGet(out _).ShouldBeTrue(); registry.TryGet(out _).ShouldBeTrue(); registry.TryGet(out _).ShouldBeTrue(); registry.TryGet(out _).ShouldBeFalse(); } finally { await host.StopAsync(); } } /// Grpc mode: the dial supervisor + fleet bridge are registered; none of the four telemetry bridges are. [Fact] public async Task Grpc_mode_spawns_the_dial_supervisor_and_none_of_the_four_telemetry_bridges() { using var host = BuildBridgeHost(TelemetryDialOptions.ModeGrpc); await host.StartAsync(); try { var registry = host.Services.GetRequiredService(); registry.TryGet(out _).ShouldBeTrue(); registry.TryGet(out _).ShouldBeTrue(); registry.TryGet(out _).ShouldBeFalse(); registry.TryGet(out _).ShouldBeFalse(); registry.TryGet(out _).ShouldBeFalse(); registry.TryGet(out _).ShouldBeFalse(); } finally { await host.StopAsync(); } } /// Unset (default) mode is treated as Dps — the four bridges spawn, the supervisor does not. [Fact] public async Task Default_mode_is_dps() { using var host = BuildBridgeHost(mode: null); await host.StartAsync(); try { var registry = host.Services.GetRequiredService(); registry.TryGet(out _).ShouldBeTrue(); registry.TryGet(out _).ShouldBeFalse(); } finally { await host.StopAsync(); } } /// Builds an admin-role host that runs WithOtOpcUaSignalRBridges under the given telemetry mode. /// The TelemetryDial:Mode value, or to leave it at its default. private static IHost BuildBridgeHost(string? mode) => Host.CreateDefaultBuilder() .ConfigureServices((_, services) => { services.AddSignalR(); services.AddOtOpcUaDriverStatusServices(); services.AddSingleton>( new InMemoryConfigDbFactory(Guid.NewGuid().ToString("N"))); var options = new TelemetryDialOptions { ApiKey = "test-key" }; if (mode is not null) { options.Mode = mode; } services.AddSingleton>(Options.Create(options)); services.AddAkka("otopcua-test", (ab, _) => { ab.AddHocon(@" akka.actor.provider = ""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster"" akka.remote.dot-netty.tcp.hostname = ""127.0.0.1"" akka.remote.dot-netty.tcp.port = 0 akka.cluster.seed-nodes = [] akka.cluster.roles = [""admin""] ", HoconAddMode.Prepend); ab.WithOtOpcUaSignalRBridges(); }); }) .Build(); /// An whose contexts share one InMemory database. private sealed class InMemoryConfigDbFactory(string dbName) : IDbContextFactory { public OtOpcUaConfigDbContext CreateDbContext() => new(new DbContextOptionsBuilder() .UseInMemoryDatabase(dbName) .Options); public Task CreateDbContextAsync(CancellationToken cancellationToken = default) => Task.FromResult(CreateDbContext()); } }