refactor(opcuaclient): remove vestigial TargetNamespaceKind + UnsMappingTable (#648)
Nothing read the mapping table beyond the startup validator; v3 UNS projection is
explicit via UnsTagReference. Legacy keys in stored DriverConfig are ignored (pinned).
Reverts the docker-dev opc-plc seed workaround from fdee129f.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+1
-1
@@ -58,7 +58,7 @@ public sealed class OpcUaClientDriverBrowserTests
|
||||
|
||||
/// <summary>
|
||||
/// AdminUI emits enum-valued driver-config fields (SecurityPolicy, SecurityMode,
|
||||
/// AuthType, TargetNamespaceKind) as their <b>string</b> names (e.g.
|
||||
/// AuthType) as their <b>string</b> names (e.g.
|
||||
/// <c>"SecurityPolicy":"Basic256Sha256"</c>). Without a
|
||||
/// <see cref="System.Text.Json.Serialization.JsonStringEnumConverter"/> on the browser's
|
||||
/// <c>JsonSerializerOptions</c> the deserialization throws a
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
-4
@@ -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);
|
||||
|
||||
|
||||
+1
-58
@@ -6,68 +6,11 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression coverage for driver-specs.md §8 namespace handling — the
|
||||
/// <c>TargetNamespaceKind</c> startup enforcement and the server-stable NodeId encoding
|
||||
/// that <see cref="NamespaceMap"/> produces (findings Driver.OpcUaClient-004).
|
||||
/// Regression coverage for the server-stable NodeId encoding that <see cref="NamespaceMap"/> produces (findings Driver.OpcUaClient-004).
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class OpcUaClientNamespaceTests
|
||||
{
|
||||
// ---- Driver.OpcUaClient-004: TargetNamespaceKind startup enforcement ----
|
||||
|
||||
/// <summary>Verifies that Equipment namespace kind without mapping table is rejected.</summary>
|
||||
[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<InvalidOperationException>(() => OpcUaClientDriver.ValidateNamespaceKind(opts))
|
||||
.Message.ShouldContain("UnsMappingTable");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that Equipment namespace kind with mapping table passes.</summary>
|
||||
[Fact]
|
||||
public void ValidateNamespaceKind_Equipment_with_mapping_table_passes()
|
||||
{
|
||||
var opts = new OpcUaClientDriverOptions
|
||||
{
|
||||
TargetNamespaceKind = OpcUaTargetNamespaceKind.Equipment,
|
||||
UnsMappingTable = new Dictionary<string, string> { ["Line1"] = "Site/AreaA/Line1" },
|
||||
};
|
||||
Should.NotThrow(() => OpcUaClientDriver.ValidateNamespaceKind(opts));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SystemPlatform namespace kind with mapping table is rejected.</summary>
|
||||
[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<string, string> { ["x"] = "y" },
|
||||
};
|
||||
Should.Throw<InvalidOperationException>(() => OpcUaClientDriver.ValidateNamespaceKind(opts))
|
||||
.Message.ShouldContain("SystemPlatform");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that SystemPlatform namespace kind without mapping table passes.</summary>
|
||||
[Fact]
|
||||
public void ValidateNamespaceKind_SystemPlatform_without_mapping_table_passes()
|
||||
{
|
||||
var opts = new OpcUaClientDriverOptions { TargetNamespaceKind = OpcUaTargetNamespaceKind.SystemPlatform };
|
||||
Should.NotThrow(() => OpcUaClientDriver.ValidateNamespaceKind(opts));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the default target namespace kind is Equipment.</summary>
|
||||
[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 ----
|
||||
|
||||
/// <summary>Verifies that NamespaceMap.FromSession rejects null session.</summary>
|
||||
|
||||
+42
-15
@@ -13,21 +13,21 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||
public sealed class OpcUaClientReinitConfigAdoptionTests
|
||||
{
|
||||
/// <summary>
|
||||
/// <c>ValidateNamespaceKind</c> runs at the top of init and rejects <c>TargetNamespaceKind=Equipment</c>
|
||||
/// 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.
|
||||
/// <c>ValidateTuning</c> runs at the top of init and rejects a non-absolute <c>discoveryUrl</c>.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<InvalidOperationException>(
|
||||
() => 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<InvalidOperationException>(
|
||||
() => driver.ReinitializeAsync("{}", CancellationToken.None));
|
||||
ex.Message.ShouldContain("DiscoveryUrl");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #648 — <c>targetNamespaceKind</c> / <c>unsMappingTable</c> 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.
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
|
||||
+27
-16
@@ -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<string> { "opc.tcp://primary:4840", "opc.tcp://backup:4840" };
|
||||
var unsMappingTable = new Dictionary<string, string>
|
||||
{
|
||||
["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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #648: stored DriverConfig JSON from before <c>targetNamespaceKind</c>/<c>unsMappingTable</c> were removed
|
||||
/// must still load into the form (the form's options use <c>UnmappedMemberHandling.Skip</c>) and the legacy
|
||||
/// keys must NOT be re-emitted on save.
|
||||
/// </summary>
|
||||
[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<OpcUaClientDriverOptions>(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()
|
||||
{
|
||||
|
||||
-2
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user