Files
lmxopcua/tests/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs
Joseph Doherty fb760bc465 Task #135 — update integration-test NodeIds for path-based scheme
7 integration tests in Server.Tests were left behind by the path-based
NodeId rename (#134). Each was constructing test NodeIds in the old
"FullReference" shape ("TestFolder.Var1", "raw.var", "AlphaFolder.Var1",
"plcaddr-temperature"), which the node manager no longer mints — the new
shape is `{driverId}/{folder-path}/{browseName}` per OPC UA Part 3 §5.2.2
NodeId immutability.

Fixed by re-deriving each test NodeId from the actual browse path the test
fixture's driver registers:

- OpcUaServerIntegrationTests: "TestFolder.Var1" → "fake/TestFolder/Var1"
- HistoryReadIntegrationTests (4 tests): "raw.var" → "history-driver/raw",
  "proc.var" → "history-driver/proc" (×2), "atTime.var" → "history-driver/atTime"
- MultipleDriverInstancesIntegrationTests: "AlphaFolder.Var1" →
  "alpha/AlphaFolder/Var1"; "BetaFolder.Var1" → "beta/BetaFolder/Var1"
- OpcUaEquipmentWalkerIntegrationTests: "plcaddr-temperature" →
  "galaxy-prod/warsaw/line-a/oven-3/Temperature" (the walker uses Tag.Name
  as the browseName; the FullReference lives in TagConfig but no longer
  surfaces in the NodeId path)

Server.Tests now 277/277 green excluding LiveLdap. Clears the regression
flagged during the #124 verification run.
2026-04-24 22:03:03 -04:00

163 lines
7.8 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Server.OpcUa;
using ZB.MOM.WW.OtOpcUa.Server.Security;
namespace ZB.MOM.WW.OtOpcUa.Server.Tests;
[Trait("Category", "Integration")]
public sealed class OpcUaServerIntegrationTests : IAsyncLifetime
{
// Use a non-default port + per-test-run PKI root to avoid colliding with anything else
// running on the box (a live v1 Host or a developer's previous run).
private static readonly int Port = 48400 + Random.Shared.Next(0, 99);
private readonly string _endpoint = $"opc.tcp://localhost:{Port}/OtOpcUaTest";
private readonly string _pkiRoot = Path.Combine(Path.GetTempPath(), $"otopcua-test-{Guid.NewGuid():N}");
private DriverHost _driverHost = null!;
private OpcUaApplicationHost _server = null!;
private FakeDriver _driver = null!;
public async ValueTask InitializeAsync()
{
_driverHost = new DriverHost();
_driver = new FakeDriver();
await _driverHost.RegisterAsync(_driver, "{}", CancellationToken.None);
var options = new OpcUaServerOptions
{
EndpointUrl = _endpoint,
ApplicationName = "OtOpcUaTest",
ApplicationUri = "urn:OtOpcUa:Server:Test",
PkiStoreRoot = _pkiRoot,
AutoAcceptUntrustedClientCertificates = true, HealthEndpointsEnabled = false,
};
_server = new OpcUaApplicationHost(options, _driverHost, new DenyAllUserAuthenticator(),
NullLoggerFactory.Instance, NullLogger<OpcUaApplicationHost>.Instance);
await _server.StartAsync(CancellationToken.None);
}
public async ValueTask DisposeAsync()
{
await _server.DisposeAsync();
await _driverHost.DisposeAsync();
try { Directory.Delete(_pkiRoot, recursive: true); } catch { /* best-effort */ }
}
[Fact]
public async Task Client_can_connect_and_browse_driver_subtree()
{
using var session = await OpenSessionAsync();
// Browse the driver subtree registered under ObjectsFolder. FakeDriver registers one
// folder ("TestFolder") with one variable ("Var1"), so we expect to see our driver's
// root folder plus standard UA children.
var rootRef = new NodeId("fake", (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:fake"));
session.Browse(null, null, rootRef, 0, BrowseDirection.Forward, ReferenceTypeIds.HierarchicalReferences,
true, (uint)NodeClass.Object | (uint)NodeClass.Variable, out _, out var references);
references.Count.ShouldBeGreaterThan(0);
references.ShouldContain(r => r.BrowseName.Name == "TestFolder");
}
[Fact]
public async Task Client_can_read_a_driver_variable_through_the_node_manager()
{
using var session = await OpenSessionAsync();
var nsIndex = (ushort)session.NamespaceUris.GetIndex("urn:OtOpcUa:fake");
// Path-based NodeId per #134 — `{driverId}/{folder-path}/{browseName}`. The driver-side
// FullReference ("TestFolder.Var1") is now decoupled from the NodeId so a backend rename
// doesn't shift the identifier seen by clients (OPC UA Part 3 §5.2.2 immutability).
var varNodeId = new NodeId("fake/TestFolder/Var1", nsIndex);
var dv = session.ReadValue(varNodeId);
dv.ShouldNotBeNull();
// FakeDriver.ReadAsync returns 42 as the value.
dv.Value.ShouldBe(42);
}
private async Task<ISession> OpenSessionAsync()
{
var cfg = new ApplicationConfiguration
{
ApplicationName = "OtOpcUaTestClient",
ApplicationUri = "urn:OtOpcUa:TestClient",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(_pkiRoot, "client-own"),
SubjectName = "CN=OtOpcUaTestClient",
},
TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-issuers") },
TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-trusted") },
RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(_pkiRoot, "client-rejected") },
AutoAcceptUntrustedCertificates = true,
AddAppCertToTrustedStore = true,
},
TransportConfigurations = new TransportConfigurationCollection(),
TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 },
};
await cfg.Validate(ApplicationType.Client);
cfg.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true;
var instance = new ApplicationInstance { ApplicationConfiguration = cfg, ApplicationType = ApplicationType.Client };
await instance.CheckApplicationInstanceCertificate(true, CertificateFactory.DefaultKeySize);
// Let the client fetch the live endpoint description from the running server so the
// UserTokenPolicy it signs with matches what the server actually advertised (including
// the PolicyId = "Anonymous" the server sets).
var selected = CoreClientUtils.SelectEndpoint(cfg, _endpoint, useSecurity: false);
var endpointConfig = EndpointConfiguration.Create(cfg);
var configuredEndpoint = new ConfiguredEndpoint(null, selected, endpointConfig);
return await Session.Create(cfg, configuredEndpoint, false, "OtOpcUaTestClientSession", 60000,
new UserIdentity(new AnonymousIdentityToken()), null);
}
/// <summary>
/// Minimum driver that implements enough of IDriver + ITagDiscovery + IReadable to drive
/// the integration test. Returns a single folder with one variable that reads as 42.
/// </summary>
private sealed class FakeDriver : IDriver, ITagDiscovery, IReadable
{
public string DriverInstanceId => "fake";
public string DriverType => "Fake";
public Task InitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
public Task ReinitializeAsync(string driverConfigJson, CancellationToken ct) => Task.CompletedTask;
public Task ShutdownAsync(CancellationToken ct) => Task.CompletedTask;
public DriverHealth GetHealth() => new(DriverState.Healthy, DateTime.UtcNow, null);
public long GetMemoryFootprint() => 0;
public Task FlushOptionalCachesAsync(CancellationToken ct) => Task.CompletedTask;
public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken ct)
{
var folder = builder.Folder("TestFolder", "TestFolder");
folder.Variable("Var1", "Var1", new DriverAttributeInfo(
"TestFolder.Var1", DriverDataType.Int32, false, null, SecurityClassification.FreeAccess, false, IsAlarm: false));
return Task.CompletedTask;
}
public Task<IReadOnlyList<DataValueSnapshot>> ReadAsync(
IReadOnlyList<string> fullReferences, CancellationToken cancellationToken)
{
var now = DateTime.UtcNow;
IReadOnlyList<DataValueSnapshot> result =
fullReferences.Select(_ => new DataValueSnapshot(42, 0u, now, now)).ToArray();
return Task.FromResult(result);
}
}
}