Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/ServiceLevelEndToEndTests.cs
Joseph Doherty bd6c0b4d3d docs: complete XML doc comments via fixdocs (2757 to 131 findings)
Add missing <returns>/<param>/<summary>/<typeparam> tags and clean up
misused inheritdoc across 481 files so the documented API surface is
complete. Documentation-only (zero code lines changed). The 131 remaining
findings are inheritdoc-style warnings deliberately left to preserve
hand-written implementation rationale (plan-decision notes, race-condition
explanations).
2026-06-03 12:34:34 -04:00

152 lines
5.6 KiB
C#

using Akka.Actor;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua.Server;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Redundancy;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.OpcUa;
/// <summary>
/// Task 60 / #81 — verifies the full path from cluster redundancy state to OPC UA
/// <c>Server.ServiceLevel</c> visible on the wire. Boots a real <see cref="StandardServer"/>,
/// wires <see cref="SdkServiceLevelPublisher"/> into a <see cref="DeferredServiceLevelPublisher"/>
/// (the production binding pattern), spawns <see cref="OpcUaPublishActor"/> against the
/// deferred publisher, and sends a <see cref="RedundancyStateChanged"/> snapshot. Asserts
/// <c>ServerObject.ServiceLevel.Value</c> reflects the role-derived byte.
/// </summary>
public sealed class ServiceLevelEndToEndTests : RuntimeActorTestBase
{
private static CancellationToken Ct => CancellationToken.None;
/// <summary>Verifies that the primary cluster leader sets Server ServiceLevel to 240.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Primary_leader_drives_Server_ServiceLevel_to_240()
{
var pkiRoot = AllocatePkiRoot();
try
{
var server = new StandardServer();
await using var host = new OpcUaApplicationHost(
BuildOptions("PrimaryLeader", pkiRoot),
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, Ct);
var deferred = new DeferredServiceLevelPublisher();
deferred.SetInner(new SdkServiceLevelPublisher(
server.CurrentInstance,
NullLogger<SdkServiceLevelPublisher>.Instance));
var localNode = NodeId.Parse("node-A");
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
serviceLevel: deferred,
subscribeRedundancyTopic: false,
localNode: localNode));
actor.Tell(new RedundancyStateChanged(
Nodes: new[]
{
new NodeRedundancyState(localNode, RedundancyRole.Primary, IsClusterLeader: true, IsRoleLeaderForDriver: true, AsOfUtc: DateTime.UtcNow),
},
CorrelationId: CorrelationId.NewId()));
AwaitAssertion(() =>
server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)240));
}
finally
{
DeletePkiRoot(pkiRoot);
}
}
/// <summary>Verifies that the secondary node sets Server ServiceLevel to 100.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task Secondary_drives_Server_ServiceLevel_to_100()
{
var pkiRoot = AllocatePkiRoot();
try
{
var server = new StandardServer();
await using var host = new OpcUaApplicationHost(
BuildOptions("Secondary", pkiRoot),
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(server, Ct);
var deferred = new DeferredServiceLevelPublisher();
deferred.SetInner(new SdkServiceLevelPublisher(
server.CurrentInstance,
NullLogger<SdkServiceLevelPublisher>.Instance));
var localNode = NodeId.Parse("node-B");
var actor = Sys.ActorOf(OpcUaPublishActor.PropsForTests(
serviceLevel: deferred,
subscribeRedundancyTopic: false,
localNode: localNode));
actor.Tell(new RedundancyStateChanged(
Nodes: new[]
{
new NodeRedundancyState(localNode, RedundancyRole.Secondary, IsClusterLeader: false, IsRoleLeaderForDriver: false, AsOfUtc: DateTime.UtcNow),
},
CorrelationId: CorrelationId.NewId()));
AwaitAssertion(() =>
server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)100));
}
finally
{
DeletePkiRoot(pkiRoot);
}
}
private static OpcUaApplicationHostOptions BuildOptions(string name, string pkiRoot) =>
new()
{
ApplicationName = $"OtOpcUa.E2E.{name}",
ApplicationUri = $"urn:OtOpcUa.E2E.{name}:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = pkiRoot,
};
private static string AllocatePkiRoot() =>
Path.Combine(Path.GetTempPath(), $"otopcua-pki-{Guid.NewGuid():N}");
private static void DeletePkiRoot(string root)
{
if (Directory.Exists(root))
{
try { Directory.Delete(root, recursive: true); }
catch { /* best-effort */ }
}
}
private static int AllocateFreePort()
{
using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
listener.Start();
var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
private void AwaitAssertion(Action assertion)
{
var deadline = DateTime.UtcNow.AddSeconds(3);
Exception? last = null;
while (DateTime.UtcNow < deadline)
{
try { assertion(); return; }
catch (Exception ex) { last = ex; Thread.Sleep(30); }
}
if (last is not null) throw last;
}
}