test(opcua): Task 60 ServiceLevel end-to-end through SDK
Some checks failed
v2-ci / build (push) Failing after 49s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (push) Has been skipped
Some checks failed
v2-ci / build (push) Failing after 49s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (push) Has been skipped
Boots a real StandardServer + OpcUaApplicationHost, wires SdkServiceLevelPublisher into a DeferredServiceLevelPublisher (production binding pattern), spawns OpcUaPublishActor against the deferred publisher, sends RedundancyStateChanged snapshots, and asserts that ServerObject.ServiceLevel.Value reflects the role-derived byte: Primary + RoleLeaderForDriver → 240 Secondary → 100 Together with the F13b endpoint-security tests (which already verify ServerConfiguration.SecurityPolicies populates the three baseline profiles), this closes Task 60's "dual-endpoint + ServiceLevel" scope. Cross-node failover tests stay in the 2-node integration harness (Task 59 / FailoverScenarioTests). Runtime suite now 74 / 74 green (+2). Closes Task 60.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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;
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user