using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Configuration.Validation; namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests; /// /// Verifies materialises a /// from the live config DB whose Tag/VirtualTag rows feed the /// equipment-signal collision rule — the one rule wired into the deploy gate (Task 3). /// [Trait("Category", "Unit")] public sealed class DraftSnapshotFactoryTests : IDisposable { private readonly OtOpcUaConfigDbContext _db; /// Initializes a new instance with an isolated in-memory config DB. public DraftSnapshotFactoryTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"draft-snapshot-{Guid.NewGuid():N}") .Options; _db = new OtOpcUaConfigDbContext(options); } /// Disposes the database context. public void Dispose() => _db.Dispose(); /// Seeds one Equipment plus a Tag and a VirtualTag sharing (EquipmentId, Name); the /// snapshot must carry both signal collections AND the validator must flag the collision. [Fact] public async Task FromConfigDb_populates_Tags_and_VirtualTags_and_surfaces_collision() { SeedEquipment("eq-1"); _db.Tags.Add(BuildTag(equipmentId: "eq-1", name: "speed")); _db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "speed")); await _db.SaveChangesAsync(); var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db); snapshot.Tags.Count.ShouldBe(1); snapshot.VirtualTags.Count.ShouldBe(1); DraftValidator.Validate(snapshot).ShouldContain(e => e.Code == "EquipmentSignalNameCollision"); } /// A Tag and a VirtualTag with distinct names under the same equipment do not collide, /// so the snapshot validates clean of the collision code. [Fact] public async Task FromConfigDb_no_collision_when_names_differ() { SeedEquipment("eq-1"); _db.Tags.Add(BuildTag(equipmentId: "eq-1", name: "speed")); _db.VirtualTags.Add(BuildVirtualTag(equipmentId: "eq-1", name: "temperature")); await _db.SaveChangesAsync(); var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db); snapshot.Tags.Count.ShouldBe(1); snapshot.VirtualTags.Count.ShouldBe(1); DraftValidator.Validate(snapshot).ShouldNotContain(e => e.Code == "EquipmentSignalNameCollision"); } /// Seeds one active and one released reservation for the same equipment context; /// only the active row (ReleasedAt == null) should appear in the snapshot. [Fact] public async Task FromConfigDb_ActiveReservations_excludes_released_rows() { var equipmentUuid = Guid.NewGuid(); _db.ExternalIdReservations.Add(new ExternalIdReservation { ReservationId = Guid.NewGuid(), Kind = ReservationKind.ZTag, Value = "ZT-001", EquipmentUuid = equipmentUuid, ClusterId = "cluster-a", FirstPublishedBy = "test", ReleasedAt = null, // active }); _db.ExternalIdReservations.Add(new ExternalIdReservation { ReservationId = Guid.NewGuid(), Kind = ReservationKind.ZTag, Value = "ZT-002", EquipmentUuid = equipmentUuid, ClusterId = "cluster-a", FirstPublishedBy = "test", ReleasedAt = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc), // released ReleasedBy = "test", ReleaseReason = "retired", }); await _db.SaveChangesAsync(); var snapshot = await DraftSnapshotFactory.FromConfigDbAsync(_db); snapshot.ActiveReservations.Count.ShouldBe(1); snapshot.ActiveReservations[0].Value.ShouldBe("ZT-001"); snapshot.ActiveReservations[0].ReleasedAt.ShouldBeNull(); } private void SeedEquipment(string equipmentId) { var uuid = Guid.NewGuid(); _db.Equipment.Add(new Equipment { EquipmentUuid = uuid, EquipmentId = equipmentId, Name = "eq", DriverInstanceId = "d", UnsLineId = "line-a", MachineCode = "m", }); } private static Tag BuildTag(string equipmentId, string name) => new() { TagId = $"tag-{name}", DriverInstanceId = "d", EquipmentId = equipmentId, Name = name, DataType = "Float", AccessLevel = TagAccessLevel.Read, TagConfig = "{}", }; private static VirtualTag BuildVirtualTag(string equipmentId, string name) => new() { VirtualTagId = $"vtag-{name}", EquipmentId = equipmentId, Name = name, DataType = "Float", ScriptId = "s-1", }; }