using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests.LiveStack; /// /// End-to-end smoke against the installed OtOpcUaGalaxyHost Windows service. /// Closes LMX follow-up #5 — exercises the full topology: /// in-process → named-pipe IPC → OtOpcUaGalaxyHost service → MxAccessGalaxyBackend → /// live MXAccess runtime → real Galaxy objects + attributes. /// /// /// /// Preconditions (all checked by , surfaced via /// Assert.Skip when missing): /// /// /// AVEVA System Platform installed + Platform deployed. /// aaBootstrap / aaGR / NmxSvc / MSSQLSERVER running. /// MXAccess COM server registered. /// ZB database exists with at least one deployed gobject. /// OtOpcUaGalaxyHost service installed + running (named pipe accepting connections). /// Shared secret discoverable via OTOPCUA_GALAXY_SECRET env var or the /// service's registry Environment values (test host typically needs to be elevated /// to read the latter). /// Test process runs as the account listed in the service's pipe ACL /// (OTOPCUA_ALLOWED_SID, typically the service account per decision #76). /// /// /// Tests here are deliberately read-only. Writes against live Galaxy attributes are a /// separate concern — they need a test-only UDA or an agreed scratch tag so they can't /// accidentally mutate a process-critical value. Adding a write test is a follow-up /// PR that reuses this fixture. /// /// [Trait("Category", "LiveGalaxy")] [Collection(LiveStackCollection.Name)] public sealed class LiveStackSmokeTests(LiveStackFixture fixture) { [Fact] public void Fixture_initialized_successfully() { fixture.SkipIfUnavailable(); // If the fixture init succeeded, Driver is non-null and InitializeAsync completed. // This is the cheapest possible assertion that the IPC handshake worked end-to-end; // every other test in this class depends on it. fixture.Driver.ShouldNotBeNull(); fixture.Config.ShouldNotBeNull(); fixture.PrerequisiteReport.ShouldNotBeNull(); fixture.PrerequisiteReport!.IsLivetestReady.ShouldBeTrue(fixture.PrerequisiteReport.SkipReason); } [Fact] public void Driver_reports_Healthy_after_IPC_handshake() { fixture.SkipIfUnavailable(); var health = fixture.Driver!.GetHealth(); health.State.ShouldBe(DriverState.Healthy, $"Expected Healthy after successful IPC connect; Reason={health.LastError}"); } [Fact] public async Task DiscoverAsync_returns_at_least_one_variable_from_live_galaxy() { fixture.SkipIfUnavailable(); var builder = new CapturingAddressSpaceBuilder(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await fixture.Driver!.DiscoverAsync(builder, cts.Token); builder.Variables.Count.ShouldBeGreaterThan(0, "Live Galaxy has > 0 deployed objects per the prereq check — at least one variable must be discovered. " + "Zero usually means the Host couldn't read ZB (check OTOPCUA_GALAXY_ZB_CONN in the service Environment)."); // Every discovered attribute must carry a non-empty FullName so the OPC UA server can // route reads/writes back. Regression guard — PR 19 normalized this across drivers. builder.Variables.ShouldAllBe(v => !string.IsNullOrEmpty(v.AttributeInfo.FullName)); } [Fact] public void GetHostStatuses_reports_at_least_one_platform() { fixture.SkipIfUnavailable(); var statuses = fixture.Driver!.GetHostStatuses(); statuses.Count.ShouldBeGreaterThan(0, "Live Galaxy must report at least one Platform/AppEngine host via IHostConnectivityProbe. " + "Zero means the Host's probe loop hasn't completed its first tick or the Platform isn't deployed locally."); // Host names are driver-opaque to the Core but non-empty by contract. statuses.ShouldAllBe(h => !string.IsNullOrEmpty(h.HostName)); } [Fact] public async Task Can_read_a_discovered_variable_from_live_galaxy() { fixture.SkipIfUnavailable(); var builder = new CapturingAddressSpaceBuilder(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); await fixture.Driver!.DiscoverAsync(builder, cts.Token); builder.Variables.Count.ShouldBeGreaterThan(0); // Pick the first discovered variable. Read-only smoke — we don't assert on Value, // only that a ReadAsync round-trip through Proxy → Host pipe → MXAccess → back // returns a snapshot with a non-BadInternalError status. Galaxy attributes default to // Uncertain quality until the Engine's first scan publishes them, which is fine here. var full = builder.Variables[0].AttributeInfo.FullName; var snapshots = await fixture.Driver!.ReadAsync([full], cts.Token); snapshots.Count.ShouldBe(1); var snap = snapshots[0]; snap.StatusCode.ShouldNotBe(0x80020000u, $"Read returned BadInternalError for {full} — the Host couldn't fulfil the request. " + $"Investigate: the Host service's logs at {System.Environment.GetFolderPath(System.Environment.SpecialFolder.CommonApplicationData)}\\OtOpcUa\\Galaxy\\logs."); } /// /// Minimal implementation that captures every /// Variable() call into a flat list so tests can inspect what discovery produced /// without running the full OPC UA node-manager stack. /// private sealed class CapturingAddressSpaceBuilder : IAddressSpaceBuilder { public List<(string BrowseName, DriverAttributeInfo AttributeInfo)> Variables { get; } = []; public IAddressSpaceBuilder Folder(string browseName, string displayName) => this; public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo) { Variables.Add((browseName, attributeInfo)); return new NoopHandle(attributeInfo.FullName); } public void AddProperty(string browseName, DriverDataType dataType, object? value) { } private sealed class NoopHandle(string fullReference) : IVariableHandle { public string FullReference { get; } = fullReference; public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NoopSink(); private sealed class NoopSink : IAlarmConditionSink { public void OnTransition(AlarmEventArgs args) { } } } } }