using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.OpcUa; namespace ZB.MOM.WW.OtOpcUa.Core.Tests.OpcUa; [Trait("Category", "Unit")] public sealed class IdentificationFolderBuilderTests { private sealed class RecordingBuilder : IAddressSpaceBuilder { public List<(string BrowseName, string DisplayName)> Folders { get; } = []; public List<(string BrowseName, DriverDataType DataType, object? Value)> Properties { get; } = []; public IAddressSpaceBuilder Folder(string browseName, string displayName) { Folders.Add((browseName, displayName)); return this; // flat recording — identification fields land in the same bucket } public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo) => throw new NotSupportedException("Identification fields use AddProperty, not Variable"); public void AddProperty(string browseName, DriverDataType dataType, object? value) => Properties.Add((browseName, dataType, value)); } private static Equipment EmptyEquipment() => new() { EquipmentId = "EQ-000000000001", DriverInstanceId = "drv-1", UnsLineId = "line-1", Name = "eq-1", MachineCode = "machine_001", }; private static Equipment FullyPopulatedEquipment() => new() { EquipmentId = "EQ-000000000001", DriverInstanceId = "drv-1", UnsLineId = "line-1", Name = "eq-1", MachineCode = "machine_001", Manufacturer = "Siemens", Model = "S7-1500", SerialNumber = "SN-12345", HardwareRevision = "Rev-A", SoftwareRevision = "Fw-2.3.1", YearOfConstruction = 2023, AssetLocation = "Warsaw-West/Bldg-3", ManufacturerUri = "https://siemens.example", DeviceManualUri = "https://siemens.example/manual", }; [Fact] public void HasAnyFields_AllNull_ReturnsFalse() { IdentificationFolderBuilder.HasAnyFields(EmptyEquipment()).ShouldBeFalse(); } [Fact] public void HasAnyFields_OneNonNull_ReturnsTrue() { var eq = EmptyEquipment(); eq.SerialNumber = "SN-1"; IdentificationFolderBuilder.HasAnyFields(eq).ShouldBeTrue(); } [Fact] public void Build_AllNull_ReturnsNull_AndDoesNotEmit_Folder() { var builder = new RecordingBuilder(); var result = IdentificationFolderBuilder.Build(builder, EmptyEquipment()); result.ShouldBeNull(); builder.Folders.ShouldBeEmpty("no Identification folder when every field is null"); builder.Properties.ShouldBeEmpty(); } [Fact] public void Build_FullyPopulated_EmitsAllNineFields() { var builder = new RecordingBuilder(); var result = IdentificationFolderBuilder.Build(builder, FullyPopulatedEquipment()); result.ShouldNotBeNull(); builder.Folders.ShouldContain(f => f.BrowseName == "Identification"); builder.Properties.Count.ShouldBe(9); builder.Properties.Select(p => p.BrowseName).ShouldBe( ["Manufacturer", "Model", "SerialNumber", "HardwareRevision", "SoftwareRevision", "YearOfConstruction", "AssetLocation", "ManufacturerUri", "DeviceManualUri"], "property order matches decision #139 exactly"); } [Fact] public void Build_OnlyNonNull_Are_Emitted() { var eq = EmptyEquipment(); eq.Manufacturer = "Siemens"; eq.SerialNumber = "SN-1"; eq.YearOfConstruction = 2024; var builder = new RecordingBuilder(); IdentificationFolderBuilder.Build(builder, eq); builder.Properties.Count.ShouldBe(3, "only the 3 non-null fields are exposed"); builder.Properties.Select(p => p.BrowseName).ShouldBe( ["Manufacturer", "SerialNumber", "YearOfConstruction"]); } [Fact] public void YearOfConstruction_Maps_Short_To_Int32_DriverDataType() { var eq = EmptyEquipment(); eq.YearOfConstruction = 2023; var builder = new RecordingBuilder(); IdentificationFolderBuilder.Build(builder, eq); var prop = builder.Properties.Single(p => p.BrowseName == "YearOfConstruction"); prop.DataType.ShouldBe(DriverDataType.Int32); prop.Value.ShouldBe(2023, "short is widened to int for OPC UA Int32 representation"); } [Fact] public void Build_StringValues_RoundTrip() { var eq = FullyPopulatedEquipment(); var builder = new RecordingBuilder(); IdentificationFolderBuilder.Build(builder, eq); builder.Properties.Single(p => p.BrowseName == "Manufacturer").Value.ShouldBe("Siemens"); builder.Properties.Single(p => p.BrowseName == "DeviceManualUri").Value.ShouldBe("https://siemens.example/manual"); } [Fact] public void FieldNames_Match_Decision139_Exactly() { IdentificationFolderBuilder.FieldNames.ShouldBe( ["Manufacturer", "Model", "SerialNumber", "HardwareRevision", "SoftwareRevision", "YearOfConstruction", "AssetLocation", "ManufacturerUri", "DeviceManualUri"]); } [Fact] public void FolderName_Is_Identification() { IdentificationFolderBuilder.FolderName.ShouldBe("Identification"); } }