using System.Threading; using System.Threading.Tasks; using Xunit; using ZB.MOM.WW.LmxOpcUa.Host; using ZB.MOM.WW.LmxOpcUa.Host.Configuration; 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; /// /// Gets the started service instance managed by the fixture. /// public OpcUaService Service { get; private set; } = null!; /// /// Gets the OPC UA port assigned to this fixture instance. /// public int OpcUaPort { get; } /// /// Gets the OPC UA endpoint URL exposed by the fixture. /// 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; /// /// Initializes a fixture around a prepared service builder and optional fake dependencies. /// /// The builder used to construct the service under test. /// The optional fake Galaxy repository exposed to tests. /// The optional fake MXAccess client exposed to tests. /// The optional fake MXAccess proxy exposed to tests. 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. /// /// An optional fake proxy to inject; otherwise a default fake is created. /// An optional fake repository to inject; otherwise standard test data is used. /// A fixture configured to exercise the COM-style runtime path. 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. /// /// An optional fake MXAccess client to inject; otherwise a default fake is created. /// An optional fake repository to inject; otherwise standard test data is used. /// An optional security profile configuration for the test server. /// An optional redundancy configuration for the test server. /// An optional explicit application URI for the test server. /// An optional server name override for the test server. /// A fixture configured to exercise the direct fake-client path. public static OpcUaServerFixture WithFakeMxAccessClient( FakeMxAccessClient? mxClient = null, FakeGalaxyRepository? repo = null, SecurityProfileConfiguration? security = null, RedundancyConfiguration? redundancy = null, string? applicationUri = null, string? serverName = 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"); if (security != null) builder.WithSecurity(security); if (redundancy != null) builder.WithRedundancy(redundancy); if (applicationUri != null) builder.WithApplicationUri(applicationUri); if (serverName != null) builder.WithGalaxyName(serverName); return new OpcUaServerFixture(builder, repo: r, mxClient: client); } /// /// Builds and starts the OPC UA service for the current fixture. /// public Task InitializeAsync() { Service = _builder.Build(); Service.Start(); _started = true; return Task.CompletedTask; } /// /// Stops the OPC UA service when the fixture had previously been started. /// public Task DisposeAsync() { if (_started) { try { Service.Stop(); } catch { /* swallow cleanup errors */ } } return Task.CompletedTask; } } }