using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; /// /// Gitea #516 — OpcUaClientDriver.InitializeAsync used to serve the options its CONSTRUCTOR /// captured. Its own comment claimed "resolving on every InitializeAsync … picks up rotations", which /// was true of SECRET rotation only: the endpoint, the security policy and the tag set were all frozen /// at construction, so an operator's edit was discarded while the deployment still sealed green. /// [Trait("Category", "Unit")] public sealed class OpcUaClientReinitConfigAdoptionTests { /// /// 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://127.0.0.1:1"}"""); await Should.ThrowAsync( () => driver.ReinitializeAsync( """{"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)"); } /// An empty/placeholder document must still keep the constructor options. [Fact] public async Task Reinitialize_with_an_empty_document_keeps_the_constructor_options() { // 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"); // 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"); } }