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