using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; /// /// Verifies the bulk path: valid rows insert, /// rows whose MachineCode already exists (in the DB or earlier in the same batch) are skipped, /// rows referencing an unknown UNS line or unknown driver are reported as errors, and the /// decision-#122 driver-cluster guard rejects a driver in a different cluster than the line. /// [Trait("Category", "Unit")] public sealed class UnsTreeServiceImportTests { private static (UnsTreeService Service, string DbName) Fresh() { var dbName = $"uns-import-{Guid.NewGuid():N}"; return (new UnsTreeService(UnsTreeTestDb.Factory(dbName)), dbName); } /// /// Seeds a line under an area in , plus an optional driver in /// . The line id is always LINE-1; the driver (when /// requested) is always DRV-1. /// private static void SeedLineAndDriver(string dbName, string lineCluster, string? driverCluster) { using var db = UnsTreeTestDb.CreateNamed(dbName); db.UnsAreas.Add(new UnsArea { UnsAreaId = "AREA-1", ClusterId = lineCluster, Name = "a" }); db.UnsLines.Add(new UnsLine { UnsLineId = "LINE-1", UnsAreaId = "AREA-1", Name = "l" }); if (driverCluster is not null) { db.DriverInstances.Add(new DriverInstance { DriverInstanceId = "DRV-1", ClusterId = driverCluster, NamespaceId = "NS-1", Name = "drv", DriverType = "ModbusTcp", DriverConfig = "{}", }); } db.SaveChanges(); } private static EquipmentInput Input(string name, string machineCode, string unsLineId, string? driverInstanceId) => new(name, machineCode, unsLineId, driverInstanceId, ZTag: null, SAPID: null, Manufacturer: null, Model: null, SerialNumber: null, HardwareRevision: null, SoftwareRevision: null, YearOfConstruction: null, AssetLocation: null, ManufacturerUri: null, DeviceManualUri: null, Enabled: true); /// Valid rows insert with system-generated EQ- ids and are counted in Inserted. [Fact] public async Task Import_inserts_valid_rows() { var (service, dbName) = Fresh(); SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null); var result = await service.ImportEquipmentAsync( [ Input("machine-1", "mc_1", "LINE-1", null), Input("machine-2", "mc_2", "LINE-1", null), ]); result.Inserted.ShouldBe(2); result.Skipped.ShouldBe(0); result.Errors.ShouldBeEmpty(); using var db = UnsTreeTestDb.CreateNamed(dbName); db.Equipment.Count().ShouldBe(2); var eq = db.Equipment.Single(e => e.MachineCode == "mc_1"); eq.EquipmentId.ShouldStartWith("EQ-"); eq.EquipmentId.Length.ShouldBe(15); // "EQ-" + 12 hex chars eq.Name.ShouldBe("machine-1"); eq.UnsLineId.ShouldBe("LINE-1"); eq.Enabled.ShouldBeTrue(); } /// /// A row whose MachineCode already exists in the DB is skipped (not an error), and a second row /// later in the batch that reuses an earlier row's MachineCode is also skipped. /// [Fact] public async Task Import_skips_duplicate_machinecode() { var (service, dbName) = Fresh(); SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null); // Pre-existing equipment with MachineCode "mc_existing". await service.CreateEquipmentAsync(Input("machine-x", "mc_existing", "LINE-1", null)); var result = await service.ImportEquipmentAsync( [ Input("machine-1", "mc_existing", "LINE-1", null), // dup of DB row → skip Input("machine-2", "mc_new", "LINE-1", null), // inserts Input("machine-3", "mc_new", "LINE-1", null), // dup of in-batch row → skip ]); result.Inserted.ShouldBe(1); result.Skipped.ShouldBe(2); result.Errors.ShouldBeEmpty(); using var db = UnsTreeTestDb.CreateNamed(dbName); db.Equipment.Count(e => e.MachineCode == "mc_new").ShouldBe(1); db.Equipment.Count(e => e.MachineCode == "mc_existing").ShouldBe(1); } /// A row whose UnsLineId does not exist is reported as an error and not inserted. [Fact] public async Task Import_reports_unknown_line() { var (service, dbName) = Fresh(); SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null); var result = await service.ImportEquipmentAsync( [ Input("machine-1", "mc_1", "LINE-BOGUS", null), Input("machine-2", "mc_2", "LINE-1", null), ]); result.Inserted.ShouldBe(1); result.Skipped.ShouldBe(0); result.Errors.Count.ShouldBe(1); result.Errors[0].ShouldContain("mc_1"); result.Errors[0].ShouldContain("LINE-BOGUS"); using var db = UnsTreeTestDb.CreateNamed(dbName); db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse(); db.Equipment.Any(e => e.MachineCode == "mc_2").ShouldBeTrue(); } /// A driver-bound row whose DriverInstanceId does not resolve is reported as an error. [Fact] public async Task Import_reports_unknown_driver() { var (service, dbName) = Fresh(); SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null); // no driver seeded var result = await service.ImportEquipmentAsync( [ Input("machine-1", "mc_1", "LINE-1", "DRV-GHOST"), ]); result.Inserted.ShouldBe(0); result.Skipped.ShouldBe(0); result.Errors.Count.ShouldBe(1); result.Errors[0].ShouldContain("mc_1"); result.Errors[0].ShouldContain("DRV-GHOST"); result.Errors[0].ShouldContain("not found"); using var db = UnsTreeTestDb.CreateNamed(dbName); db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse(); } /// /// The decision-#122 guard rejects a row that binds a driver living in a different cluster than /// the row's UNS line. /// [Fact] public async Task Import_enforces_122_cluster() { var (service, dbName) = Fresh(); SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "SITE-A"); var result = await service.ImportEquipmentAsync( [ Input("machine-1", "mc_1", "LINE-1", "DRV-1"), ]); result.Inserted.ShouldBe(0); result.Skipped.ShouldBe(0); result.Errors.Count.ShouldBe(1); result.Errors[0].ShouldContain("mc_1"); result.Errors[0].ShouldContain("decision #122"); using var db = UnsTreeTestDb.CreateNamed(dbName); db.Equipment.Any(e => e.MachineCode == "mc_1").ShouldBeFalse(); } /// A driver in the same cluster as the line imports cleanly. [Fact] public async Task Import_allows_driver_in_same_cluster() { var (service, dbName) = Fresh(); SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "MAIN"); var result = await service.ImportEquipmentAsync( [ Input("machine-1", "mc_1", "LINE-1", "DRV-1"), ]); result.Inserted.ShouldBe(1); result.Skipped.ShouldBe(0); result.Errors.ShouldBeEmpty(); using var db = UnsTreeTestDb.CreateNamed(dbName); db.Equipment.Single(e => e.MachineCode == "mc_1").DriverInstanceId.ShouldBe("DRV-1"); } }