using Microsoft.EntityFrameworkCore; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Server.OpcUa; namespace ZB.MOM.WW.OtOpcUa.Server.Tests; [Trait("Category", "Unit")] public sealed class EquipmentNamespaceContentLoaderTests : IDisposable { private const string DriverId = "galaxy-prod"; private const string OtherDriverId = "galaxy-dev"; private const long Gen = 5; private readonly OtOpcUaConfigDbContext _db; private readonly EquipmentNamespaceContentLoader _loader; public EquipmentNamespaceContentLoaderTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"eq-content-loader-{Guid.NewGuid():N}") .Options; _db = new OtOpcUaConfigDbContext(options); _loader = new EquipmentNamespaceContentLoader(_db); } public void Dispose() => _db.Dispose(); [Fact] public async Task Returns_Null_When_Driver_Has_No_Equipment_At_Generation() { var result = await _loader.LoadAsync(DriverId, Gen, CancellationToken.None); result.ShouldBeNull(); } [Fact] public async Task Loads_Areas_Lines_Equipment_Tags_For_Driver_At_Generation() { SeedBaseline(); var result = await _loader.LoadAsync(DriverId, Gen, CancellationToken.None); result.ShouldNotBeNull(); result!.Areas.ShouldHaveSingleItem().UnsAreaId.ShouldBe("area-1"); result.Lines.ShouldHaveSingleItem().UnsLineId.ShouldBe("line-a"); result.Equipment.Count.ShouldBe(2); result.Equipment.ShouldContain(e => e.EquipmentId == "eq-oven-3"); result.Equipment.ShouldContain(e => e.EquipmentId == "eq-press-7"); result.Tags.Count.ShouldBe(2); result.Tags.ShouldContain(t => t.TagId == "tag-temp"); result.Tags.ShouldContain(t => t.TagId == "tag-press"); } [Fact] public async Task Skips_Other_Drivers_Equipment() { SeedBaseline(); // Equipment + Tag owned by a different driver at the same generation — must not leak. _db.Equipment.Add(new Equipment { EquipmentRowId = Guid.NewGuid(), GenerationId = Gen, EquipmentId = "eq-other", EquipmentUuid = Guid.NewGuid(), DriverInstanceId = OtherDriverId, UnsLineId = "line-a", Name = "other-eq", MachineCode = "MC-other", }); _db.Tags.Add(new Tag { TagRowId = Guid.NewGuid(), GenerationId = Gen, TagId = "tag-other", DriverInstanceId = OtherDriverId, EquipmentId = "eq-other", Name = "OtherTag", DataType = "Int32", AccessLevel = TagAccessLevel.ReadWrite, TagConfig = "plcaddr-other", }); await _db.SaveChangesAsync(); var result = await _loader.LoadAsync(DriverId, Gen, CancellationToken.None); result.ShouldNotBeNull(); result!.Equipment.ShouldNotContain(e => e.EquipmentId == "eq-other"); result.Tags.ShouldNotContain(t => t.TagId == "tag-other"); } [Fact] public async Task Skips_Other_Generations() { SeedBaseline(); // Same driver, different generation — must not leak in. Walker consumes one sealed // generation per bootstrap per decision #148. _db.Equipment.Add(new Equipment { EquipmentRowId = Guid.NewGuid(), GenerationId = 99, EquipmentId = "eq-futuristic", EquipmentUuid = Guid.NewGuid(), DriverInstanceId = DriverId, UnsLineId = "line-a", Name = "futuristic", MachineCode = "MC-fut", }); await _db.SaveChangesAsync(); var result = await _loader.LoadAsync(DriverId, Gen, CancellationToken.None); result.ShouldNotBeNull(); result!.Equipment.ShouldNotContain(e => e.EquipmentId == "eq-futuristic"); } [Fact] public async Task Skips_Disabled_Equipment() { SeedBaseline(); _db.Equipment.Add(new Equipment { EquipmentRowId = Guid.NewGuid(), GenerationId = Gen, EquipmentId = "eq-disabled", EquipmentUuid = Guid.NewGuid(), DriverInstanceId = DriverId, UnsLineId = "line-a", Name = "disabled-eq", MachineCode = "MC-dis", Enabled = false, }); await _db.SaveChangesAsync(); var result = await _loader.LoadAsync(DriverId, Gen, CancellationToken.None); result.ShouldNotBeNull(); result!.Equipment.ShouldNotContain(e => e.EquipmentId == "eq-disabled"); } private void SeedBaseline() { _db.UnsAreas.Add(new UnsArea { UnsAreaRowId = Guid.NewGuid(), UnsAreaId = "area-1", ClusterId = "c-warsaw", Name = "warsaw", GenerationId = Gen, }); _db.UnsLines.Add(new UnsLine { UnsLineRowId = Guid.NewGuid(), UnsLineId = "line-a", UnsAreaId = "area-1", Name = "line-a", GenerationId = Gen, }); _db.Equipment.AddRange( new Equipment { EquipmentRowId = Guid.NewGuid(), GenerationId = Gen, EquipmentId = "eq-oven-3", EquipmentUuid = Guid.NewGuid(), DriverInstanceId = DriverId, UnsLineId = "line-a", Name = "oven-3", MachineCode = "MC-oven-3", }, new Equipment { EquipmentRowId = Guid.NewGuid(), GenerationId = Gen, EquipmentId = "eq-press-7", EquipmentUuid = Guid.NewGuid(), DriverInstanceId = DriverId, UnsLineId = "line-a", Name = "press-7", MachineCode = "MC-press-7", }); _db.Tags.AddRange( new Tag { TagRowId = Guid.NewGuid(), GenerationId = Gen, TagId = "tag-temp", DriverInstanceId = DriverId, EquipmentId = "eq-oven-3", Name = "Temperature", DataType = "Int32", AccessLevel = TagAccessLevel.ReadWrite, TagConfig = "plcaddr-temperature", }, new Tag { TagRowId = Guid.NewGuid(), GenerationId = Gen, TagId = "tag-press", DriverInstanceId = DriverId, EquipmentId = "eq-press-7", Name = "PressTemp", DataType = "Int32", AccessLevel = TagAccessLevel.ReadWrite, TagConfig = "plcaddr-press-temp", }); _db.SaveChanges(); } }