Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.Admin.Tests/EquipmentCsvImporterTests.cs
Joseph Doherty a25593a9c6 chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:55:28 -04:00

170 lines
5.7 KiB
C#

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<InvalidCsvFormatException>(() => 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<InvalidCsvFormatException>(() => 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<InvalidCsvFormatException>(() => 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<InvalidCsvFormatException>(() => 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<InvalidCsvFormatException>(() => 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"]);
}
}