using System.Collections.Concurrent; using System.Net; using System.Text.Json; using MBN_STOCK_WEBVIEW.Core.Playout; using MBN_STOCK_WEBVIEW.Playout.Diagnostics; using MBN_STOCK_WEBVIEW.Playout.Interop; namespace MBN_STOCK_WEBVIEW.Playout.Tests; public sealed class PgmConnectDiagnosticTests { [Fact] public void RealContactDiagnosticApi_IsNotPublic() { Assert.False(typeof(PgmConnectDiagnosticRunner).IsVisible); Assert.False(typeof(PgmConnectDiagnosticRequest).IsVisible); Assert.False(typeof(PgmConnectDiagnosticReport).IsVisible); Assert.Null(typeof(PgmConnectDiagnosticRunner).GetMethod( nameof(PgmConnectDiagnosticRunner.ExecuteAsync), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)); } [Theory] [InlineData("localhost", 30001, "PGM")] [InlineData("192.0.2.1", 30001, "PGM")] [InlineData("127.0.0.1", 0, "PGM")] [InlineData("127.0.0.1", 30001, "TEST")] [InlineData("127.0.0.1", 30001, "CONPROGRAM")] public void RequestValidation_RejectsUnsafeEndpointOrWindow( string host, int port, string title) { var valid = PgmConnectDiagnosticRunner.TryValidateRequest( new PgmConnectDiagnosticRequest(host, port, title), out _); Assert.False(valid); } [Fact] public async Task Runner_SuccessUsesOnlyConnectThenDisconnectAndReportsUnconfirmedEvidence() { var snapshot = ReadySnapshot(10); var safety = new FakeSafetyProbe(snapshot, snapshot, snapshot, snapshot); var session = new FakeConnectOnlySession(); var factory = new FakeConnectOnlySessionFactory(session); var runner = new PgmConnectDiagnosticRunner( safety, factory, new FakeRegistrationProbe(), new TrackingStaDispatcherFactory()); var request = new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM"); var report = await runner.ExecuteAsync(request); Assert.Equal(["Connect", "Disconnect"], session.Calls); Assert.True(report.Completed); Assert.False(report.OutcomeUnknown); Assert.True(report.ConnectRequestIssued); Assert.True(report.ComActivationAttempted); Assert.Equal(PlayoutKtapConnectState.AcceptedUnconfirmed, report.LastKtapConnectState); Assert.True(report.KtapConnectAttempted); Assert.True(report.KtapConnectAccepted); Assert.Null(report.KtapHelloObserved); Assert.True(report.NetworkMonitoringRecordExpected); Assert.True(report.NetworkMonitoringCheckRequired); Assert.Null(report.NetworkMonitoringVerified); Assert.Equal("success", report.DisconnectCode); Assert.False(report.RenderCommandSurfaceExposed); Assert.False(report.RenderCommandAttempted); Assert.False(report.AutomaticReconnectEnabled); var json = JsonSerializer.Serialize(report); Assert.DoesNotContain("127.0.0.1", json, StringComparison.Ordinal); Assert.DoesNotContain("30001", json, StringComparison.Ordinal); Assert.DoesNotContain("PGM", json, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Runner_TargetRejectedStopsBeforeRegistrationAndCom() { var rejected = new PgmDiagnosticTargetSnapshot(false, 0, 0, false, null); var factory = new FakeConnectOnlySessionFactory(new FakeConnectOnlySession()); var registration = new FakeRegistrationProbe(); var runner = new PgmConnectDiagnosticRunner( new FakeSafetyProbe(rejected), factory, registration, new TrackingStaDispatcherFactory()); var report = await runner.ExecuteAsync( new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM")); Assert.False(report.Completed); Assert.False(report.ConnectRequestIssued); Assert.False(report.ComActivationAttempted); Assert.Equal(0, factory.CreateCount); Assert.Equal(0, registration.ProbeCount); Assert.Equal("pgm-target-rejected", report.ErrorCode); } [Fact] public async Task Runner_UnapprovedInstalledInteropIsDistinctFromComActivationFailure() { var snapshot = ReadySnapshot(10); var runner = new PgmConnectDiagnosticRunner( new FakeSafetyProbe(snapshot), new UnavailableInteropSessionFactory(), new FakeRegistrationProbe(), new TrackingStaDispatcherFactory()); var report = await runner.ExecuteAsync( new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM")); Assert.False(report.Completed); Assert.False(report.ComActivationAttempted); Assert.False(report.KtapConnectAttempted); Assert.False(report.OutcomeUnknown); Assert.Equal("not-attempted", report.DisconnectCode); Assert.Equal("installed-interop-metadata-unavailable", report.ErrorCode); } [Fact] public async Task Runner_TargetChangeAfterAcceptanceAbandonsWithoutDisconnect() { var baseline = ReadySnapshot(10); var changed = ReadySnapshot(11); var session = new FakeConnectOnlySession(); var runner = new PgmConnectDiagnosticRunner( new FakeSafetyProbe(baseline, baseline, baseline, changed), new FakeConnectOnlySessionFactory(session), new FakeRegistrationProbe(), new TrackingStaDispatcherFactory()); var report = await runner.ExecuteAsync( new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM")); Assert.Equal(["Connect", "Abandon"], session.Calls); Assert.False(report.Completed); Assert.True(report.OutcomeUnknown); Assert.True(report.KtapConnectAttempted); Assert.DoesNotContain("Disconnect", session.Calls); Assert.Equal("pgm-target-changed-after-connect", report.ErrorCode); } [Fact] public async Task DynamicSession_InvokesOnlyKtapConnectAndDisconnect() { var log = new ConcurrentQueue(); var session = new DynamicKtapConnectOnlySession(new RecordingComBinding(log)); await using var dispatcher = new Runtime.StaDispatcher(1); await dispatcher.InvokeAsync( () => { session.Connect("127.0.0.1", 30001, static () => true); session.Disconnect(); return true; }, TimeSpan.FromSeconds(2), CancellationToken.None); Assert.Equal( [ "CreateEventHandler", "CreateEngine", "KTAPConnect", "Disconnect", "Release", "Release" ], log); Assert.Equal(PlayoutKtapConnectState.AcceptedUnconfirmed, session.LastKtapConnectState); } [Fact] public async Task DynamicSession_FinalSafetyCheckRunsAfterActivationAndBeforeKtap() { var log = new ConcurrentQueue(); var session = new DynamicKtapConnectOnlySession(new RecordingComBinding(log)); await using var dispatcher = new Runtime.StaDispatcher(1); await Assert.ThrowsAsync(() => dispatcher.InvokeAsync( () => { session.Connect( "127.0.0.1", 30001, () => { log.Enqueue("FinalSafetyCheck"); return false; }); return true; }, TimeSpan.FromSeconds(2), CancellationToken.None)); Assert.Equal( [ "CreateEventHandler", "CreateEngine", "FinalSafetyCheck", "Release", "Release" ], log); Assert.DoesNotContain("KTAPConnect", log); Assert.Equal(PlayoutKtapConnectState.NotAttempted, session.LastKtapConnectState); } [Fact] public async Task Runner_ConnectTimeoutPreventsLateKtapDispatch() { var snapshot = ReadySnapshot(10); using var safety = new BlockingSecondCaptureSafetyProbe(snapshot); var session = new FakeConnectOnlySession(); var dispatcherFactory = new TrackingStaDispatcherFactory(); var runner = new PgmConnectDiagnosticRunner( safety, new FakeConnectOnlySessionFactory(session), new FakeRegistrationProbe(), dispatcherFactory, TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(1)); var execution = runner.ExecuteAsync( new PgmConnectDiagnosticRequest("127.0.0.1", 30001, "PGM")); try { Assert.True(safety.SecondCaptureEntered.Wait(TimeSpan.FromSeconds(2))); Assert.True(session.PreventionAttempted.Wait(TimeSpan.FromSeconds(2))); } finally { safety.ReleaseSecondCapture.Set(); } var report = await execution.WaitAsync(TimeSpan.FromSeconds(3)); Assert.True(session.ConnectCompleted.Wait(TimeSpan.FromSeconds(2))); Assert.Equal("connect-timeout", report.ErrorCode); Assert.False(report.Completed); Assert.False(report.OutcomeUnknown); Assert.False(report.KtapConnectAttempted); Assert.Equal(PlayoutKtapConnectState.NotAttempted, report.LastKtapConnectState); Assert.False(session.ComActivationAttempted); Assert.Equal(0, session.DispatchCount); Assert.Contains("Connect", session.Calls); } [Fact] public void ConnectOnlySessionSurfaceHasNoRenderCommand() { var methodNames = typeof(IKtapConnectOnlySession) .GetMethods() .Select(method => method.Name) .ToArray(); string[] forbidden = [ "GetScenePlayer", "LoadScene", "Prepare", "Play", "PlayDirect", "TakeIn", "Next", "TakeOut", "CutOut", "Stop", "StopAll" ]; Assert.DoesNotContain(methodNames, name => forbidden.Contains(name, StringComparer.OrdinalIgnoreCase)); Assert.Contains("Connect", methodNames); Assert.Contains("Disconnect", methodNames); } [Fact] public void ListenerPortDecoderUsesNetworkByteOrder() { const ushort port = 30001; var encoded = unchecked((uint)(ushort)IPAddress.HostToNetworkOrder((short)port)); Assert.Equal(port, WindowsTcpListenerOwnershipProbe.DecodePort(encoded)); } private static PgmDiagnosticTargetSnapshot ReadySnapshot(int processId) => new( true, 1, 1, true, new PgmDiagnosticTargetIdentity(processId, 100)); private sealed class FakeSafetyProbe : IPgmDiagnosticSafetyProbe { private readonly Queue _snapshots; private PgmDiagnosticTargetSnapshot _last; public FakeSafetyProbe(params PgmDiagnosticTargetSnapshot[] snapshots) { _snapshots = new Queue(snapshots); _last = snapshots.Last(); } public PgmDiagnosticTargetSnapshot Capture( PgmConnectDiagnosticRequest request, IPAddress parsedHost) { if (_snapshots.Count > 0) { _last = _snapshots.Dequeue(); } return _last; } } private sealed class BlockingSecondCaptureSafetyProbe : IPgmDiagnosticSafetyProbe, IDisposable { private readonly PgmDiagnosticTargetSnapshot _snapshot; private int _captureCount; public BlockingSecondCaptureSafetyProbe(PgmDiagnosticTargetSnapshot snapshot) { _snapshot = snapshot; } public ManualResetEventSlim SecondCaptureEntered { get; } = new(false); public ManualResetEventSlim ReleaseSecondCapture { get; } = new(false); public PgmDiagnosticTargetSnapshot Capture( PgmConnectDiagnosticRequest request, IPAddress parsedHost) { if (Interlocked.Increment(ref _captureCount) == 2) { SecondCaptureEntered.Set(); ReleaseSecondCapture.Wait(); } return _snapshot; } public void Dispose() { ReleaseSecondCapture.Set(); SecondCaptureEntered.Dispose(); ReleaseSecondCapture.Dispose(); } } private sealed class FakeConnectOnlySessionFactory : IKtapConnectOnlySessionFactory { private readonly IKtapConnectOnlySession _session; public FakeConnectOnlySessionFactory(IKtapConnectOnlySession session) { _session = session; } public int CreateCount { get; private set; } public IKtapConnectOnlySession Create() { CreateCount++; return _session; } } private sealed class UnavailableInteropSessionFactory : IKtapConnectOnlySessionFactory { public IKtapConnectOnlySession Create() => throw new InstalledK3dInteropMetadataUnavailableException(); } private sealed class FakeConnectOnlySession : IKtapConnectOnlySession { private int _dispatchGate; private int _dispatchCount; private int _abandonRecorded; public ConcurrentQueue Calls { get; } = new(); public ManualResetEventSlim PreventionAttempted { get; } = new(false); public ManualResetEventSlim ConnectCompleted { get; } = new(false); public int DispatchCount => Volatile.Read(ref _dispatchCount); public bool ComActivationAttempted { get; private set; } public PlayoutKtapConnectState LastKtapConnectState { get; private set; } public bool TryPreventKtapConnect() { PreventionAttempted.Set(); var previous = Interlocked.CompareExchange(ref _dispatchGate, 2, 0); if (previous == 1 && LastKtapConnectState == PlayoutKtapConnectState.NotAttempted) { LastKtapConnectState = PlayoutKtapConnectState.Attempted; } return previous == 0; } public void Connect(string host, int port, Func finalSafetyCheck) { Calls.Enqueue("Connect"); try { if (Volatile.Read(ref _dispatchGate) != 0) { throw new InvalidOperationException(); } ComActivationAttempted = true; if (!finalSafetyCheck()) { throw new PgmDiagnosticSafetyException(); } if (Interlocked.CompareExchange(ref _dispatchGate, 1, 0) != 0) { throw new InvalidOperationException(); } Interlocked.Increment(ref _dispatchCount); LastKtapConnectState = PlayoutKtapConnectState.AcceptedUnconfirmed; } finally { ConnectCompleted.Set(); } } public void Disconnect() => Calls.Enqueue("Disconnect"); public void Abandon() { TryPreventKtapConnect(); if (Interlocked.Exchange(ref _abandonRecorded, 1) == 0) { Calls.Enqueue("Abandon"); } } } private sealed class RecordingComBinding : IK3dConnectOnlyComBinding { private readonly ConcurrentQueue _log; public RecordingComBinding(ConcurrentQueue log) { _log = log; } public object CreateEventHandler() { _log.Enqueue("CreateEventHandler"); return new object(); } public object CreateEngine() { _log.Enqueue("CreateEngine"); return new object(); } public int KtapConnect( object engine, string host, int port, object eventHandler) { _log.Enqueue("KTAPConnect"); return 1; } public void Disconnect(object engine) => _log.Enqueue("Disconnect"); public void Release(object value) => _log.Enqueue("Release"); } }