From 9357c001b7b654c958ab21c4dfed759e734211fc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 13 Jun 2026 08:20:02 -0400 Subject: [PATCH] fix(opcuaclient): register the OpcUaClient driver factory (was always stubbed) --- .../OpcUaClientDriverFactoryExtensions.cs | 56 +++++++++++++++++++ .../OpcUaClientDriverProbe.cs | 4 ++ ...B.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj | 1 + .../Drivers/DriverFactoryBootstrap.cs | 1 + .../OpcUaClientDriverFactoryTests.cs | 36 ++++++++++++ 5 files changed, 98 insertions(+) create mode 100644 src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs create mode 100644 tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientDriverFactoryTests.cs diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs new file mode 100644 index 00000000..6943927f --- /dev/null +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using ZB.MOM.WW.OtOpcUa.Core.Hosting; + +namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient; + +/// +/// Registers the OPC UA Client driver with the . The driver's +/// DriverConfig JSON deserialises directly into +/// (the same shape reads), so no separate DTO is needed. +/// Mirrors ModbusDriverFactoryExtensions / GalaxyDriverFactoryExtensions. +/// +public static class OpcUaClientDriverFactoryExtensions +{ + /// Driver type name — matches DriverInstance.DriverType values. + public const string DriverTypeName = "OpcUaClient"; + + // Match OpcUaClientDriverProbe exactly so factory + probe parse a config identically. + // The JsonStringEnumConverter lets enum-valued knobs (SecurityMode / SecurityPolicy / + // AuthType / TargetNamespaceKind) be authored as their string names — the natural form + // for human-edited + AdminUI-emitted DriverConfig JSON. + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + Converters = { new JsonStringEnumConverter() }, + }; + + /// Register the OpcUaClient factory with the driver registry. + /// The driver factory registry to register with. + /// Optional logger factory used to create per-instance loggers. + public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null) + { + ArgumentNullException.ThrowIfNull(registry); + registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory)); + } + + /// Public for the Server-side bootstrapper + test consumers. + /// The unique identifier for the driver instance. + /// The JSON configuration string for the driver. + /// Optional logger factory for the per-instance logger. + /// A configured . + public static OpcUaClientDriver CreateInstance( + string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); + ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson); + + var options = JsonSerializer.Deserialize(driverConfigJson, JsonOptions) + ?? throw new InvalidOperationException( + $"OpcUaClient driver config for '{driverInstanceId}' deserialised to null"); + + return new OpcUaClientDriver(options, driverInstanceId, loggerFactory?.CreateLogger()); + } +} diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverProbe.cs index c5f82b2f..5f6fba17 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverProbe.cs @@ -17,10 +17,14 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient; /// public sealed class OpcUaClientDriverProbe : IDriverProbe { + // Kept identical to OpcUaClientDriverFactoryExtensions.JsonOptions so the probe and the + // factory parse a given DriverConfig the same way. The JsonStringEnumConverter lets + // enum-valued knobs be authored as their string names. private static readonly JsonSerializerOptions _opts = new() { PropertyNameCaseInsensitive = true, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + Converters = { new JsonStringEnumConverter() }, }; /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj index d41c0dce..34c57c0d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs index 26ef2284..e61aa142 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs @@ -102,6 +102,7 @@ public static class DriverFactoryBootstrap Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry); Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, loggerFactory); Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory); + Driver.OpcUaClient.OpcUaClientDriverFactoryExtensions.Register(registry, loggerFactory); Driver.S7.S7DriverFactoryExtensions.Register(registry); Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry); } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientDriverFactoryTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientDriverFactoryTests.cs new file mode 100644 index 00000000..27a4df15 --- /dev/null +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientDriverFactoryTests.cs @@ -0,0 +1,36 @@ +using Shouldly; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; + +/// +/// Tests for — the factory that lets the +/// Server-side DriverFactoryBootstrap materialise a real +/// from a DriverInstance row instead of falling back to a stub. +/// +public class OpcUaClientDriverFactoryTests +{ + private const string SampleConfig = + """{"EndpointUrl":"opc.tcp://host:4840","SecurityMode":"None","AutoAcceptCertificates":true}"""; + + /// Verifies the factory builds a driver carrying the right type + instance identity. + [Fact] + public void CreateInstance_builds_an_OpcUaClient_driver_with_the_right_identity() + { + var driver = OpcUaClientDriverFactoryExtensions.CreateInstance("drv-1", SampleConfig, NullLoggerFactory.Instance); + driver.DriverType.ShouldBe("OpcUaClient"); + driver.DriverInstanceId.ShouldBe("drv-1"); + } + + /// Verifies the public driver-type-name constant matches the driver's DriverType. + [Fact] + public void DriverTypeName_is_OpcUaClient() + => OpcUaClientDriverFactoryExtensions.DriverTypeName.ShouldBe("OpcUaClient"); + + /// Verifies a JSON literal that deserialises to null is rejected with a clear error. + [Fact] + public void CreateInstance_throws_on_null_json_deserialisation() + => Should.Throw( + () => OpcUaClientDriverFactoryExtensions.CreateInstance("drv-1", "null", NullLoggerFactory.Instance)); +}