fix(admin): resolve Low code-review findings (Admin-010,011,012)
- Admin-010: vendor Bootstrap 5.3.3 (CSS + JS bundle + maps + provenance README) under wwwroot/lib/bootstrap and reference local paths from App.razor — Admin no longer pulls Bootstrap from jsDelivr. - Admin-011: swap FleetStatusPoller's three plain dictionaries for ConcurrentDictionary so ResetCache can't race a poll tick. - Admin-012: drop the EquipmentId column from EquipmentCsvImporter (per admin-ui.md — equipment id is system-derived from EquipmentUuid); EquipmentImportBatchService and the textarea placeholder updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Admin.Services;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Admin.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Regression for Admin-012 — <c>admin-ui.md</c> ("Equipment CSV import", revised after
|
||||
/// adversarial review finding #4) requires no <c>EquipmentId</c> column: it is
|
||||
/// system-derived (<c>'EQ-' + first 12 hex chars of EquipmentUuid</c>) and "never
|
||||
/// accepted from CSV imports". Operator-supplied EquipmentId would mint duplicate
|
||||
/// equipment identity on typos.
|
||||
/// </summary>
|
||||
[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<InvalidCsvFormatException>(() => 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user