using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Opc.Ua; using Opc.Ua.Client; 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 { /// /// Integration tests verifying dynamic address space changes via a real OPC UA client. /// Tests browse, subscribe, add/remove nodes at runtime, and subscription quality changes. /// public class AddressSpaceRebuildTests { [Fact] public async Task Browse_ReturnsInitialHierarchy() { var fixture = OpcUaServerFixture.WithFakes(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Browse from ZB root var zbNode = client.MakeNodeId("ZB"); var children = await client.BrowseAsync(zbNode); children.ShouldContain(c => c.Name == "DEV"); } finally { await fixture.DisposeAsync(); } } [Fact] public async Task Browse_AfterAddingObject_NewNodeAppears() { var fixture = OpcUaServerFixture.WithFakes(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Verify initial state — browse TestMachine_001 var machineNode = client.MakeNodeId("TestMachine_001"); var initialChildren = await client.BrowseAsync(machineNode); initialChildren.ShouldNotContain(c => c.Name == "NewReceiver"); // Add a new object to the hierarchy fixture.GalaxyRepository!.Hierarchy.Add(new GalaxyObjectInfo { GobjectId = 100, TagName = "NewReceiver_001", ContainedName = "NewReceiver", BrowseName = "NewReceiver", ParentGobjectId = 3, IsArea = false // parent = TestMachine_001 }); fixture.GalaxyRepository.Attributes.Add(new GalaxyAttributeInfo { GobjectId = 100, TagName = "NewReceiver_001", AttributeName = "NewAttr", FullTagReference = "NewReceiver_001.NewAttr", MxDataType = 5, IsArray = false }); // Trigger rebuild fixture.Service.TriggerRebuild(); await Task.Delay(500); // allow rebuild to complete // Browse again — new node should appear var updatedChildren = await client.BrowseAsync(machineNode); updatedChildren.ShouldContain(c => c.Name == "NewReceiver"); } finally { await fixture.DisposeAsync(); } } [Fact] public async Task Browse_AfterRemovingObject_NodeDisappears() { var fixture = OpcUaServerFixture.WithFakes(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Verify MESReceiver exists initially var machineNode = client.MakeNodeId("TestMachine_001"); var initialChildren = await client.BrowseAsync(machineNode); initialChildren.ShouldContain(c => c.Name == "MESReceiver"); // Remove MESReceiver and its attributes from hierarchy fixture.GalaxyRepository!.Hierarchy.RemoveAll(h => h.TagName == "MESReceiver_001"); fixture.GalaxyRepository.Attributes.RemoveAll(a => a.TagName == "MESReceiver_001"); // Trigger rebuild fixture.Service.TriggerRebuild(); await Task.Delay(500); // Browse again — MESReceiver should be gone var updatedChildren = await client.BrowseAsync(machineNode); updatedChildren.ShouldNotContain(c => c.Name == "MESReceiver"); } finally { await fixture.DisposeAsync(); } } [Fact] public async Task Subscribe_RemovedNode_PublishesBadQuality() { var fixture = OpcUaServerFixture.WithFakes(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Subscribe to an attribute that will be removed var nodeId = client.MakeNodeId("MESReceiver_001.MoveInBatchID"); var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100); // Collect notifications var notifications = new List(); item.Notification += (_, e) => { if (e.NotificationValue is MonitoredItemNotification n) notifications.Add(n); }; await Task.Delay(500); // let initial subscription settle // Remove MESReceiver and its attributes fixture.GalaxyRepository!.Hierarchy.RemoveAll(h => h.TagName == "MESReceiver_001"); fixture.GalaxyRepository.Attributes.RemoveAll(a => a.TagName == "MESReceiver_001"); // Trigger rebuild — nodes get deleted fixture.Service.TriggerRebuild(); // Wait for publish cycle to deliver Bad status await Task.Delay(2000); // The subscription should have received a Bad quality notification // after the node was deleted during rebuild notifications.ShouldContain(n => StatusCode.IsBad(n.Value.StatusCode)); await sub.DeleteAsync(true); } finally { await fixture.DisposeAsync(); } } [Fact] public async Task Subscribe_SurvivingNode_StillWorksAfterRebuild() { var fixture = OpcUaServerFixture.WithFakes(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); // Subscribe to an attribute that will survive the rebuild var nodeId = client.MakeNodeId("TestMachine_001.MachineID"); var (sub, item) = await client.SubscribeAsync(nodeId, intervalMs: 100); await Task.Delay(500); // Remove only MESReceiver (MachineID on TestMachine_001 survives) fixture.GalaxyRepository!.Hierarchy.RemoveAll(h => h.TagName == "MESReceiver_001"); fixture.GalaxyRepository.Attributes.RemoveAll(a => a.TagName == "MESReceiver_001"); fixture.Service.TriggerRebuild(); await Task.Delay(1000); // The surviving node should still be browsable var machineNode = client.MakeNodeId("TestMachine_001"); var children = await client.BrowseAsync(machineNode); children.ShouldContain(c => c.Name == "MachineID"); await sub.DeleteAsync(true); } finally { await fixture.DisposeAsync(); } } [Fact] public async Task Browse_AddAttribute_NewVariableAppears() { var fixture = OpcUaServerFixture.WithFakes(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); var machineNode = client.MakeNodeId("TestMachine_001"); var initialChildren = await client.BrowseAsync(machineNode); initialChildren.ShouldNotContain(c => c.Name == "NewSensor"); // Add a new attribute fixture.GalaxyRepository!.Attributes.Add(new GalaxyAttributeInfo { GobjectId = 3, TagName = "TestMachine_001", AttributeName = "NewSensor", FullTagReference = "TestMachine_001.NewSensor", MxDataType = 4, IsArray = false // Double }); fixture.Service.TriggerRebuild(); await Task.Delay(500); var updatedChildren = await client.BrowseAsync(machineNode); updatedChildren.ShouldContain(c => c.Name == "NewSensor"); } finally { await fixture.DisposeAsync(); } } [Fact] public async Task Browse_RemoveAttribute_VariableDisappears() { var fixture = OpcUaServerFixture.WithFakes(); await fixture.InitializeAsync(); try { using var client = new OpcUaTestClient(); await client.ConnectAsync(fixture.EndpointUrl); var machineNode = client.MakeNodeId("TestMachine_001"); var initialChildren = await client.BrowseAsync(machineNode); initialChildren.ShouldContain(c => c.Name == "MachineCode"); // Remove MachineCode attribute fixture.GalaxyRepository!.Attributes.RemoveAll( a => a.TagName == "TestMachine_001" && a.AttributeName == "MachineCode"); fixture.Service.TriggerRebuild(); await Task.Delay(500); var updatedChildren = await client.BrowseAsync(machineNode); updatedChildren.ShouldNotContain(c => c.Name == "MachineCode"); } finally { await fixture.DisposeAsync(); } } } }