using System.Text.Json; using System.Text.Json.Nodes; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Unit tests for — the WP6 browse-leaf → /// mapper. Covers the driver data-type mapping, the per-driver address-field placement in the driver-typed /// TagConfig, and the target-group + folder-mirror path combine. /// [Trait("Category", "Unit")] public sealed class RawBrowseCommitMapperTests { // ---- MapDataType ---------------------------------------------------------------------------- [Theory] [InlineData("Boolean", "Boolean")] [InlineData("Int16", "Int16")] [InlineData("Int32", "Int32")] [InlineData("Int64", "Int64")] [InlineData("UInt16", "UInt16")] [InlineData("UInt32", "UInt32")] [InlineData("UInt64", "UInt64")] [InlineData("Float32", "Float")] [InlineData("Float64", "Double")] [InlineData("String", "String")] [InlineData("DateTime", "DateTime")] [InlineData("Reference", "String")] public void MapDataType_maps_every_driver_data_type_to_its_opcua_builtin_name(string driverType, string expected) => RawBrowseCommitMapper.MapDataType(driverType).ShouldBe(expected); [Fact] public void MapDataType_is_case_insensitive() => RawBrowseCommitMapper.MapDataType("float32").ShouldBe("Float"); [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] [InlineData("NotAType")] public void MapDataType_returns_null_for_blank_or_unknown(string? input) => RawBrowseCommitMapper.MapDataType(input).ShouldBeNull(); [Fact] public void MapDataType_covers_the_whole_DriverDataType_enum() { // Guard: if a new DriverDataType is added, this fails until MapDataType handles it. foreach (var dt in Enum.GetValues()) RawBrowseCommitMapper.MapDataType(dt.ToString()).ShouldNotBeNull( $"DriverDataType.{dt} has no OPC-UA type mapping."); } // ---- BuildTagConfig (address field per driver) ---------------------------------------------- [Theory] [InlineData("OpcUaClient", "nodeId", "ns=2;s=Channel1.Device1.Tag1")] [InlineData("AbCip", "tagPath", "Program:Main.Motor.Speed")] [InlineData("AbLegacy", "address", "N7:0")] [InlineData("TwinCAT", "symbolPath", "MAIN.bStart")] [InlineData("FOCAS", "address", "R100")] [InlineData("S7", "address", "DB1.DBW0")] [InlineData("GalaxyMxGateway", "attributeRef", "Tank_001.Level")] public void BuildTagConfig_writes_the_reference_into_the_drivers_address_field( string driverType, string expectedKey, string reference) { var json = RawBrowseCommitMapper.BuildTagConfig(driverType, reference); var o = JsonNode.Parse(json)!.AsObject(); o[expectedKey]!.GetValue().ShouldBe(reference); } [Fact] public void BuildTagConfig_is_case_insensitive_on_driver_type() { var json = RawBrowseCommitMapper.BuildTagConfig("opcuaclient", "ns=2;s=X"); JsonNode.Parse(json)!.AsObject()["nodeId"]!.GetValue().ShouldBe("ns=2;s=X"); } [Fact] public void BuildTagConfig_does_not_write_any_identity_key() { // v3: identity is the RawPath — TagConfig must NOT carry a FullName identity key. var json = RawBrowseCommitMapper.BuildTagConfig("OpcUaClient", "ns=2;s=X"); JsonNode.Parse(json)!.AsObject().ContainsKey("FullName").ShouldBeFalse(); } [Fact] public void BuildTagConfig_unknown_driver_falls_back_to_a_generic_address_key() { var json = RawBrowseCommitMapper.BuildTagConfig("Modbus", "40001"); JsonNode.Parse(json)!.AsObject()["address"]!.GetValue().ShouldBe("40001"); } // ---- CombineGroupPath ----------------------------------------------------------------------- [Theory] [InlineData(null, null, null)] [InlineData("Grp", null, "Grp")] [InlineData(null, "Sub", "Sub")] [InlineData("Grp", "Sub", "Grp/Sub")] [InlineData("Grp/Deep", "Sub/Leaf", "Grp/Deep/Sub/Leaf")] [InlineData(" ", "Sub", "Sub")] public void CombineGroupPath_joins_prefix_and_suffix(string? prefix, string? suffix, string? expected) => RawBrowseCommitMapper.CombineGroupPath(prefix, suffix).ShouldBe(expected); // ---- MapLeaf (end to end) ------------------------------------------------------------------- [Fact] public void MapLeaf_builds_a_read_row_with_typed_config_and_no_group_at_device_root() { var row = RawBrowseCommitMapper.MapLeaf( driverType: "TwinCAT", fullName: "MAIN.rSpeed", browseName: "rSpeed", driverDataType: "Float32", defaultDataType: "Double", groupPrefix: null, folderPath: new[] { "MAIN" }, createGroups: false); row.TagGroupPath.ShouldBeNull(); // mirror OFF, no target group row.Tag.Name.ShouldBe("rSpeed"); row.Tag.DataType.ShouldBe("Float"); // Float32 -> Float row.Tag.AccessLevel.ShouldBe(TagAccessLevel.Read); row.Tag.WriteIdempotent.ShouldBeFalse(); row.Tag.PollGroupId.ShouldBeNull(); JsonNode.Parse(row.Tag.TagConfig)!.AsObject()["symbolPath"]!.GetValue().ShouldBe("MAIN.rSpeed"); } [Fact] public void MapLeaf_mirrors_folder_path_onto_group_when_create_groups_on() { var row = RawBrowseCommitMapper.MapLeaf( driverType: "AbCip", fullName: "Program:Main.Motor.Speed", browseName: "Speed", driverDataType: "Int32", defaultDataType: "Double", groupPrefix: null, folderPath: new[] { "Program:Main", "Motor" }, createGroups: true); row.TagGroupPath.ShouldBe("Program:Main/Motor"); } [Fact] public void MapLeaf_prepends_target_group_prefix_and_mirror() { var row = RawBrowseCommitMapper.MapLeaf( driverType: "AbCip", fullName: "Program:Main.Motor.Speed", browseName: "Speed", driverDataType: "Int32", defaultDataType: "Double", groupPrefix: "Imported", folderPath: new[] { "Motor" }, createGroups: true); row.TagGroupPath.ShouldBe("Imported/Motor"); } [Fact] public void MapLeaf_uses_target_group_prefix_only_when_mirror_off() { var row = RawBrowseCommitMapper.MapLeaf( driverType: "AbCip", fullName: "Program:Main.Motor.Speed", browseName: "Speed", driverDataType: "Int32", defaultDataType: "Double", groupPrefix: "Imported", folderPath: new[] { "Motor" }, createGroups: false); row.TagGroupPath.ShouldBe("Imported"); } [Fact] public void MapLeaf_falls_back_to_default_data_type_when_browser_reports_none() { // OPC UA Client browse reports no attribute type — the default fills in. var row = RawBrowseCommitMapper.MapLeaf( driverType: "OpcUaClient", fullName: "ns=2;s=Tag1", browseName: "Tag1", driverDataType: null, defaultDataType: "Double", groupPrefix: null, folderPath: null, createGroups: true); // mirror ON but null folderPath -> still device root row.TagGroupPath.ShouldBeNull(); row.Tag.DataType.ShouldBe("Double"); JsonNode.Parse(row.Tag.TagConfig)!.AsObject()["nodeId"]!.GetValue().ShouldBe("ns=2;s=Tag1"); } }