using Opc.Ua; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; /// /// Unit tests for the curation surface added in PR-7 (selective import + namespace /// remap + root-alias). Focused on the pure-function helpers /// (, /// , /// ) so the assertions don't need a /// live upstream server. End-to-end browse + filter coverage lands in the integration /// tests against opc-plc. /// [Trait("Category", "Unit")] public sealed class OpcUaClientCurationTests { [Fact] public void CurationOptions_default_is_empty_so_existing_behaviour_is_preserved() { var opts = new OpcUaClientDriverOptions(); opts.Curation.IncludePaths.ShouldBeNull(); opts.Curation.ExcludePaths.ShouldBeNull(); opts.Curation.NamespaceRemap.ShouldBeNull(); opts.Curation.RootAlias.ShouldBeNull(); } [Fact] public void ShouldInclude_empty_include_means_include_all() { OpcUaClientDriver.ShouldInclude("Server/Status", include: null, exclude: null).ShouldBeTrue(); OpcUaClientDriver.ShouldInclude("anything", include: null, exclude: null).ShouldBeTrue(); } [Fact] public void ShouldInclude_star_glob_matches_any_segment_chars() { var include = OpcUaClientDriver.CompileGlobs(new[] { "Plant/*" }); OpcUaClientDriver.ShouldInclude("Plant/Tank1", include, null).ShouldBeTrue(); OpcUaClientDriver.ShouldInclude("Plant/Tank1/Level", include, null).ShouldBeTrue("'*' matches across slashes — single-glob simple semantics"); OpcUaClientDriver.ShouldInclude("Other/Tank1", include, null).ShouldBeFalse(); } [Fact] public void ShouldInclude_question_mark_matches_exactly_one_char() { var include = OpcUaClientDriver.CompileGlobs(new[] { "Tank?" }); OpcUaClientDriver.ShouldInclude("Tank1", include, null).ShouldBeTrue(); OpcUaClientDriver.ShouldInclude("Tank12", include, null).ShouldBeFalse(); OpcUaClientDriver.ShouldInclude("Tank", include, null).ShouldBeFalse(); } [Fact] public void ShouldInclude_exclude_wins_over_include() { var include = OpcUaClientDriver.CompileGlobs(new[] { "Plant/*" }); var exclude = OpcUaClientDriver.CompileGlobs(new[] { "Plant/Diagnostics*" }); OpcUaClientDriver.ShouldInclude("Plant/Tank1", include, exclude).ShouldBeTrue(); OpcUaClientDriver.ShouldInclude("Plant/Diagnostics", include, exclude).ShouldBeFalse(); OpcUaClientDriver.ShouldInclude("Plant/DiagnosticsTree/Sub", include, exclude).ShouldBeFalse(); } [Fact] public void ShouldInclude_multiple_includes_are_OR() { var include = OpcUaClientDriver.CompileGlobs(new[] { "PlantA/*", "PlantB/*" }); OpcUaClientDriver.ShouldInclude("PlantA/Tank", include, null).ShouldBeTrue(); OpcUaClientDriver.ShouldInclude("PlantB/Tank", include, null).ShouldBeTrue(); OpcUaClientDriver.ShouldInclude("PlantC/Tank", include, null).ShouldBeFalse(); } [Fact] public void CompileGlobs_returns_null_for_empty_or_null_input() { OpcUaClientDriver.CompileGlobs(null).ShouldBeNull(); OpcUaClientDriver.CompileGlobs(System.Array.Empty()).ShouldBeNull(); OpcUaClientDriver.CompileGlobs(new[] { string.Empty }).ShouldBeNull(); } [Fact] public void CompileGlobs_escapes_regex_metacharacters_in_literal_segments() { // A bare "." in the glob must NOT match arbitrary chars — it's escaped to a literal dot. var rx = OpcUaClientDriver.CompileGlobs(new[] { "App.exe" }); rx.ShouldNotBeNull(); rx!.IsMatch("App.exe").ShouldBeTrue(); rx.IsMatch("AppXexe").ShouldBeFalse(); } [Fact] public void BuildRemappedFullName_returns_default_string_when_remap_is_empty() { var node = new NodeId("Tank/Level", 2); OpcUaClientDriver.BuildRemappedFullName(node, table: null, remap: null) .ShouldBe(node.ToString()); } [Fact] public void BuildRemappedFullName_passes_through_namespace_zero_unchanged() { // Standard OPC UA nodes (ns=0) bypass remap so the base namespace round-trips. var node = new NodeId(2253u); // Server var remap = new Dictionary { ["http://opcfoundation.org/UA/"] = "urn:local:base" }; OpcUaClientDriver.BuildRemappedFullName(node, table: null, remap: remap) .ShouldBe(node.ToString()); } [Fact] public void BuildRemappedFullName_translates_upstream_uri_when_remap_hits() { var table = new NamespaceTable(); table.Append("urn:upstream:plc-vendor"); // index 1 var node = new NodeId("Tank/Level", 1); var remap = new Dictionary { ["urn:upstream:plc-vendor"] = "urn:local:plant", }; var result = OpcUaClientDriver.BuildRemappedFullName(node, table, remap); result.ShouldContain("urn:local:plant", Case.Sensitive); result.ShouldNotContain("urn:upstream:plc-vendor"); } [Fact] public void BuildRemappedFullName_no_op_when_upstream_uri_not_in_remap_table() { var table = new NamespaceTable(); table.Append("urn:upstream:other"); // index 1 var node = new NodeId("Some/Item", 1); var remap = new Dictionary { ["urn:not-matching"] = "urn:local:something", }; OpcUaClientDriver.BuildRemappedFullName(node, table, remap).ShouldBe(node.ToString()); } [Fact] public void RootAlias_default_null_means_Remote() { var opts = new OpcUaClientDriverOptions(); opts.Curation.RootAlias.ShouldBeNull(); } [Fact] public void RootAlias_can_be_set_via_Curation_record() { var opts = new OpcUaClientDriverOptions { Curation = new OpcUaClientCurationOptions(RootAlias: "Plant"), }; opts.Curation.RootAlias.ShouldBe("Plant"); } }