using System; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using MxGateway.Contracts.Proto; using MxGateway.Worker.MxAccess; using MxGateway.Worker.Sta; namespace MxGateway.Worker.Tests.MxAccess; public sealed class MxAccessStaSessionTests { [Fact] public async Task StartAsync_CreatesComObjectAndAttachesEventSinkOnStaThread() { FakeMxAccessComObjectFactory factory = new(); FakeMxAccessEventSink eventSink = new(); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, eventSink); WorkerReady ready = await session.StartAsync(workerProcessId: 1234); Assert.Equal(1234, ready.WorkerProcessId); Assert.Equal(MxAccessInteropInfo.ProgId, ready.MxaccessProgid); Assert.Equal(MxAccessInteropInfo.Clsid, ready.MxaccessClsid); Assert.NotNull(ready.ReadyTimestamp); Assert.Equal(runtime.StaThreadId, factory.CreateThreadId); Assert.Equal(runtime.StaThreadId, eventSink.AttachThreadId); Assert.Equal(ApartmentState.STA, factory.CreateApartmentState); Assert.Same(factory.CreatedObject, eventSink.AttachedObject); } [Fact] public async Task StartAsync_WhenFactoryFails_MapsCreationExceptionWithHResult() { const int hresult = unchecked((int)0x80040154); FakeMxAccessComObjectFactory factory = new(new COMException("Class not registered.", hresult)); FakeMxAccessEventSink eventSink = new(); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, eventSink); MxAccessCreationException exception = await Assert.ThrowsAsync( () => session.StartAsync(workerProcessId: 1234)); Assert.Equal(hresult, exception.CapturedHResult); Assert.Equal(MxAccessInteropInfo.ProgId, exception.AttemptedProgId); Assert.Equal(MxAccessInteropInfo.Clsid, exception.AttemptedClsid); Assert.Null(eventSink.AttachedObject); } [Fact] public async Task Dispose_DetachesEventSinkOnStaThread() { FakeMxAccessComObjectFactory factory = new(); FakeMxAccessEventSink eventSink = new(); using StaRuntime runtime = CreateRuntime(); MxAccessStaSession session = new(runtime, factory, eventSink); await session.StartAsync(workerProcessId: 1234); session.Dispose(); Assert.Equal(runtime.StaThreadId, eventSink.DetachThreadId); } private static StaRuntime CreateRuntime() { return new StaRuntime( new NoopComApartmentInitializer(), new StaMessagePump(), TimeSpan.FromMilliseconds(25)); } private sealed class FakeMxAccessComObjectFactory : IMxAccessComObjectFactory { private readonly Exception? exception; public FakeMxAccessComObjectFactory(Exception? exception = null) { this.exception = exception; } public object CreatedObject { get; } = new(); public int? CreateThreadId { get; private set; } public ApartmentState? CreateApartmentState { get; private set; } public object Create() { CreateThreadId = Thread.CurrentThread.ManagedThreadId; CreateApartmentState = Thread.CurrentThread.GetApartmentState(); if (exception is not null) { throw exception; } return CreatedObject; } } private sealed class FakeMxAccessEventSink : IMxAccessEventSink { public object? AttachedObject { get; private set; } public int? AttachThreadId { get; private set; } public int? DetachThreadId { get; private set; } public void Attach(object mxAccessComObject) { AttachedObject = mxAccessComObject; AttachThreadId = Thread.CurrentThread.ManagedThreadId; } public void Detach() { DetachThreadId = Thread.CurrentThread.ManagedThreadId; AttachedObject = null; } } private sealed class NoopComApartmentInitializer : IStaComApartmentInitializer { public void Initialize() { } public void Uninitialize() { } } }