diff --git a/docker-dev/seed/seed-clusters.sql b/docker-dev/seed/seed-clusters.sql index 06750621..acac7762 100644 --- a/docker-dev/seed/seed-clusters.sql +++ b/docker-dev/seed/seed-clusters.sql @@ -218,7 +218,7 @@ DECLARE @OpcPlcAlarmSourceNodeId nvarchar(400) = N''; -- e.g. N'ns=3;s= **Retired (Gitea #648).** The per-instance `TargetNamespaceKind` (Equipment + `UnsMappingTable` vs SystemPlatform) and its startup enforcement were removed as vestigial in v3: UNS projection is explicit via `UnsTagReference`, and nothing consumed the mapping table. Legacy `targetNamespaceKind` / `unsMappingTable` keys in stored config are ignored. See `docs/drivers/OpcUaClient.md`. --- @@ -877,6 +872,6 @@ The driver enforces the namespace choice at startup — a misconfigured remote ( | Siemens S7 | S7netplus | MIT | B — in-process with guards | Equipment | .NET 10 x64 | No (polled) | Config DB | 3-30 per PLC | PUT/GET enabled (S7-1200/1500) | | TwinCAT | Beckhoff.TwinCAT.Ads 6.x | Proprietary | B — in-process with guards | Equipment | .NET 10 x64 | **Yes (ADS)** | Symbol upload | 64-128 | AMS route configured | | FOCAS | Fwlib64.dll (P/Invoke) + MessagePack IPC | FANUC SDK license | **C — out-of-process** | Equipment | .NET 10 x64 (Host + Proxy) | No (polled) | Built-in + Config DB | **5-10 per CNC** | FANUC SDK license, CNC Ethernet option, Focas.Host Windows service | -| OPC UA Client | OPC Foundation 1.5.x | GPL/RCL | A — in-process | Equipment OR SystemPlatform (per-instance) | .NET 10 x64 | **Yes (native)** | Browse remote | Varies | Certificate trust | +| OPC UA Client | OPC Foundation 1.5.x | GPL/RCL | A — in-process | Equipment | .NET 10 x64 | **Yes (native)** | Browse remote | Varies | Certificate trust | Tier definitions and per-tier protections: see `driver-stability.md`. Namespace model and UNS naming rules: see `plan.md` §4 and `config-db-schema.md` (Equipment table). Equipment-namespace drivers populate `Equipment` rows whose UNS path comes from `ServerCluster.Enterprise/Site` + `Equipment.Area/Line/Name`; SystemPlatform-namespace drivers (Galaxy) preserve their own hierarchy via `Tag.FolderPath` as v1 LmxOpcUa expressed it. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientDriverBrowser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientDriverBrowser.cs index af7c2ac0..c26d1f38 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientDriverBrowser.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientDriverBrowser.cs @@ -21,7 +21,7 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser // Kept identical to OpcUaClientDriverFactoryExtensions.JsonOptions and // OpcUaClientDriverProbe._opts so the browser parses a given DriverConfig JSON // the same way as the factory and the probe. The JsonStringEnumConverter lets - // enum-valued knobs (SecurityPolicy / SecurityMode / AuthType / TargetNamespaceKind) + // enum-valued knobs (SecurityPolicy / SecurityMode / AuthType) // be authored as their string names — the natural form for AdminUI-emitted JSON. // JsonStringEnumConverter also accepts numeric ordinals, so existing numeric configs // continue to work without migration. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs index 80f56c11..ae424d2d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/OpcUaClientDriverOptions.cs @@ -153,36 +153,6 @@ public sealed record OpcUaClientDriverOptions [Range(1, int.MaxValue)] public int MaxBrowseDepth { get; init; } = 10; - /// - /// The namespace kind this driver instance gateways into. Per - /// docs/v2/driver-specs.md §8 "Namespace Assignment", the OPC UA Client driver - /// is the only driver that supports either kind, decided per instance: - /// - /// — gatewaying a remote - /// server that exposes raw equipment data; the driver remaps remote browse paths - /// to UNS. - /// — gatewaying a - /// remote server that exposes processed/derived data; hierarchy is preserved via - /// the remote browse path with no UNS conversion. - /// - /// The driver enforces the choice at startup — see - /// OpcUaClientDriver.ValidateNamespaceKind — so a misconfiguration fails - /// draft validation rather than surfacing as a runtime surprise. - /// - public OpcUaTargetNamespaceKind TargetNamespaceKind { get; init; } = OpcUaTargetNamespaceKind.Equipment; - - /// - /// Optional config-driven remote-to-UNS mapping table. Required when - /// is : - /// §8 mandates that a remote server gatewayed as Equipment carry a mapping table - /// because remote nodes do not conform to UNS by default. Keys are remote browse-path - /// prefixes; values are the UNS Area/Line/Name path the matching subtree maps onto. - /// For the table must be empty — - /// processed data preserves its own hierarchy and a mapping table would be ambiguous. - /// - public IReadOnlyDictionary UnsMappingTable { get; init; } - = new Dictionary(); - /// /// Timeout for the AdminUI Test Connect probe, in seconds. The AdminUI clamps to a /// 60s server-side maximum; this default is what the form pre-fills for new instances. @@ -266,25 +236,6 @@ public sealed record OpcUaCertificateValidationOptions public string? IssuerStorePath { get; init; } } -/// -/// The namespace kind an OPC UA Client driver instance gateways its remote server into. -/// See docs/v2/driver-specs.md §8 "Namespace Assignment". -/// -public enum OpcUaTargetNamespaceKind -{ - /// - /// Remote server exposes raw equipment data; remote browse paths are remapped to UNS - /// via . - /// - Equipment, - - /// - /// Remote server exposes processed/derived data; hierarchy is preserved via the - /// remote browse path with no UNS conversion. - /// - SystemPlatform, -} - /// OPC UA message security mode. public enum OpcUaSecurityMode { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs index 814c1f98..5b0220c2 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs @@ -241,10 +241,6 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit if (HasConfigBody(driverConfigJson)) _options = OpcUaClientDriverFactoryExtensions.ParseOptions(_driverInstanceId, driverConfigJson); - // Enforce the Equipment-vs-SystemPlatform choice at startup per driver-specs.md - // §8 "Namespace Assignment" — a misconfigured remote fails draft validation here, - // not as a runtime surprise. - ValidateNamespaceKind(_options); ValidateTuning(_options); var appConfig = await BuildApplicationConfigurationAsync(cancellationToken).ConfigureAwait(false); @@ -643,42 +639,6 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit return effective < TimeSpan.FromMilliseconds(50) ? 50 : (int)effective.TotalMilliseconds; } - /// - /// Enforce the §8 "Namespace Assignment" rule at startup. An Equipment-kind - /// instance gateways raw equipment data and therefore needs a config-driven UNS - /// mapping table (remote nodes don't conform to UNS); a SystemPlatform-kind - /// instance gateways processed data whose hierarchy is preserved verbatim, so a - /// UNS mapping table is meaningless and rejected. Throwing here surfaces the - /// misconfiguration as a draft-validation failure rather than a runtime surprise. - /// - /// Driver options containing namespace configuration. - internal static void ValidateNamespaceKind(OpcUaClientDriverOptions opts) - { - switch (opts.TargetNamespaceKind) - { - case OpcUaTargetNamespaceKind.Equipment: - if (opts.UnsMappingTable is null || opts.UnsMappingTable.Count == 0) - throw new InvalidOperationException( - "OpcUaClient driver configured with TargetNamespaceKind=Equipment but no " + - "UnsMappingTable: §8 requires a config-driven remote-to-UNS mapping table " + - "because remote nodes do not conform to UNS by default. Provide a mapping " + - "table or set TargetNamespaceKind=SystemPlatform if the remote exposes " + - "processed data."); - break; - case OpcUaTargetNamespaceKind.SystemPlatform: - if (opts.UnsMappingTable is { Count: > 0 }) - throw new InvalidOperationException( - "OpcUaClient driver configured with TargetNamespaceKind=SystemPlatform but " + - "a UnsMappingTable was supplied: processed data preserves its own hierarchy " + - "and a UNS mapping table is ambiguous here. Clear the mapping table or set " + - "TargetNamespaceKind=Equipment if the remote exposes raw equipment data."); - break; - default: - throw new ArgumentOutOfRangeException( - nameof(opts), opts.TargetNamespaceKind, "Unknown TargetNamespaceKind."); - } - } - /// /// Build the user-identity token from the driver options. Split out of /// so the failover sweep reuses one identity across diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs index 146ec164..efb52b02 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.cs @@ -23,7 +23,7 @@ public static class OpcUaClientDriverFactoryExtensions // 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 + // AuthType) be authored as their string names — the natural form // for human-edited + AdminUI-emitted DriverConfig JSON. private static readonly JsonSerializerOptions JsonOptions = new() { diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/OpcUaClientDriverForm.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/OpcUaClientDriverForm.razor index bf04f380..36c8f19e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/OpcUaClientDriverForm.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/OpcUaClientDriverForm.razor @@ -161,30 +161,6 @@ -@* Namespace mapping *@ -
-
Namespace mapping
-
-
-
- - - @foreach (var e in Enum.GetValues()) - { - - } - -
Equipment = raw data re-mapped to UNS. SystemPlatform = processed data; hierarchy preserved as-is.
-
-
- -
@_unsMappingTableJson
-
Keys = remote browse-path prefixes; values = UNS Area/Line/Name paths. Required when TargetNamespaceKind = Equipment.
-
-
-
-
- @* Subscriptions, limits, revocation, discovery (#273/#275/#277/#278) *@
Subscriptions, limits & discovery
@@ -272,7 +248,6 @@ private FormModel _form = new(); private List _endpoints = []; - private string _unsMappingTableJson = "{}"; private string? _lastParsed; protected override void OnParametersSet() @@ -282,7 +257,6 @@ var opts = TryDeserialize(DriverConfigJson) ?? new OpcUaClientDriverOptions(); _form = new FormModel { OpcUa = OpcUaClientFormModel.FromRecord(opts) }; _endpoints = opts.EndpointUrls.Select(EndpointUrlRow.FromUrl).ToList(); - _unsMappingTableJson = JsonSerializer.Serialize(opts.UnsMappingTable, _jsonOpts); _lastParsed = DriverConfigJson; } } @@ -369,7 +343,6 @@ public string? UserCertificatePath { get; set; } public string? UserCertificatePassword { get; set; } - public OpcUaTargetNamespaceKind TargetNamespaceKind { get; set; } = OpcUaTargetNamespaceKind.Equipment; public int ProbeTimeoutSeconds { get; set; } = 15; @@ -385,7 +358,6 @@ public string? DiscoveryUrl { get; set; } public string? DiscoveryServerUri { get; set; } - internal IReadOnlyDictionary _unsMappingTable = new Dictionary(); public static OpcUaClientFormModel FromRecord(OpcUaClientDriverOptions r) => new() { @@ -408,7 +380,6 @@ Password = r.Password, UserCertificatePath = r.UserCertificatePath, UserCertificatePassword = r.UserCertificatePassword, - TargetNamespaceKind = r.TargetNamespaceKind, ProbeTimeoutSeconds = r.ProbeTimeoutSeconds, SubscriptionPublishingIntervalMs = r.SubscriptionPublishingInterval is { } spi ? (int)spi.TotalMilliseconds : null, SubscriptionKeepAliveCount = (int)Math.Min(r.SubscriptionKeepAliveCount, int.MaxValue), @@ -419,7 +390,6 @@ IssuerStorePath = r.CertificateValidation.IssuerStorePath, DiscoveryUrl = r.DiscoveryUrl, DiscoveryServerUri = r.DiscoveryServerUri, - _unsMappingTable = r.UnsMappingTable, }; public OpcUaClientDriverOptions ToRecord(IReadOnlyList endpointUrls) => new() @@ -444,8 +414,6 @@ Password = string.IsNullOrWhiteSpace(Password) ? null : Password, UserCertificatePath = string.IsNullOrWhiteSpace(UserCertificatePath) ? null : UserCertificatePath, UserCertificatePassword = string.IsNullOrWhiteSpace(UserCertificatePassword) ? null : UserCertificatePassword, - TargetNamespaceKind = TargetNamespaceKind, - UnsMappingTable = _unsMappingTable, ProbeTimeoutSeconds = ProbeTimeoutSeconds, SubscriptionPublishingInterval = SubscriptionPublishingIntervalMs is { } ms and > 0 ? TimeSpan.FromMilliseconds(ms) : null, diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientDriverBrowserTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientDriverBrowserTests.cs index cc291ed5..086f9018 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientDriverBrowserTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/OpcUaClientDriverBrowserTests.cs @@ -58,7 +58,7 @@ public sealed class OpcUaClientDriverBrowserTests /// /// AdminUI emits enum-valued driver-config fields (SecurityPolicy, SecurityMode, - /// AuthType, TargetNamespaceKind) as their string names (e.g. + /// AuthType) as their string names (e.g. /// "SecurityPolicy":"Basic256Sha256"). Without a /// on the browser's /// JsonSerializerOptions the deserialization throws a diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcProfile.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcProfile.cs index d9fbe6b3..447806b1 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcProfile.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcProfile.cs @@ -72,8 +72,5 @@ public static class OpcPlcProfile AutoAcceptCertificates = true, Timeout = TimeSpan.FromSeconds(10), SessionTimeout = TimeSpan.FromSeconds(30), - // opc-plc's standard address space mirrors verbatim — treat it as SystemPlatform so - // §8 namespace validation passes without requiring a UNS mapping table. - TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform, }; } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientFailoverTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientFailoverTests.cs index 4d68f7bf..733fba17 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientFailoverTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientFailoverTests.cs @@ -72,9 +72,6 @@ public sealed class OpcUaClientFailoverTests PerEndpointConnectTimeout = TimeSpan.FromMilliseconds(500), Timeout = TimeSpan.FromMilliseconds(500), AutoAcceptCertificates = true, - // SystemPlatform kind needs no UNS mapping table — keeps this failover test - // focused on the endpoint sweep rather than §8 namespace validation. - TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform, }; using var drv = new OpcUaClientDriver(opts, "opcua-failover"); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientMediumFindingsRegressionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientMediumFindingsRegressionTests.cs index 6da77610..e8ca622c 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientMediumFindingsRegressionTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientMediumFindingsRegressionTests.cs @@ -79,10 +79,6 @@ public sealed class OpcUaClientMediumFindingsRegressionTests EndpointUrl = "opc.tcp://127.0.0.1:1", Timeout = TimeSpan.FromMilliseconds(300), AutoAcceptCertificates = true, - // Equipment is the default TargetNamespaceKind; it requires a mapping table to - // pass ValidateNamespaceKind, which runs before BuildApplicationConfigurationAsync - // where the warning is emitted. - TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform, }; using var drv = new OpcUaClientDriver(opts, "opcua-autocert", logger); diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientNamespaceTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientNamespaceTests.cs index 5ecabe44..5939957c 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientNamespaceTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientNamespaceTests.cs @@ -6,68 +6,11 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; /// -/// Regression coverage for driver-specs.md §8 namespace handling — the -/// TargetNamespaceKind startup enforcement and the server-stable NodeId encoding -/// that produces (findings Driver.OpcUaClient-004). +/// Regression coverage for the server-stable NodeId encoding that produces (findings Driver.OpcUaClient-004). /// [Trait("Category", "Unit")] public sealed class OpcUaClientNamespaceTests { - // ---- Driver.OpcUaClient-004: TargetNamespaceKind startup enforcement ---- - - /// Verifies that Equipment namespace kind without mapping table is rejected. - [Fact] - public void ValidateNamespaceKind_Equipment_without_mapping_table_is_rejected() - { - // §8: an Equipment-kind instance gateways raw equipment data and MUST carry a - // config-driven UNS mapping table — remote nodes don't conform to UNS by default. - var opts = new OpcUaClientDriverOptions { TargetNamespaceKind = OpcUaTargetNamespaceKind.Equipment }; - Should.Throw(() => OpcUaClientDriver.ValidateNamespaceKind(opts)) - .Message.ShouldContain("UnsMappingTable"); - } - - /// Verifies that Equipment namespace kind with mapping table passes. - [Fact] - public void ValidateNamespaceKind_Equipment_with_mapping_table_passes() - { - var opts = new OpcUaClientDriverOptions - { - TargetNamespaceKind = OpcUaTargetNamespaceKind.Equipment, - UnsMappingTable = new Dictionary { ["Line1"] = "Site/AreaA/Line1" }, - }; - Should.NotThrow(() => OpcUaClientDriver.ValidateNamespaceKind(opts)); - } - - /// Verifies that SystemPlatform namespace kind with mapping table is rejected. - [Fact] - public void ValidateNamespaceKind_SystemPlatform_with_mapping_table_is_rejected() - { - // §8: processed data preserves its own hierarchy — a UNS mapping table is ambiguous. - var opts = new OpcUaClientDriverOptions - { - TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform, - UnsMappingTable = new Dictionary { ["x"] = "y" }, - }; - Should.Throw(() => OpcUaClientDriver.ValidateNamespaceKind(opts)) - .Message.ShouldContain("SystemPlatform"); - } - - /// Verifies that SystemPlatform namespace kind without mapping table passes. - [Fact] - public void ValidateNamespaceKind_SystemPlatform_without_mapping_table_passes() - { - var opts = new OpcUaClientDriverOptions { TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform }; - Should.NotThrow(() => OpcUaClientDriver.ValidateNamespaceKind(opts)); - } - - /// Verifies that the default target namespace kind is Equipment. - [Fact] - public void Default_TargetNamespaceKind_is_Equipment() - { - // §8 comparison table: OPC UA Client default lane is Equipment. - new OpcUaClientDriverOptions().TargetNamespaceKind.ShouldBe(OpcUaTargetNamespaceKind.Equipment); - } - // ---- Driver.OpcUaClient-004: server-stable NodeId encoding ---- /// Verifies that NamespaceMap.FromSession rejects null session. diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientReinitConfigAdoptionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientReinitConfigAdoptionTests.cs index 5299fdb5..10fb11ae 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientReinitConfigAdoptionTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientReinitConfigAdoptionTests.cs @@ -13,21 +13,21 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; public sealed class OpcUaClientReinitConfigAdoptionTests { /// - /// ValidateNamespaceKind runs at the top of init and rejects TargetNamespaceKind=Equipment - /// with an empty UNS mapping table. Reinitializing INTO that shape must therefore throw — a driver - /// still serving the constructor's valid options would validate those and succeed, which is exactly - /// how the discarded edit stayed invisible. + /// ValidateTuning runs at the top of init and rejects a non-absolute discoveryUrl. + /// Reinitializing INTO that shape must therefore throw — a driver still serving the constructor's + /// valid options would validate those and succeed, which is exactly how the discarded edit stayed + /// invisible. /// [Fact] public async Task Reinitialize_adopts_a_changed_config() { var driver = OpcUaClientDriverFactoryExtensions.CreateInstance( "opc1", - """{"endpointUrl":"opc.tcp://10.0.0.1:4840","targetNamespaceKind":"Equipment","unsMappingTable":{"a":"b"}}"""); + """{"endpointUrl":"opc.tcp://127.0.0.1:1"}"""); await Should.ThrowAsync( () => driver.ReinitializeAsync( - """{"endpointUrl":"opc.tcp://10.0.0.1:4840","targetNamespaceKind":"Equipment","unsMappingTable":{}}""", + """{"endpointUrl":"opc.tcp://127.0.0.1:1","discoveryUrl":"not a uri"}""", CancellationToken.None), "OpcUaClientDriver kept its constructor-supplied options after a reinitialize with a changed config (#516)"); } @@ -36,15 +36,42 @@ public sealed class OpcUaClientReinitConfigAdoptionTests [Fact] public async Task Reinitialize_with_an_empty_document_keeps_the_constructor_options() { - var driver = OpcUaClientDriverFactoryExtensions.CreateInstance( - "opc1", - """{"endpointUrl":"opc.tcp://10.0.0.1:4840","targetNamespaceKind":"Equipment","unsMappingTable":{"a":"b"}}"""); + // Built directly (the factory's parse would refuse the bad discoveryUrl up front). + using var driver = new OpcUaClientDriver( + new OpcUaClientDriverOptions { EndpointUrl = "opc.tcp://127.0.0.1:1", DiscoveryUrl = "not a uri" }, "opc1"); - // "{}" keeps the (valid) constructor options, so ValidateNamespaceKind passes and the driver gets as - // far as the connect — which fails against an unreachable endpoint. Anything BUT the validation - // throw proves the constructor options survived. - var ex = await Record.ExceptionAsync(() => driver.ReinitializeAsync("{}", CancellationToken.None)); - (ex is InvalidOperationException ioe && ioe.Message.Contains("UnsMappingTable", StringComparison.OrdinalIgnoreCase)) - .ShouldBeFalse("an empty document must keep the constructor-supplied options, not re-validate an empty config"); + // The constructor options are invalid (bad discoveryUrl) but "{}" keeps them WITHOUT re-parsing; + // a re-parse of an empty config would yield valid defaults. So tuning validation must still see + // the constructor's bad discoveryUrl and throw — a driver that re-parsed "{}" would not. + var ex = await Should.ThrowAsync( + () => driver.ReinitializeAsync("{}", CancellationToken.None)); + ex.Message.ShouldContain("DiscoveryUrl"); + } + + /// + /// #648 — targetNamespaceKind / unsMappingTable were removed (vestigial in v3). DriverConfig + /// JSON stored before the removal still carries them, in every shape the old validator used to refuse + /// (Equipment with no table, Equipment with a table, SystemPlatform with a table). None may fail the + /// parse or the initialise: the unknown keys are ignored. Falsifiable — it fails if a namespace-kind + /// validator is reinstated, because the Equipment-without-table blob would throw again. + /// + [Theory] + [InlineData("""{"endpointUrl":"opc.tcp://127.0.0.1:1","targetNamespaceKind":"Equipment"}""")] + [InlineData("""{"endpointUrl":"opc.tcp://127.0.0.1:1","targetNamespaceKind":"Equipment","unsMappingTable":{"a":"b"}}""")] + [InlineData("""{"endpointUrl":"opc.tcp://127.0.0.1:1","targetNamespaceKind":"SystemPlatform","unsMappingTable":{"a":"b"}}""")] + [InlineData("""{"endpointUrl":"opc.tcp://127.0.0.1:1","targetNamespaceKind":1,"unsMappingTable":{}}""")] + public async Task Legacy_namespace_keys_are_ignored_by_parse_and_initialise(string legacyJson) + { + Should.NotThrow(() => OpcUaClientDriverFactoryExtensions.ParseOptions("opc1", legacyJson)); + + var driver = OpcUaClientDriverFactoryExtensions.CreateInstance("opc1", legacyJson); + + // Initialise gets past config validation and fails only at the connect (nothing listens on port 1). + // Anything other than a namespace-validation InvalidOperationException proves the keys were ignored. + var ex = await Record.ExceptionAsync(() => driver.InitializeAsync(legacyJson, CancellationToken.None)); + (ex is InvalidOperationException ioe + && (ioe.Message.Contains("UnsMappingTable", StringComparison.OrdinalIgnoreCase) + || ioe.Message.Contains("TargetNamespaceKind", StringComparison.OrdinalIgnoreCase))) + .ShouldBeFalse("legacy namespace keys must be ignored, not validated"); } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/OpcUaClientDriverPageFormSerializationTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/OpcUaClientDriverPageFormSerializationTests.cs index 28dc2a66..f0002a9b 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/OpcUaClientDriverPageFormSerializationTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/OpcUaClientDriverPageFormSerializationTests.cs @@ -39,7 +39,6 @@ public sealed class OpcUaClientDriverPageFormSerializationTests BrowseRoot = "i=85", MaxDiscoveredNodes = 5000, MaxBrowseDepth = 6, - TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform, ProbeTimeoutSeconds = 20, }; @@ -64,7 +63,6 @@ public sealed class OpcUaClientDriverPageFormSerializationTests back.BrowseRoot.ShouldBe("i=85"); back.MaxDiscoveredNodes.ShouldBe(5000); back.MaxBrowseDepth.ShouldBe(6); - back.TargetNamespaceKind.ShouldBe(OpcUaTargetNamespaceKind.SystemPlatform); back.ProbeTimeoutSeconds.ShouldBe(20); } @@ -86,16 +84,9 @@ public sealed class OpcUaClientDriverPageFormSerializationTests [Fact] public void FormModel_RoundTrip_PreservesAllFields() { - // Construct options with non-default values for every editable property plus a - // non-empty UnsMappingTable (read-only in the form, round-tripped via the original - // record). EndpointUrls is now edited via the CollectionEditor on the page and is + // Construct options with non-default values for every editable property. EndpointUrls is now edited via the CollectionEditor on the page and is // threaded into ToRecord explicitly; see EndpointUrls_ListRoundTrip_PreservesOrder. var endpointUrls = new List { "opc.tcp://primary:4840", "opc.tcp://backup:4840" }; - var unsMappingTable = new Dictionary - { - ["Line1/"] = "Site/Area1/Line1", - ["Line2/"] = "Site/Area1/Line2", - }; var original = new OpcUaClientDriverOptions { @@ -119,8 +110,6 @@ public sealed class OpcUaClientDriverPageFormSerializationTests BrowseRoot = "i=85", MaxDiscoveredNodes = 3000, MaxBrowseDepth = 5, - TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform, - UnsMappingTable = unsMappingTable, ProbeTimeoutSeconds = 25, }; @@ -149,10 +138,6 @@ public sealed class OpcUaClientDriverPageFormSerializationTests result.BrowseRoot.ShouldBe("i=85"); result.MaxDiscoveredNodes.ShouldBe(3000); result.MaxBrowseDepth.ShouldBe(5); - result.TargetNamespaceKind.ShouldBe(OpcUaTargetNamespaceKind.SystemPlatform); - result.UnsMappingTable.Count.ShouldBe(2); - result.UnsMappingTable["Line1/"].ShouldBe("Site/Area1/Line1"); - result.UnsMappingTable["Line2/"].ShouldBe("Site/Area1/Line2"); result.ProbeTimeoutSeconds.ShouldBe(25); } @@ -199,6 +184,32 @@ public sealed class OpcUaClientDriverPageFormSerializationTests def.DiscoveryUrl.ShouldBeNull(); } + /// + /// #648: stored DriverConfig JSON from before targetNamespaceKind/unsMappingTable were removed + /// must still load into the form (the form's options use UnmappedMemberHandling.Skip) and the legacy + /// keys must NOT be re-emitted on save. + /// + [Fact] + public void LegacyNamespaceKeys_LoadIntoForm_AndAreNotReEmitted() + { + var formOpts = new JsonSerializerOptions(_opts) + { + UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, + Converters = { new JsonStringEnumConverter() }, + }; + const string legacy = + """{"endpointUrl":"opc.tcp://plc:4840","targetNamespaceKind":"Equipment","unsMappingTable":{"Line1/":"Site/Area1/Line1"}}"""; + + var opts = JsonSerializer.Deserialize(legacy, formOpts); + opts.ShouldNotBeNull(); + opts.EndpointUrl.ShouldBe("opc.tcp://plc:4840"); + + var saved = JsonSerializer.Serialize( + OpcUaClientDriverForm.OpcUaClientFormModel.FromRecord(opts).ToRecord([]), formOpts); + saved.ShouldNotContain("targetNamespaceKind", Case.Insensitive); + saved.ShouldNotContain("unsMappingTable", Case.Insensitive); + } + [Fact] public void EndpointUrlRow_FromUrl_ToUrl_Trims() { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/OpcUaClientDriverWireTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/OpcUaClientDriverWireTests.cs index 23f8adeb..8ea8a526 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/OpcUaClientDriverWireTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/OpcUaClientDriverWireTests.cs @@ -58,7 +58,6 @@ public sealed class OpcUaClientDriverWireTests Username = "apikey", Password = ServerEventRig.KeyPassword, AutoAcceptCertificates = true, - TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform, Timeout = TimeSpan.FromSeconds(10), SessionTimeout = TimeSpan.FromSeconds(30), KeepAliveInterval = TimeSpan.FromSeconds(1), @@ -281,7 +280,6 @@ public sealed class OpcUaClientDriverWireTests Username = "apikey", Password = ServerEventRig.KeyPassword, AutoAcceptCertificates = true, - TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform, Timeout = TimeSpan.FromSeconds(10), SessionTimeout = TimeSpan.FromSeconds(30), KeepAliveInterval = TimeSpan.FromSeconds(1),