using System.Threading; using System.Threading.Tasks; using Xunit; using ZB.MOM.WW.LmxOpcUa.Host; namespace ZB.MOM.WW.LmxOpcUa.Tests.Helpers { /// /// xUnit fixture that manages an OpcUaService lifecycle with automatic port allocation. /// Guarantees no port conflicts between parallel tests. /// /// Usage (per-test): /// var fixture = OpcUaServerFixture.WithFakes(); /// await fixture.InitializeAsync(); /// try { ... } finally { await fixture.DisposeAsync(); } /// /// Usage (skip COM entirely): /// var fixture = OpcUaServerFixture.WithFakeMxAccessClient(); /// internal class OpcUaServerFixture : IAsyncLifetime { private static int _nextPort = 16000; public OpcUaService Service { get; private set; } = null!; public int OpcUaPort { get; } public string EndpointUrl => $"opc.tcp://localhost:{OpcUaPort}/LmxOpcUa"; /// /// The fake Galaxy repository injected into the service. Mutate Hierarchy/Attributes /// then call Service.TriggerRebuild() to simulate a Galaxy redeployment. /// public FakeGalaxyRepository? GalaxyRepository { get; } /// /// The fake MxAccess client injected into the service (when using WithFakeMxAccessClient). /// public FakeMxAccessClient? MxAccessClient { get; } /// /// The fake MxProxy injected into the service (when using WithFakes). /// public FakeMxProxy? MxProxy { get; } private readonly OpcUaServiceBuilder _builder; private bool _started; private OpcUaServerFixture(OpcUaServiceBuilder builder, FakeGalaxyRepository? repo = null, FakeMxAccessClient? mxClient = null, FakeMxProxy? mxProxy = null) { OpcUaPort = Interlocked.Increment(ref _nextPort); _builder = builder; _builder.WithOpcUaPort(OpcUaPort); _builder.DisableDashboard(); GalaxyRepository = repo; MxAccessClient = mxClient; MxProxy = mxProxy; } /// /// Creates fixture with FakeMxProxy + FakeGalaxyRepository (standard test data). /// The STA thread and COM interop run against FakeMxProxy. /// public static OpcUaServerFixture WithFakes( FakeMxProxy? proxy = null, FakeGalaxyRepository? repo = null) { var p = proxy ?? new FakeMxProxy(); var r = repo ?? new FakeGalaxyRepository { Hierarchy = TestData.CreateStandardHierarchy(), Attributes = TestData.CreateStandardAttributes() }; var builder = new OpcUaServiceBuilder() .WithMxProxy(p) .WithGalaxyRepository(r) .WithGalaxyName("TestGalaxy"); return new OpcUaServerFixture(builder, repo: r, mxProxy: p); } /// /// Creates fixture using FakeMxAccessClient directly — skips STA thread + COM entirely. /// Fastest option for tests that don't need real COM interop. /// public static OpcUaServerFixture WithFakeMxAccessClient( FakeMxAccessClient? mxClient = null, FakeGalaxyRepository? repo = null) { var client = mxClient ?? new FakeMxAccessClient(); var r = repo ?? new FakeGalaxyRepository { Hierarchy = TestData.CreateStandardHierarchy(), Attributes = TestData.CreateStandardAttributes() }; var builder = new OpcUaServiceBuilder() .WithMxAccessClient(client) .WithGalaxyRepository(r) .WithGalaxyName("TestGalaxy"); return new OpcUaServerFixture(builder, repo: r, mxClient: client); } public Task InitializeAsync() { Service = _builder.Build(); Service.Start(); _started = true; return Task.CompletedTask; } public Task DisposeAsync() { if (_started) { try { Service.Stop(); } catch { /* swallow cleanup errors */ } } return Task.CompletedTask; } } }