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