using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Admin.Services; namespace ZB.MOM.WW.OtOpcUa.Admin.Tests; /// /// Regression for Admin-012 — admin-ui.md ("Equipment CSV import", revised after /// adversarial review finding #4) requires no EquipmentId column: it is /// system-derived ('EQ-' + first 12 hex chars of EquipmentUuid) and "never /// accepted from CSV imports". Operator-supplied EquipmentId would mint duplicate /// equipment identity on typos. /// [Trait("Category", "Unit")] public sealed class EquipmentCsvNoEquipmentIdColumnTests { [Fact] public void RequiredColumns_does_not_include_EquipmentId() { EquipmentCsvImporter.RequiredColumns .ShouldNotContain("EquipmentId", customMessage: "Admin-012: admin-ui.md forbids an EquipmentId column on the CSV import — it is system-derived from EquipmentUuid."); } [Fact] public void OptionalColumns_does_not_include_EquipmentId() { EquipmentCsvImporter.OptionalColumns .ShouldNotContain("EquipmentId", customMessage: "Admin-012: EquipmentId must not be an optional column either — it is never accepted from the CSV."); } [Fact] public void EquipmentCsvRow_has_no_EquipmentId_property() { // The CSV row shape mirrors the accepted columns. Keeping EquipmentId on the // row would invite the same misuse — drop it so the type system prevents // accidental population from a future column. var prop = typeof(EquipmentCsvRow).GetProperty("EquipmentId"); prop.ShouldBeNull("Admin-012: EquipmentCsvRow must not expose an EquipmentId — the value is derived at finalise time."); } [Fact] public void Header_with_EquipmentId_column_is_rejected_as_unknown() { // After the fix, EquipmentId is an unknown column — the header validator must // refuse it like any other unrecognised column so operators get an explicit // error rather than silently importing the value. const string csv = "# OtOpcUaCsv v1\n" + "ZTag,MachineCode,SAPID,EquipmentId,EquipmentUuid,Name,UnsAreaName,UnsLineName\n" + "z-1,MC,SAP,eq,uuid,Oven,Warsaw,Line1"; var ex = Should.Throw(() => EquipmentCsvImporter.Parse(csv)); ex.Message.ShouldContain("EquipmentId", customMessage: "Importer must reject CSVs that still carry the (now disallowed) EquipmentId column."); } [Fact] public void Valid_csv_without_EquipmentId_is_accepted() { // The canonical header should now omit EquipmentId. const string csv = "# OtOpcUaCsv v1\n" + "ZTag,MachineCode,SAPID,EquipmentUuid,Name,UnsAreaName,UnsLineName\n" + "z-1,MC,SAP,11111111-2222-3333-4444-555555555555,Oven,Warsaw,Line1"; var result = EquipmentCsvImporter.Parse(csv); result.AcceptedRows.Count.ShouldBe(1); result.RejectedRows.ShouldBeEmpty(); result.AcceptedRows[0].ZTag.ShouldBe("z-1"); result.AcceptedRows[0].EquipmentUuid.ShouldBe("11111111-2222-3333-4444-555555555555"); } }