using System.Text; using System.Text.Json; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Runtime.Drivers; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers; public sealed class DeploymentArtifactTests { /// Verifies that empty blob returns empty list. [Fact] public void Empty_blob_returns_empty_list() { DeploymentArtifact.ParseDriverInstances(ReadOnlySpan.Empty).ShouldBeEmpty(); } /// Verifies that malformed JSON returns empty list. [Fact] public void Malformed_json_returns_empty_list() { DeploymentArtifact.ParseDriverInstances(Encoding.UTF8.GetBytes("not json")).ShouldBeEmpty(); } /// Verifies that snapshot without DriverInstances returns empty. [Fact] public void Snapshot_without_DriverInstances_returns_empty() { var blob = Encoding.UTF8.GetBytes("{\"Clusters\":[]}"); DeploymentArtifact.ParseDriverInstances(blob).ShouldBeEmpty(); } /// Verifies that driver instances are parsed from composer-shaped blob. [Fact] public void Parses_driver_instances_from_composer_shaped_blob() { // Mirrors the shape ConfigComposer.SnapshotAndFlattenAsync emits — Pascal-case fields // serialised directly off the EF entity. var rowId = Guid.NewGuid(); var blob = JsonSerializer.SerializeToUtf8Bytes(new { DriverInstances = new[] { new { DriverInstanceRowId = rowId, DriverInstanceId = "DI-modbus-1", Name = "Modbus Line A", DriverType = "Modbus", Enabled = true, DriverConfig = "{\"host\":\"127.0.0.1\"}", }, new { DriverInstanceRowId = Guid.NewGuid(), DriverInstanceId = "DI-disabled", Name = "Decommissioned", DriverType = "AbCip", Enabled = false, DriverConfig = "{}", }, }, }); var specs = DeploymentArtifact.ParseDriverInstances(blob); specs.Count.ShouldBe(2); specs[0].DriverInstanceRowId.ShouldBe(rowId); specs[0].DriverInstanceId.ShouldBe("DI-modbus-1"); specs[0].DriverType.ShouldBe("Modbus"); specs[0].Enabled.ShouldBeTrue(); specs[0].DriverConfig.ShouldContain("127.0.0.1"); specs[1].Enabled.ShouldBeFalse(); } /// Verifies that ParseComposition returns empty for empty blob. [Fact] public void ParseComposition_returns_empty_for_empty_blob() { var c = DeploymentArtifact.ParseComposition(ReadOnlySpan.Empty); c.EquipmentNodes.ShouldBeEmpty(); c.DriverInstancePlans.ShouldBeEmpty(); c.ScriptedAlarmPlans.ShouldBeEmpty(); } /// Verifies that ParseComposition reads all three entity classes sorted by ID. [Fact] public void ParseComposition_reads_all_three_entity_classes_sorted_by_id() { var blob = JsonSerializer.SerializeToUtf8Bytes(new { Equipment = new[] { new { EquipmentId = "eq-z", MachineCode = "Z", UnsLineId = "line-1" }, new { EquipmentId = "eq-a", MachineCode = "A", UnsLineId = "line-1" }, }, DriverInstances = new[] { new { DriverInstanceId = "drv-1", DriverType = "Modbus", DriverConfig = "{}" }, }, ScriptedAlarms = new[] { new { ScriptedAlarmId = "alarm-1", EquipmentId = "eq-a", PredicateScriptId = "script-1", MessageTemplate = "high", }, }, }); var c = DeploymentArtifact.ParseComposition(blob); c.EquipmentNodes.Select(e => e.EquipmentId).ShouldBe(new[] { "eq-a", "eq-z" }); c.DriverInstancePlans.Single().DriverInstanceId.ShouldBe("drv-1"); c.ScriptedAlarmPlans.Single().ScriptedAlarmId.ShouldBe("alarm-1"); } /// /// Verifies ParseComposition surfaces Equipment-namespace tags (non-null EquipmentId in an /// Equipment-kind namespace) as EquipmentTags, with FullName extracted /// from the tag's TagConfig blob — the equipment-signal mirror of the Galaxy-tag path. A /// SystemPlatform (Galaxy) tag in the same blob must NOT leak into EquipmentTags and must /// still route to GalaxyTags. /// [Fact] public void ParseComposition_reads_EquipmentTags_from_equipment_namespace() { var blob = JsonSerializer.SerializeToUtf8Bytes(new { Namespaces = new[] { new { NamespaceId = "ns-eq", Kind = 0 }, // NamespaceKind.Equipment new { NamespaceId = "ns-sp", Kind = 1 }, // NamespaceKind.SystemPlatform }, DriverInstances = new[] { new { DriverInstanceId = "drv-modbus", DriverType = "Modbus", DriverConfig = "{}", NamespaceId = "ns-eq" }, new { DriverInstanceId = "drv-galaxy", DriverType = "Galaxy", DriverConfig = "{}", NamespaceId = "ns-sp" }, }, Tags = new object[] { new { TagId = "tag-eq", DriverInstanceId = "drv-modbus", EquipmentId = "eq-1", Name = "Speed", FolderPath = (string?)null, DataType = "Float", TagConfig = "{\"FullName\":\"40001\"}", }, new { TagId = "tag-gx", DriverInstanceId = "drv-galaxy", EquipmentId = (string?)null, Name = "Temp", FolderPath = "area", DataType = "Float", TagConfig = "{\"FullName\":\"area.Temp\"}", }, }, }); var c = DeploymentArtifact.ParseComposition(blob); var tag = c.EquipmentTags.ShouldHaveSingleItem(); tag.TagId.ShouldBe("tag-eq"); tag.EquipmentId.ShouldBe("eq-1"); tag.DriverInstanceId.ShouldBe("drv-modbus"); tag.Name.ShouldBe("Speed"); tag.DataType.ShouldBe("Float"); tag.FullName.ShouldBe("40001"); // extracted from TagConfig, not the raw blob // The Galaxy tag still routes to GalaxyTags and does NOT leak into EquipmentTags. c.GalaxyTags.ShouldContain(g => g.TagId == "tag-gx"); } /// /// Verifies ParseComposition sets the equipment folder DisplayName to the UNS Name /// segment — the source the live rebuild actually uses — not the colloquial MachineCode, so /// equipment browses by its friendly UNS name. NodeId stays the logical EquipmentId. /// [Fact] public void ParseComposition_equipment_DisplayName_is_UNS_Name_not_MachineCode() { var blob = JsonSerializer.SerializeToUtf8Bytes(new { Equipment = new[] { new { EquipmentId = "eq-1", Name = "filling-eq", MachineCode = "FILLING-EQ", UnsLineId = "line-1" }, }, }); var node = DeploymentArtifact.ParseComposition(blob).EquipmentNodes.ShouldHaveSingleItem(); node.EquipmentId.ShouldBe("eq-1"); node.DisplayName.ShouldBe("filling-eq"); } /// Verifies that specs missing required fields are dropped. [Fact] public void Spec_missing_required_fields_is_dropped() { var blob = JsonSerializer.SerializeToUtf8Bytes(new { DriverInstances = new object[] { new { Name = "no-id" }, new { DriverInstanceId = "DI-ok", DriverType = "Modbus", DriverConfig = "{}", }, }, }); var specs = DeploymentArtifact.ParseDriverInstances(blob); specs.Single().DriverInstanceId.ShouldBe("DI-ok"); } }