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;
///
/// Tests for .
///
public sealed class MxAccessStaSessionTests
{
///
/// Verifies that StartAsync creates the MXAccess COM object and attaches the event sink on the STA thread.
///
[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("session-1", 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);
Assert.Equal("session-1", eventSink.SessionId);
}
///
/// Verifies that StartAsync maps creation exceptions with HResult when the factory fails.
///
[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);
}
///
/// Verifies that Dispose detaches the event sink on the STA thread.
///
[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));
}
///
/// Fake MXAccess COM object factory for testing.
///
private sealed class FakeMxAccessComObjectFactory : IMxAccessComObjectFactory
{
private readonly Exception? exception;
///
/// Initializes a fake factory that optionally throws an exception.
///
/// Exception to throw when Create is called; null to succeed.
public FakeMxAccessComObjectFactory(Exception? exception = null)
{
this.exception = exception;
}
///
/// Gets the COM object created by this factory.
///
public object CreatedObject { get; } = new();
///
/// Gets the managed thread ID when Create was called.
///
public int? CreateThreadId { get; private set; }
///
/// Gets the apartment state when Create was called.
///
public ApartmentState? CreateApartmentState { get; private set; }
///
/// Creates the COM object or throws the configured exception.
///
public object Create()
{
CreateThreadId = Thread.CurrentThread.ManagedThreadId;
CreateApartmentState = Thread.CurrentThread.GetApartmentState();
if (exception is not null)
{
throw exception;
}
return CreatedObject;
}
}
///
/// Fake MXAccess event sink for testing.
///
private sealed class FakeMxAccessEventSink : IMxAccessEventSink
{
///
/// Gets the attached MXAccess COM object.
///
public object? AttachedObject { get; private set; }
///
/// Gets the managed thread ID when Attach was called.
///
public int? AttachThreadId { get; private set; }
///
/// Gets the managed thread ID when Detach was called.
///
public int? DetachThreadId { get; private set; }
///
/// Gets the session identifier.
///
public string? SessionId { get; private set; }
///
/// Attaches the MXAccess COM object and records thread context.
///
/// MXAccess COM object to attach.
/// Identifier of the session.
public void Attach(
object mxAccessComObject,
string sessionId)
{
AttachedObject = mxAccessComObject;
AttachThreadId = Thread.CurrentThread.ManagedThreadId;
SessionId = sessionId;
}
///
/// Detaches the MXAccess COM object and records thread context.
///
public void Detach()
{
DetachThreadId = Thread.CurrentThread.ManagedThreadId;
AttachedObject = null;
}
}
///
/// Noop STA COM apartment initializer for testing.
///
private sealed class NoopComApartmentInitializer : IStaComApartmentInitializer
{
///
/// Initializes the COM apartment (no-op).
///
public void Initialize()
{
}
///
/// Uninitializes the COM apartment (no-op).
///
public void Uninitialize()
{
}
}
}