d15c613ef3
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
135 lines
5.7 KiB
C#
135 lines
5.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Verifies the Phase 5 dark switch in <see cref="HubServiceCollectionExtensions.WithOtOpcUaSignalRBridges"/>:
|
|
/// <c>TelemetryDial:Mode = Dps</c> (default) spawns the four telemetry DPS bridge actors and NOT the
|
|
/// gRPC dial supervisor; <c>Grpc</c> 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 <see cref="ActorRegistry"/> from a started Akka host.
|
|
/// </summary>
|
|
public sealed class TelemetryModeWiringTests
|
|
{
|
|
/// <summary>Dps mode: the four telemetry bridges + fleet bridge are registered; the dial supervisor is not.</summary>
|
|
[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<ActorRegistry>();
|
|
|
|
registry.TryGet<FleetStatusSignalRBridgeKey>(out _).ShouldBeTrue();
|
|
registry.TryGet<AlertSignalRBridgeKey>(out _).ShouldBeTrue();
|
|
registry.TryGet<ScriptLogSignalRBridgeKey>(out _).ShouldBeTrue();
|
|
registry.TryGet<DriverStatusSignalRBridgeKey>(out _).ShouldBeTrue();
|
|
registry.TryGet<DriverResilienceStatusBridgeKey>(out _).ShouldBeTrue();
|
|
|
|
registry.TryGet<TelemetryDialSupervisorKey>(out _).ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
await host.StopAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>Grpc mode: the dial supervisor + fleet bridge are registered; none of the four telemetry bridges are.</summary>
|
|
[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<ActorRegistry>();
|
|
|
|
registry.TryGet<FleetStatusSignalRBridgeKey>(out _).ShouldBeTrue();
|
|
registry.TryGet<TelemetryDialSupervisorKey>(out _).ShouldBeTrue();
|
|
|
|
registry.TryGet<AlertSignalRBridgeKey>(out _).ShouldBeFalse();
|
|
registry.TryGet<ScriptLogSignalRBridgeKey>(out _).ShouldBeFalse();
|
|
registry.TryGet<DriverStatusSignalRBridgeKey>(out _).ShouldBeFalse();
|
|
registry.TryGet<DriverResilienceStatusBridgeKey>(out _).ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
await host.StopAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>Unset (default) mode is treated as Dps — the four bridges spawn, the supervisor does not.</summary>
|
|
[Fact]
|
|
public async Task Default_mode_is_dps()
|
|
{
|
|
using var host = BuildBridgeHost(mode: null);
|
|
await host.StartAsync();
|
|
try
|
|
{
|
|
var registry = host.Services.GetRequiredService<ActorRegistry>();
|
|
registry.TryGet<AlertSignalRBridgeKey>(out _).ShouldBeTrue();
|
|
registry.TryGet<TelemetryDialSupervisorKey>(out _).ShouldBeFalse();
|
|
}
|
|
finally
|
|
{
|
|
await host.StopAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>Builds an admin-role host that runs <c>WithOtOpcUaSignalRBridges</c> under the given telemetry mode.</summary>
|
|
/// <param name="mode">The <c>TelemetryDial:Mode</c> value, or <see langword="null"/> to leave it at its default.</param>
|
|
private static IHost BuildBridgeHost(string? mode)
|
|
=> Host.CreateDefaultBuilder()
|
|
.ConfigureServices((_, services) =>
|
|
{
|
|
services.AddSignalR();
|
|
services.AddOtOpcUaDriverStatusServices();
|
|
services.AddSingleton<IDbContextFactory<OtOpcUaConfigDbContext>>(
|
|
new InMemoryConfigDbFactory(Guid.NewGuid().ToString("N")));
|
|
|
|
var options = new TelemetryDialOptions { ApiKey = "test-key" };
|
|
if (mode is not null)
|
|
{
|
|
options.Mode = mode;
|
|
}
|
|
|
|
services.AddSingleton<IOptions<TelemetryDialOptions>>(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();
|
|
|
|
/// <summary>An <see cref="IDbContextFactory{TContext}"/> whose contexts share one InMemory database.</summary>
|
|
private sealed class InMemoryConfigDbFactory(string dbName) : IDbContextFactory<OtOpcUaConfigDbContext>
|
|
{
|
|
public OtOpcUaConfigDbContext CreateDbContext() =>
|
|
new(new DbContextOptionsBuilder<OtOpcUaConfigDbContext>()
|
|
.UseInMemoryDatabase(dbName)
|
|
.Options);
|
|
|
|
public Task<OtOpcUaConfigDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(CreateDbContext());
|
|
}
|
|
}
|