Second protocol plugin proving multi-protocol architecture. New Junction.Protocols.OpcUa (multi-target net48;net8.0 — SDK has no ns2.0): - OpcuaDriverFactory: config validation (EndpointUrl/SecurityMode/Policy/ AuthMode/Username/Password/NodeIds/BrowseRoot/TimeoutSeconds), CONFIG_* codes. - OpcuaDriver: ReadCurrentAsync (read configured nodes) + ProbeAsync (browse address space), linked-CTS timeout, Result mapping, opt-in FilterToMonitored — mirrors MtconnectDriver. - IUaClient seam (SDK-free) injected for testability; UaClient wraps OPC UA Session (non-obsolete async API), Directory PKI store, anon/user-pass auth. SDK: OPCFoundation.NetStandard.Opc.Ua.Client 1.5.378.156 (MIT), pinned exact, transitive closure copied per-TFM into plugins/opcua/ (CopyLocalLockFileAssemblies + CopyOpcuaPlugin target). Host: AutoGenerateBindingRedirects. Core untouched — protocol auto-discovered by plugin loader. Projects: +Junction.Protocols.OpcUa, Junction.App (wiring), Junction.sln, Junction.Tests (+23 tests: 13 factory + 10 driver via Mock<IUaClient>). 171 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
176 lines
6.2 KiB
C#
176 lines
6.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Junction.Domain.Models;
|
|
using Junction.Protocols.OpcUa;
|
|
using Xunit;
|
|
|
|
namespace Junction.Tests.Unit
|
|
{
|
|
/// <summary>
|
|
/// Config-validation tests for <see cref="OpcuaDriverFactory"/>, mirroring
|
|
/// MtconnectDriverFactory tests: required/absolute/scheme checks, enum checks, the
|
|
/// UsernamePassword credential rule and case-insensitive key lookup.
|
|
/// </summary>
|
|
public sealed class OpcuaDriverFactoryTests
|
|
{
|
|
private static readonly Guid MachineId = Guid.Parse("99999999-8888-7777-6666-555555555555");
|
|
private const string EndpointUrl = "opc.tcp://opcua-server.test:4840";
|
|
|
|
private static Machine MachineWithConfig(IReadOnlyDictionary<string, string>? config) =>
|
|
new Machine(MachineId, "PLC-01", "opcua", config, TimeSpan.FromSeconds(5));
|
|
|
|
[Fact]
|
|
public void ProtocolId_IsOpcua()
|
|
{
|
|
Assert.Equal("opcua", new OpcuaDriverFactory().ProtocolId);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingEndpointUrl_ReturnsFail()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(new Dictionary<string, string>()));
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Contains(result.Errors, e => e.Code == "CONFIG_ENDPOINTURL_MISSING");
|
|
}
|
|
|
|
[Fact]
|
|
public void NonAbsoluteEndpointUrl_ReturnsFail()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(
|
|
new Dictionary<string, string> { ["EndpointUrl"] = "not a uri" }));
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Contains(result.Errors, e => e.Code == "CONFIG_ENDPOINTURL_INVALID");
|
|
}
|
|
|
|
[Fact]
|
|
public void WrongScheme_ReturnsFail()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(
|
|
new Dictionary<string, string> { ["EndpointUrl"] = "http://opcua-server.test:4840" }));
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Contains(result.Errors, e => e.Code == "CONFIG_ENDPOINTURL_SCHEME");
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidEndpointUrl_ReturnsOkDriverWithOpcuaProtocolId()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(
|
|
new Dictionary<string, string> { ["EndpointUrl"] = EndpointUrl }));
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.NotNull(result.Value);
|
|
Assert.Equal("opcua", result.Value.ProtocolId);
|
|
}
|
|
|
|
[Fact]
|
|
public void EndpointUrlKeyIsCaseInsensitive()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(
|
|
new Dictionary<string, string> { ["endpointurl"] = EndpointUrl }));
|
|
|
|
Assert.True(result.IsSuccess);
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidSecurityMode_ReturnsFail()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(new Dictionary<string, string>
|
|
{
|
|
["EndpointUrl"] = EndpointUrl,
|
|
["SecurityMode"] = "Bogus",
|
|
}));
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Contains(result.Errors, e => e.Code == "CONFIG_SECURITYMODE_INVALID");
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidAuthMode_ReturnsFail()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(new Dictionary<string, string>
|
|
{
|
|
["EndpointUrl"] = EndpointUrl,
|
|
["AuthMode"] = "Kerberos",
|
|
}));
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Contains(result.Errors, e => e.Code == "CONFIG_AUTHMODE_INVALID");
|
|
}
|
|
|
|
[Fact]
|
|
public void UsernamePasswordWithoutUsername_ReturnsFail()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(new Dictionary<string, string>
|
|
{
|
|
["EndpointUrl"] = EndpointUrl,
|
|
["AuthMode"] = "UsernamePassword",
|
|
["Password"] = "secret",
|
|
}));
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Contains(result.Errors, e => e.Code == "CONFIG_USERNAME_MISSING");
|
|
}
|
|
|
|
[Fact]
|
|
public void UsernamePasswordWithoutPassword_ReturnsFail()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(new Dictionary<string, string>
|
|
{
|
|
["EndpointUrl"] = EndpointUrl,
|
|
["AuthMode"] = "UsernamePassword",
|
|
["Username"] = "admin",
|
|
}));
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Contains(result.Errors, e => e.Code == "CONFIG_PASSWORD_MISSING");
|
|
}
|
|
|
|
[Fact]
|
|
public void UsernamePasswordWithCredentials_ReturnsOk()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(new Dictionary<string, string>
|
|
{
|
|
["EndpointUrl"] = EndpointUrl,
|
|
["AuthMode"] = "UsernamePassword",
|
|
["Username"] = "admin",
|
|
["Password"] = "secret",
|
|
}));
|
|
|
|
Assert.True(result.IsSuccess);
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidTimeout_ReturnsFail()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(new Dictionary<string, string>
|
|
{
|
|
["EndpointUrl"] = EndpointUrl,
|
|
["TimeoutSeconds"] = "-5",
|
|
}));
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.Contains(result.Errors, e => e.Code == "CONFIG_TIMEOUT_INVALID");
|
|
}
|
|
|
|
[Fact]
|
|
public void FullValidConfig_ReturnsOk()
|
|
{
|
|
var result = new OpcuaDriverFactory().Create(MachineWithConfig(new Dictionary<string, string>
|
|
{
|
|
["EndpointUrl"] = EndpointUrl,
|
|
["SecurityMode"] = "SignAndEncrypt",
|
|
["SecurityPolicy"] = "Basic256Sha256",
|
|
["AuthMode"] = "Anonymous",
|
|
["NodeIds"] = "ns=2;s=Temp, ns=2;s=Speed",
|
|
["BrowseRoot"] = "i=85",
|
|
["TimeoutSeconds"] = "15",
|
|
}));
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal("opcua", result.Value.ProtocolId);
|
|
}
|
|
}
|
|
}
|