diff --git a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackFixture.cs b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackFixture.cs index 7199c22..2915811 100644 --- a/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackFixture.cs +++ b/tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests/LiveStack/LiveStackFixture.cs @@ -1,3 +1,6 @@ +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -40,6 +43,25 @@ public sealed class LiveStackFixture : IAsyncLifetime public async ValueTask InitializeAsync() { + // 0. Elevated-shell short-circuit. The OtOpcUaGalaxyHost pipe ACL allows the configured + // SID but explicitly DENIES Administrators (decision #76 — production hardening). + // A test process running with a high-integrity token (any elevated shell) carries the + // Admins group in its security context, so the deny rule trumps the user's allow and + // the pipe connect returns UnauthorizedAccessException — technically correct but + // the operationally confusing failure mode that ate most of the PR 37 install + // debugging session. Surfacing it explicitly here saves the next operator the same + // five-step diagnosis. ParityFixture has the same skip with the same rationale. + if (IsElevatedAdministratorOnWindows()) + { + SkipReason = + "Test host is running with elevated (Administrators) privileges, but the " + + "OtOpcUaGalaxyHost named-pipe ACL explicitly denies Administrators per the IPC " + + "security design (decision #76 / PipeAcl.cs). Re-run from a NORMAL (non-admin) " + + "PowerShell window — even when your user is already in the pipe's allow list, " + + "the elevated token's Admins group membership trumps the allow rule."; + return; + } + // 1. AVEVA + OtOpcUa service state — actionable diagnostic if anything is missing. using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); PrerequisiteReport = await AvevaPrerequisites.CheckAllAsync( @@ -111,6 +133,28 @@ public sealed class LiveStackFixture : IAsyncLifetime { if (SkipReason is not null) Assert.Skip(SkipReason); } + + private static bool IsElevatedAdministratorOnWindows() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false; + return CheckWindowsAdminToken(); + } + + [SupportedOSPlatform("windows")] + private static bool CheckWindowsAdminToken() + { + try + { + using var identity = WindowsIdentity.GetCurrent(); + return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); + } + catch + { + // Probe shouldn't crash the test; if we can't determine elevation, optimistically + // continue and let the actual pipe connect surface its own error. + return false; + } + } } [CollectionDefinition(Name)]