using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua.Server;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
///
/// #81 residual — verifies locates the standard
/// VariableIds.Server_ServiceLevel node through the SDK's DiagnosticsNodeManager and
/// writes the byte value. Boots a real on a free port so the
/// SDK populates its predefined diagnostics nodes — that's what production sees.
///
public sealed class SdkServiceLevelPublisherTests : IDisposable
{
private static CancellationToken Ct => TestContext.Current.CancellationToken;
private readonly string _pkiRoot = Path.Combine(
Path.GetTempPath(),
$"otopcua-pki-{Guid.NewGuid():N}");
[Fact]
public void Missing_ServiceLevel_is_reported_as_failure()
{
var server = System.Reflection.DispatchProxy.Create();
var publisher = new SdkServiceLevelPublisher(server, NullLogger.Instance);
Should.Throw(() => publisher.Publish(0));
}
[Fact]
public void Sdk_write_failure_propagates_the_original_exception()
{
var server = System.Reflection.DispatchProxy.Create();
var proxy = (FaultingServer)(object)server;
proxy.Node = new Opc.Ua.ServerObjectState(null)
{
ServiceLevel = new Opc.Ua.PropertyState(null),
};
proxy.Fault = new InvalidOperationException("controlled diagnostics failure");
var publisher = new SdkServiceLevelPublisher(server, NullLogger.Instance);
Should.Throw(() => publisher.Publish(0)).ShouldBeSameAs(proxy.Fault);
}
public class FaultingServer : System.Reflection.DispatchProxy
{
public Opc.Ua.ServerObjectState? Node { get; set; }
public Exception? Fault { get; set; }
protected override object? Invoke(System.Reflection.MethodInfo? method, object?[]? args)
{
if (method?.Name == "get_ServerObject") return Node;
if (method?.Name == "get_DiagnosticsLock") throw Fault!;
throw new NotSupportedException(method?.Name);
}
}
/// Verifies that the publisher writes values to the standard Server.ServiceLevel variable.
[Fact]
public async Task Publish_writes_value_to_Server_ServiceLevel_variable()
{
var applicationUri = $"urn:OtOpcUa.SvcLevel:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SvcLevel",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger.Instance),
() => new StandardServer(), Ct);
await using var host = started.Host;
var server = started.Server;
var publisher = new SdkServiceLevelPublisher(
server.CurrentInstance,
NullLogger.Instance);
publisher.Publish(200);
var variable = server.CurrentInstance.ServerObject.ServiceLevel;
variable.ShouldNotBeNull("Server.ServiceLevel must be present in the address space");
variable.Value.ShouldBe((byte)200);
}
/// Verifies that publishing service level values is idempotent when called multiple times.
[Fact]
public async Task Publish_is_idempotent_when_called_multiple_times()
{
var applicationUri = $"urn:OtOpcUa.SvcLevel.Idem:{Guid.NewGuid():N}";
var started = await OpcUaHostStart.StartAsync(
port => new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SvcLevel.Idem",
ApplicationUri = applicationUri,
OpcUaPort = port,
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
},
NullLogger.Instance),
() => new StandardServer(), Ct);
await using var host = started.Host;
var server = started.Server;
var publisher = new SdkServiceLevelPublisher(
server.CurrentInstance,
NullLogger.Instance);
publisher.Publish(100);
publisher.Publish(150);
publisher.Publish(240);
server.CurrentInstance.ServerObject.ServiceLevel.Value.ShouldBe((byte)240);
}
/// Disposes and cleans up the test resources.
public void Dispose()
{
if (Directory.Exists(_pkiRoot))
{
try { Directory.Delete(_pkiRoot, recursive: true); }
catch { /* best-effort */ }
}
}
}