using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.LmxOpcUa.Host.Domain; using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; namespace ZB.MOM.WW.LmxOpcUa.Tests.Integration { public class IncrementalSyncTests { /// /// Verifies that adding a new Galaxy object and attribute causes the corresponding OPC UA node subtree to appear after sync. /// [Fact] public async Task Sync_AddObject_NewNodeAppears() { var fixture = OpcUaServerFixture.WithFakeMxAccessClient(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Verify initial state var children = await client.BrowseAsync(client.MakeNodeId("TestArea")); children.Select(c => c.Name).ShouldContain("TestMachine_001"); children.Select(c => c.Name).ShouldNotContain("NewObject"); // Add a new object fixture.GalaxyRepository!.Hierarchy.Add(new GalaxyObjectInfo { GobjectId = 100, TagName = "NewObject_001", ContainedName = "NewObject", BrowseName = "NewObject", ParentGobjectId = 2, IsArea = false }); fixture.GalaxyRepository.Attributes.Add(new GalaxyAttributeInfo { GobjectId = 100, TagName = "NewObject_001", AttributeName = "Status", FullTagReference = "NewObject_001.Status", MxDataType = 5 }); fixture.Service.TriggerRebuild(); await Task.Delay(1000); // Reconnect in case session was disrupted during rebuild using var client2 = new OpcUaTestClient(); await client2.ConnectAsync(fixture.EndpointUrl); // New object should appear when browsing parent children = await client2.BrowseAsync(client2.MakeNodeId("TestArea")); children.Select(c => c.Name).ShouldContain("NewObject", $"Browse returned: [{string.Join(", ", children.Select(c => c.Name))}]"); // Original object should still be there children.Select(c => c.Name).ShouldContain("TestMachine_001"); } finally { await fixture.DisposeAsync(); } } /// /// Verifies that removing a Galaxy object tears down the corresponding OPC UA subtree without affecting siblings. /// [Fact] public async Task Sync_RemoveObject_NodeDisappears() { var fixture = OpcUaServerFixture.WithFakeMxAccessClient(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Verify MESReceiver exists var children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001")); children.Select(c => c.Name).ShouldContain("MESReceiver"); // Remove MESReceiver (gobject_id 5) and its attributes fixture.GalaxyRepository!.Hierarchy.RemoveAll(h => h.GobjectId == 5); fixture.GalaxyRepository.Attributes.RemoveAll(a => a.GobjectId == 5); fixture.Service.TriggerRebuild(); await Task.Delay(500); // MESReceiver should be gone children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001")); children.Select(c => c.Name).ShouldNotContain("MESReceiver"); // DelmiaReceiver should still be there children.Select(c => c.Name).ShouldContain("DelmiaReceiver"); } finally { await fixture.DisposeAsync(); } } /// /// Verifies that adding a Galaxy attribute creates a new OPC UA variable during incremental rebuild. /// [Fact] public async Task Sync_AddAttribute_NewVariableAppears() { var fixture = OpcUaServerFixture.WithFakeMxAccessClient(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Add a new attribute to TestMachine_001 fixture.GalaxyRepository!.Attributes.Add(new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "NewAttr", FullTagReference = "TestMachine_001.NewAttr", MxDataType = 5 }); fixture.Service.TriggerRebuild(); await Task.Delay(500); var children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001")); children.Select(c => c.Name).ShouldContain("NewAttr"); children.Select(c => c.Name).ShouldContain("MachineID"); } finally { await fixture.DisposeAsync(); } } /// /// Verifies that subscriptions on unchanged objects continue receiving data after unrelated subtree rebuilds. /// [Fact] public async Task Sync_UnchangedObject_SubscriptionSurvives() { var mxClient = new FakeMxAccessClient(); var fixture = OpcUaServerFixture.WithFakeMxAccessClient(mxClient: mxClient); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Subscribe to MachineID on TestMachine_001 var nodeId = client.MakeNodeId("TestMachine_001.MachineID"); var (sub, item) = await client.SubscribeAsync(nodeId, 250); await Task.Delay(500); // Modify a DIFFERENT object (MESReceiver) — TestMachine_001 should be unaffected var mesAttr = fixture.GalaxyRepository!.Attributes .First(a => a.GobjectId == 5 && a.AttributeName == "MoveInBatchID"); mesAttr.SecurityClassification = 2; // change something fixture.Service.TriggerRebuild(); await Task.Delay(500); // Push a value change through MXAccess — subscription should still deliver mxClient.SimulateDataChange("TestMachine_001.MachineID", Vtq.Good("UPDATED")); await Task.Delay(1000); var lastValue = (item.LastValue as MonitoredItemNotification)?.Value?.Value; lastValue.ShouldBe("UPDATED"); } finally { await fixture.DisposeAsync(); } } /// /// Verifies that a rebuild request with no repository changes leaves the published namespace intact. /// [Fact] public async Task Sync_NoChanges_NothingHappens() { var fixture = OpcUaServerFixture.WithFakeMxAccessClient(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Trigger rebuild with no changes fixture.Service.TriggerRebuild(); await Task.Delay(500); // Everything should still work var children = await client.BrowseAsync(client.MakeNodeId("TestMachine_001")); children.Select(c => c.Name).ShouldContain("MachineID"); } finally { await fixture.DisposeAsync(); } } } }