using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Admin.Services; namespace ZB.MOM.WW.OtOpcUa.Admin.Tests; [Trait("Category", "Unit")] public sealed class EquipmentCsvImporterTests { private const string Header = "# OtOpcUaCsv v1\n" + "ZTag,MachineCode,SAPID,EquipmentId,EquipmentUuid,Name,UnsAreaName,UnsLineName"; [Fact] public void EmptyFile_Throws() { Should.Throw(() => EquipmentCsvImporter.Parse("")); } [Fact] public void MissingVersionMarker_Throws() { var csv = "ZTag,MachineCode,SAPID,EquipmentId,EquipmentUuid,Name,UnsAreaName,UnsLineName\nx,x,x,x,x,x,x,x"; var ex = Should.Throw(() => EquipmentCsvImporter.Parse(csv)); ex.Message.ShouldContain("# OtOpcUaCsv v1"); } [Fact] public void MissingRequiredColumn_Throws() { var csv = "# OtOpcUaCsv v1\n" + "ZTag,MachineCode,SAPID,EquipmentId,Name,UnsAreaName,UnsLineName\n" + "z1,mc,sap,eq1,Name1,area,line"; var ex = Should.Throw(() => EquipmentCsvImporter.Parse(csv)); ex.Message.ShouldContain("EquipmentUuid"); } [Fact] public void UnknownColumn_Throws() { var csv = Header + ",WeirdColumn\nz1,mc,sap,eq1,uu,Name1,area,line,value"; var ex = Should.Throw(() => EquipmentCsvImporter.Parse(csv)); ex.Message.ShouldContain("WeirdColumn"); } [Fact] public void DuplicateColumn_Throws() { var csv = "# OtOpcUaCsv v1\n" + "ZTag,ZTag,MachineCode,SAPID,EquipmentId,EquipmentUuid,Name,UnsAreaName,UnsLineName\n" + "z1,z1,mc,sap,eq,uu,Name,area,line"; Should.Throw(() => EquipmentCsvImporter.Parse(csv)); } [Fact] public void ValidSingleRow_RoundTrips() { var csv = Header + "\nz-001,MC-1,SAP-1,eq-001,uuid-1,Oven-A,Warsaw,Line-1"; var result = EquipmentCsvImporter.Parse(csv); result.AcceptedRows.Count.ShouldBe(1); result.RejectedRows.ShouldBeEmpty(); var row = result.AcceptedRows[0]; row.ZTag.ShouldBe("z-001"); row.MachineCode.ShouldBe("MC-1"); row.Name.ShouldBe("Oven-A"); row.UnsLineName.ShouldBe("Line-1"); } [Fact] public void OptionalColumns_Populated_WhenPresent() { var csv = "# OtOpcUaCsv v1\n" + "ZTag,MachineCode,SAPID,EquipmentId,EquipmentUuid,Name,UnsAreaName,UnsLineName,Manufacturer,Model,SerialNumber,HardwareRevision,SoftwareRevision,YearOfConstruction,AssetLocation,ManufacturerUri,DeviceManualUri\n" + "z-1,MC,SAP,eq,uuid,Oven,Warsaw,Line1,Siemens,S7-1500,SN123,Rev-1,Fw-2.3,2023,Bldg-3,https://siemens.example,https://siemens.example/manual"; var result = EquipmentCsvImporter.Parse(csv); result.AcceptedRows.Count.ShouldBe(1); var row = result.AcceptedRows[0]; row.Manufacturer.ShouldBe("Siemens"); row.Model.ShouldBe("S7-1500"); row.SerialNumber.ShouldBe("SN123"); row.YearOfConstruction.ShouldBe("2023"); row.ManufacturerUri.ShouldBe("https://siemens.example"); } [Fact] public void BlankRequiredField_Rejects_Row() { var csv = Header + "\nz-1,MC,SAP,eq,uuid,,Warsaw,Line1"; // Name blank var result = EquipmentCsvImporter.Parse(csv); result.AcceptedRows.ShouldBeEmpty(); result.RejectedRows.Count.ShouldBe(1); result.RejectedRows[0].Reason.ShouldContain("Name"); } [Fact] public void DuplicateZTag_Rejects_SecondRow() { var csv = Header + "\nz-1,MC1,SAP1,eq1,u1,N1,A,L1" + "\nz-1,MC2,SAP2,eq2,u2,N2,A,L1"; var result = EquipmentCsvImporter.Parse(csv); result.AcceptedRows.Count.ShouldBe(1); result.RejectedRows.Count.ShouldBe(1); result.RejectedRows[0].Reason.ShouldContain("Duplicate ZTag"); } [Fact] public void QuotedField_With_CommaAndQuote_Parses_Correctly() { // RFC 4180: "" inside a quoted field is an escaped quote. var csv = Header + "\n\"z-1\",\"MC\",\"SAP,with,commas\",\"eq\",\"uuid\",\"Oven \"\"Ultra\"\"\",\"Warsaw\",\"Line1\""; var result = EquipmentCsvImporter.Parse(csv); result.AcceptedRows.Count.ShouldBe(1); result.AcceptedRows[0].SAPID.ShouldBe("SAP,with,commas"); result.AcceptedRows[0].Name.ShouldBe("Oven \"Ultra\""); } [Fact] public void MismatchedColumnCount_Rejects_Row() { var csv = Header + "\nz-1,MC,SAP,eq,uuid,Name,Warsaw"; // missing UnsLineName cell var result = EquipmentCsvImporter.Parse(csv); result.AcceptedRows.ShouldBeEmpty(); result.RejectedRows.Count.ShouldBe(1); result.RejectedRows[0].Reason.ShouldContain("Column count"); } [Fact] public void BlankLines_BetweenRows_AreIgnored() { var csv = Header + "\nz-1,MC,SAP,eq1,u1,N1,A,L1" + "\n" + "\nz-2,MC,SAP,eq2,u2,N2,A,L1"; var result = EquipmentCsvImporter.Parse(csv); result.AcceptedRows.Count.ShouldBe(2); result.RejectedRows.ShouldBeEmpty(); } [Fact] public void Header_Constants_Match_Decision_117_and_139() { EquipmentCsvImporter.RequiredColumns.ShouldBe( ["ZTag", "MachineCode", "SAPID", "EquipmentId", "EquipmentUuid", "Name", "UnsAreaName", "UnsLineName"]); EquipmentCsvImporter.OptionalColumns.ShouldBe( ["Manufacturer", "Model", "SerialNumber", "HardwareRevision", "SoftwareRevision", "YearOfConstruction", "AssetLocation", "ManufacturerUri", "DeviceManualUri"]); } }