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");
}
}