fix(uns): reject driver-bind on unresolvable line + enforce MachineCode uniqueness on update (review)

This commit is contained in:
Joseph Doherty
2026-06-08 12:55:36 -04:00
parent 2836a0704b
commit ab0ff8aedf
2 changed files with 114 additions and 4 deletions
@@ -157,6 +157,41 @@ public sealed class UnsTreeServiceEquipmentTests
db.Equipment.Single(e => e.MachineCode == "machine_001").DriverInstanceId.ShouldBeNull();
}
/// <summary>
/// The #122 guard blocks binding equipment to a driver when the UNS line does not resolve to
/// a cluster (e.g. the line does not exist in the DB).
/// </summary>
[Fact]
public async Task CreateEquipment_driver_bound_unresolvable_line_blocked()
{
var (service, dbName) = Fresh();
// Seed a driver in MAIN cluster, but do NOT create the UnsLine that the input references.
using (var db = UnsTreeTestDb.CreateNamed(dbName))
{
db.DriverInstances.Add(new DriverInstance
{
DriverInstanceId = "DRV-1",
ClusterId = "MAIN",
NamespaceId = "NS-1",
Name = "drv",
DriverType = "ModbusTcp",
DriverConfig = "{}",
});
db.SaveChanges();
}
var result = await service.CreateEquipmentAsync(
Input("machine-1", "machine_001", "LINE-BOGUS", "DRV-1"));
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
result.Error.ShouldContain("decision #122");
result.Error.ShouldContain("LINE-BOGUS");
using var verify = UnsTreeTestDb.CreateNamed(dbName);
verify.Equipment.Any(e => e.MachineCode == "machine_001").ShouldBeFalse();
}
// ----- UpdateEquipment -----
/// <summary>Updating equipment changes its mutable fields (name, MachineCode, a 40010 field).</summary>
@@ -235,6 +270,64 @@ public sealed class UnsTreeServiceEquipmentTests
verify.Equipment.Single(e => e.EquipmentId == equipmentId).DriverInstanceId.ShouldBeNull();
}
/// <summary>Updating equipment with a MachineCode that already belongs to another row is blocked.</summary>
[Fact]
public async Task UpdateEquipment_duplicate_machinecode_blocked()
{
var (service, dbName) = Fresh();
SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: null);
await service.CreateEquipmentAsync(Input("machine-a", "mc_a", "LINE-1", null));
await service.CreateEquipmentAsync(Input("machine-b", "mc_b", "LINE-1", null));
string equipmentId;
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
{
var eq = db.Equipment.Single(e => e.MachineCode == "mc_a");
equipmentId = eq.EquipmentId;
rv = eq.RowVersion;
}
// Try to rename mc_a → mc_b (which already exists).
var result = await service.UpdateEquipmentAsync(
equipmentId, Input("machine-a", "mc_b", "LINE-1", null), rv);
result.Ok.ShouldBeFalse();
result.Error.ShouldNotBeNull();
result.Error.ShouldBe("MachineCode 'mc_b' already exists in this fleet.");
// The original row must be unchanged.
using var verify = UnsTreeTestDb.CreateNamed(dbName);
verify.Equipment.Single(e => e.EquipmentId == equipmentId).MachineCode.ShouldBe("mc_a");
}
/// <summary>Updating equipment to bind a driver that is in the SAME cluster as the line is allowed.</summary>
[Fact]
public async Task UpdateEquipment_driver_in_same_cluster_allowed()
{
var (service, dbName) = Fresh();
SeedLineAndDriver(dbName, lineCluster: "MAIN", driverCluster: "MAIN");
await service.CreateEquipmentAsync(Input("machine-1", "machine_001", "LINE-1", null));
string equipmentId;
byte[] rv;
using (var db = UnsTreeTestDb.CreateNamed(dbName))
{
var eq = db.Equipment.Single(e => e.MachineCode == "machine_001");
equipmentId = eq.EquipmentId;
rv = eq.RowVersion;
}
var result = await service.UpdateEquipmentAsync(
equipmentId, Input("machine-1", "machine_001", "LINE-1", "DRV-1"), rv);
result.Ok.ShouldBeTrue();
result.Error.ShouldBeNull();
using var verify = UnsTreeTestDb.CreateNamed(dbName);
verify.Equipment.Single(e => e.EquipmentId == equipmentId).DriverInstanceId.ShouldBe("DRV-1");
}
// ----- DeleteEquipment -----
/// <summary>Deleting equipment removes the row.</summary>