using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using MxGateway.Contracts.Proto; using MxGateway.Worker.MxAccess; using MxGateway.Worker.Sta; namespace MxGateway.Worker.Tests.MxAccess; public sealed class MxAccessCommandExecutorTests { [Fact] public async Task DispatchAsync_Register_CallsMxAccessOnStaAndPreservesServerHandle() { FakeMxAccessComObjectFactory factory = new(new FakeMxAccessComObject(registerHandle: 42)); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); MxCommandReply reply = await session.DispatchAsync(CreateRegisterCommand("correlation-1", "client-a")); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult); Assert.Equal(0, reply.Hresult); Assert.Equal(42, reply.Register.ServerHandle); Assert.Equal(MxDataType.Integer, reply.ReturnValue.DataType); Assert.Equal(42, reply.ReturnValue.Int32Value); Assert.Equal(runtime.StaThreadId, factory.FakeComObject.RegisterThreadId); Assert.Equal("client-a", factory.FakeComObject.RegisteredClientName); RegisteredServerHandle registeredServerHandle = Assert.Single( await session.GetRegisteredServerHandlesAsync()); Assert.Equal(42, registeredServerHandle.ServerHandle); Assert.Equal("client-a", registeredServerHandle.ClientName); } [Fact] public async Task DispatchAsync_Unregister_CallsMxAccessOnStaAndRemovesTrackedServerHandle() { FakeMxAccessComObject fakeComObject = new(registerHandle: 43); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register", "client-a")); MxCommandReply reply = await session.DispatchAsync(CreateUnregisterCommand("unregister", 43)); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.Equal(43, fakeComObject.UnregisteredServerHandle); Assert.Equal(runtime.StaThreadId, fakeComObject.UnregisterThreadId); Assert.Empty(await session.GetRegisteredServerHandlesAsync()); } [Fact] public async Task DispatchAsync_UnregisterWhenMxAccessThrows_PreservesHResultAndDoesNotRewriteFailure() { const int hresult = unchecked((int)0x80070057); FakeMxAccessComObject fakeComObject = new( registerHandle: 44, unregisterException: new COMException("Invalid handle.", hresult)); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-failure", "client-a")); MxCommandReply reply = await session.DispatchAsync(CreateUnregisterCommand("invalid-unregister", 44)); Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult); Assert.Equal(hresult, reply.Hresult); Assert.Contains("0x80070057", reply.DiagnosticMessage); Assert.Equal(44, fakeComObject.UnregisteredServerHandle); RegisteredServerHandle registeredServerHandle = Assert.Single( await session.GetRegisteredServerHandlesAsync()); Assert.Equal(44, registeredServerHandle.ServerHandle); } [Fact] public async Task DispatchAsync_AddItem_CallsMxAccessOnStaAndTracksItemHandle() { FakeMxAccessComObject fakeComObject = new( registerHandle: 46, addItemHandle: 501); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-add", "client-a")); MxCommandReply reply = await session.DispatchAsync(CreateAddItemCommand( "add-item", 46, "Galaxy.Tag.Value")); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult); Assert.Equal(0, reply.Hresult); Assert.Equal(501, reply.AddItem.ItemHandle); Assert.Equal(MxDataType.Integer, reply.ReturnValue.DataType); Assert.Equal(501, reply.ReturnValue.Int32Value); Assert.Equal(46, fakeComObject.AddItemServerHandle); Assert.Equal("Galaxy.Tag.Value", fakeComObject.AddItemDefinition); Assert.Equal(runtime.StaThreadId, fakeComObject.AddItemThreadId); RegisteredItemHandle registeredItemHandle = Assert.Single( await session.GetRegisteredItemHandlesAsync()); Assert.Equal(46, registeredItemHandle.ServerHandle); Assert.Equal(501, registeredItemHandle.ItemHandle); Assert.Equal("Galaxy.Tag.Value", registeredItemHandle.ItemDefinition); Assert.Equal(string.Empty, registeredItemHandle.ItemContext); Assert.False(registeredItemHandle.HasItemContext); } [Fact] public async Task DispatchAsync_AddItem2_PassesContextExactlyAndTracksItemHandle() { FakeMxAccessComObject fakeComObject = new( registerHandle: 47, addItem2Handle: 502); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-add2", "client-a")); MxCommandReply reply = await session.DispatchAsync(CreateAddItem2Command( "add-item2", 47, "TestInt", "TestChildObject")); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.Equal(502, reply.AddItem2.ItemHandle); Assert.Equal(MxDataType.Integer, reply.ReturnValue.DataType); Assert.Equal(502, reply.ReturnValue.Int32Value); Assert.Equal(47, fakeComObject.AddItem2ServerHandle); Assert.Equal("TestInt", fakeComObject.AddItem2Definition); Assert.Equal("TestChildObject", fakeComObject.AddItem2Context); Assert.Equal(runtime.StaThreadId, fakeComObject.AddItem2ThreadId); RegisteredItemHandle registeredItemHandle = Assert.Single( await session.GetRegisteredItemHandlesAsync()); Assert.Equal(47, registeredItemHandle.ServerHandle); Assert.Equal(502, registeredItemHandle.ItemHandle); Assert.Equal("TestInt", registeredItemHandle.ItemDefinition); Assert.Equal("TestChildObject", registeredItemHandle.ItemContext); Assert.True(registeredItemHandle.HasItemContext); } [Fact] public async Task DispatchAsync_RemoveItem_CallsMxAccessOnStaAndRemovesTrackedItemHandle() { FakeMxAccessComObject fakeComObject = new( registerHandle: 48, addItemHandle: 503); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-remove", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-remove", 48, "Galaxy.Tag.Value")); MxCommandReply reply = await session.DispatchAsync(CreateRemoveItemCommand( "remove-item", 48, 503)); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult); Assert.Equal(0, reply.Hresult); Assert.Equal(48, fakeComObject.RemoveItemServerHandle); Assert.Equal(503, fakeComObject.RemovedItemHandle); Assert.Equal(runtime.StaThreadId, fakeComObject.RemoveItemThreadId); Assert.Empty(await session.GetRegisteredItemHandlesAsync()); } [Fact] public async Task DispatchAsync_RemoveItemWithAdvisedHandle_RemovesTrackedAdviceAfterMxAccessSucceeds() { FakeMxAccessComObject fakeComObject = new( registerHandle: 148, addItemHandle: 603); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-advised-remove", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-advised-remove", 148, "Galaxy.Tag.Value")); await session.DispatchAsync(CreateAdviseCommand("advise-before-remove", 148, 603)); MxCommandReply reply = await session.DispatchAsync(CreateRemoveItemCommand( "remove-advised-item", 148, 603)); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.Empty(await session.GetRegisteredItemHandlesAsync()); Assert.Empty(await session.GetRegisteredAdviceHandlesAsync()); } [Fact] public async Task DispatchAsync_RemoveItemWithCrossServerHandle_PreservesHResultAndKeepsTrackedItemHandle() { const int hresult = unchecked((int)0x80070057); FakeMxAccessComObject fakeComObject = new( registerHandle: 49, addItemHandle: 504, removeItemException: new COMException("Invalid item handle.", hresult)); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-remove-failure", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-remove-failure", 49, "Galaxy.Tag.Value")); MxCommandReply reply = await session.DispatchAsync(CreateRemoveItemCommand( "remove-item-failure", 999, 504)); Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult); Assert.Equal(hresult, reply.Hresult); Assert.Contains("0x80070057", reply.DiagnosticMessage); Assert.Equal(999, fakeComObject.RemoveItemServerHandle); Assert.Equal(504, fakeComObject.RemovedItemHandle); RegisteredItemHandle registeredItemHandle = Assert.Single( await session.GetRegisteredItemHandlesAsync()); Assert.Equal(49, registeredItemHandle.ServerHandle); Assert.Equal(504, registeredItemHandle.ItemHandle); } [Fact] public async Task DispatchAsync_AddItem2WhenMxAccessThrows_PreservesHResultAndDoesNotTrackItemHandle() { const int hresult = unchecked((int)0x80070057); FakeMxAccessComObject fakeComObject = new( registerHandle: 50, addItem2Exception: new COMException("Invalid server handle.", hresult)); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); MxCommandReply reply = await session.DispatchAsync(CreateAddItem2Command( "add-item2-failure", 9001, "TestInt", "TestChildObject")); Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult); Assert.Equal(hresult, reply.Hresult); Assert.Contains("0x80070057", reply.DiagnosticMessage); Assert.Equal(9001, fakeComObject.AddItem2ServerHandle); Assert.Equal("TestInt", fakeComObject.AddItem2Definition); Assert.Equal("TestChildObject", fakeComObject.AddItem2Context); Assert.Empty(await session.GetRegisteredItemHandlesAsync()); } [Fact] public async Task DispatchAsync_Advise_CallsMxAccessOnStaAndTracksPlainAdvice() { FakeMxAccessComObject fakeComObject = new( registerHandle: 52, addItemHandle: 505); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-advise", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-advise", 52, "Galaxy.Tag.Value")); MxCommandReply reply = await session.DispatchAsync(CreateAdviseCommand( "advise", 52, 505)); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult); Assert.Equal(0, reply.Hresult); Assert.Equal(52, fakeComObject.AdviseServerHandle); Assert.Equal(505, fakeComObject.AdvisedItemHandle); Assert.Equal(runtime.StaThreadId, fakeComObject.AdviseThreadId); RegisteredAdviceHandle adviceHandle = Assert.Single( await session.GetRegisteredAdviceHandlesAsync()); Assert.Equal(52, adviceHandle.ServerHandle); Assert.Equal(505, adviceHandle.ItemHandle); Assert.Equal(MxAccessAdviceKind.Plain, adviceHandle.AdviceKind); } [Fact] public async Task DispatchAsync_AdviseSupervisory_CallsDistinctMxAccessMethodAndTracksSupervisoryAdvice() { FakeMxAccessComObject fakeComObject = new( registerHandle: 53, addItemHandle: 506); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-supervisory", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-supervisory", 53, "Galaxy.Tag.Value")); MxCommandReply reply = await session.DispatchAsync(CreateAdviseSupervisoryCommand( "advise-supervisory", 53, 506)); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.Equal(53, fakeComObject.AdviseSupervisoryServerHandle); Assert.Equal(506, fakeComObject.AdviseSupervisoryItemHandle); Assert.Equal(runtime.StaThreadId, fakeComObject.AdviseSupervisoryThreadId); Assert.Null(fakeComObject.AdviseServerHandle); RegisteredAdviceHandle adviceHandle = Assert.Single( await session.GetRegisteredAdviceHandlesAsync()); Assert.Equal(53, adviceHandle.ServerHandle); Assert.Equal(506, adviceHandle.ItemHandle); Assert.Equal(MxAccessAdviceKind.Supervisory, adviceHandle.AdviceKind); } [Fact] public async Task DispatchAsync_UnAdvise_CallsMxAccessOnStaAndRemovesTrackedAdvice() { FakeMxAccessComObject fakeComObject = new( registerHandle: 54, addItemHandle: 507); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-unadvise", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-unadvise", 54, "Galaxy.Tag.Value")); await session.DispatchAsync(CreateAdviseCommand("advise-before-unadvise", 54, 507)); await session.DispatchAsync(CreateAdviseSupervisoryCommand("supervisory-before-unadvise", 54, 507)); MxCommandReply reply = await session.DispatchAsync(CreateUnAdviseCommand( "unadvise", 54, 507)); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.Equal(54, fakeComObject.UnAdviseServerHandle); Assert.Equal(507, fakeComObject.UnAdvisedItemHandle); Assert.Equal(runtime.StaThreadId, fakeComObject.UnAdviseThreadId); Assert.Empty(await session.GetRegisteredAdviceHandlesAsync()); } [Fact] public async Task DispatchAsync_AdviseWhenMxAccessThrows_PreservesHResultAndDoesNotTrackAdvice() { const int hresult = unchecked((int)0x80070057); FakeMxAccessComObject fakeComObject = new( registerHandle: 55, addItemHandle: 508, adviseException: new COMException("Invalid item handle.", hresult)); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-advise-failure", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-advise-failure", 55, "Galaxy.Tag.Value")); MxCommandReply reply = await session.DispatchAsync(CreateAdviseCommand( "advise-failure", 55, 999)); Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult); Assert.Equal(hresult, reply.Hresult); Assert.Contains("0x80070057", reply.DiagnosticMessage); Assert.Equal(55, fakeComObject.AdviseServerHandle); Assert.Equal(999, fakeComObject.AdvisedItemHandle); Assert.Empty(await session.GetRegisteredAdviceHandlesAsync()); } [Fact] public async Task DispatchAsync_UnAdviseWhenMxAccessThrows_PreservesHResultAndKeepsTrackedAdvice() { const int hresult = unchecked((int)0x80070057); FakeMxAccessComObject fakeComObject = new( registerHandle: 56, addItemHandle: 509, unAdviseException: new COMException("Invalid item handle.", hresult)); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-unadvise-failure", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-unadvise-failure", 56, "Galaxy.Tag.Value")); await session.DispatchAsync(CreateAdviseCommand("advise-before-unadvise-failure", 56, 509)); MxCommandReply reply = await session.DispatchAsync(CreateUnAdviseCommand( "unadvise-failure", 56, 509)); Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); Assert.True(reply.HasHresult); Assert.Equal(hresult, reply.Hresult); Assert.Contains("0x80070057", reply.DiagnosticMessage); Assert.Equal(56, fakeComObject.UnAdviseServerHandle); Assert.Equal(509, fakeComObject.UnAdvisedItemHandle); RegisteredAdviceHandle adviceHandle = Assert.Single( await session.GetRegisteredAdviceHandlesAsync()); Assert.Equal(MxAccessAdviceKind.Plain, adviceHandle.AdviceKind); } [Fact] public async Task DispatchAsync_SubscribeBulk_RunsSequentialMxAccessCallsAndReturnsPerItemResults() { FakeMxAccessComObject fakeComObject = new( registerHandle: 60, addItemHandle: 512); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); MxCommandReply reply = await session.DispatchAsync(CreateSubscribeBulkCommand( "subscribe-bulk", 60, ["", "Galaxy.Tag.Value"])); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); Assert.Equal(MxCommandKind.SubscribeBulk, reply.Kind); Assert.Collection( reply.SubscribeBulk.Results, result => { Assert.False(result.WasSuccessful); Assert.Equal(string.Empty, result.TagAddress); Assert.Equal(0, result.ItemHandle); Assert.Contains("required", result.ErrorMessage, StringComparison.OrdinalIgnoreCase); }, result => { Assert.True(result.WasSuccessful); Assert.Equal("Galaxy.Tag.Value", result.TagAddress); Assert.Equal(512, result.ItemHandle); }); Assert.Equal( ["AddItem:60:Galaxy.Tag.Value", "Advise:60:512"], fakeComObject.OperationNames); Assert.Equal(runtime.StaThreadId, fakeComObject.AddItemThreadId); Assert.Equal(runtime.StaThreadId, fakeComObject.AdviseThreadId); } [Fact] public async Task DispatchAsync_UnsubscribeBulk_RemovesItemAfterUnAdviseFailure() { const int hresult = unchecked((int)0x80070057); FakeMxAccessComObject fakeComObject = new( registerHandle: 61, unAdviseException: new COMException("Invalid item handle.", hresult)); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); MxCommandReply reply = await session.DispatchAsync(CreateUnsubscribeBulkCommand( "unsubscribe-bulk", 61, [513])); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); SubscribeResult result = Assert.Single(reply.UnsubscribeBulk.Results); Assert.False(result.WasSuccessful); Assert.Equal(513, result.ItemHandle); Assert.Equal(string.Empty, result.TagAddress); Assert.Contains("UnAdvise failed", result.ErrorMessage); Assert.Equal( ["UnAdvise:61:513", "RemoveItem:61:513"], fakeComObject.OperationNames); } [Fact] public async Task ShutdownGracefullyAsync_CleansHandlesInAdviceItemServerOrder() { FakeMxAccessComObject fakeComObject = new( registerHandle: 58, addItemHandle: 510); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-shutdown", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-shutdown", 58, "Galaxy.Tag.Value")); await session.DispatchAsync(CreateAdviseCommand("advise-before-shutdown", 58, 510)); await session.DispatchAsync(CreateAdviseSupervisoryCommand("supervisory-before-shutdown", 58, 510)); MxAccessShutdownResult result = await session.ShutdownGracefullyAsync(TimeSpan.FromSeconds(2)); Assert.True(result.Succeeded); Assert.Equal( new[] { "UnAdvise:58:510", "RemoveItem:58:510", "Unregister:58" }, fakeComObject.OperationNames.Where(name => name.StartsWith("Un", StringComparison.Ordinal) || name.StartsWith("Remove", StringComparison.Ordinal))); } [Fact] public async Task ShutdownGracefullyAsync_RecordsCleanupFailuresAndContinues() { const int hresult = unchecked((int)0x80070057); COMException cleanupException = new("Invalid handle.", hresult); FakeMxAccessComObject fakeComObject = new( registerHandle: 59, addItemHandle: 511, unregisterException: cleanupException, removeItemException: cleanupException, unAdviseException: cleanupException); FakeMxAccessComObjectFactory factory = new(fakeComObject); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); await session.DispatchAsync(CreateRegisterCommand("register-before-shutdown-failure", "client-a")); await session.DispatchAsync(CreateAddItemCommand("add-before-shutdown-failure", 59, "Galaxy.Tag.Value")); await session.DispatchAsync(CreateAdviseCommand("advise-before-shutdown-failure", 59, 511)); MxAccessShutdownResult result = await session.ShutdownGracefullyAsync(TimeSpan.FromSeconds(2)); Assert.False(result.Succeeded); Assert.Equal(new[] { "UnAdvise", "RemoveItem", "Unregister" }, result.Failures.Select(failure => failure.Operation)); Assert.All(result.Failures, failure => Assert.Equal(hresult, failure.HResult)); Assert.Contains("Unregister:59", fakeComObject.OperationNames); } [Fact] public async Task DispatchAsync_RegisterWithoutPayload_ReturnsInvalidRequest() { FakeMxAccessComObjectFactory factory = new(new FakeMxAccessComObject(registerHandle: 45)); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); MxCommandReply reply = await session.DispatchAsync(new StaCommand( "session-1", "missing-payload", new MxCommand { Kind = MxCommandKind.Register, })); Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); Assert.Null(factory.FakeComObject.RegisteredClientName); } [Fact] public async Task DispatchAsync_AddItemWithoutPayload_ReturnsInvalidRequest() { FakeMxAccessComObjectFactory factory = new(new FakeMxAccessComObject(registerHandle: 51)); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); MxCommandReply reply = await session.DispatchAsync(new StaCommand( "session-1", "missing-add-payload", new MxCommand { Kind = MxCommandKind.AddItem, })); Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); Assert.Null(factory.FakeComObject.AddItemDefinition); } [Fact] public async Task DispatchAsync_AdviseWithoutPayload_ReturnsInvalidRequest() { FakeMxAccessComObjectFactory factory = new(new FakeMxAccessComObject(registerHandle: 57)); using StaRuntime runtime = CreateRuntime(); using MxAccessStaSession session = new(runtime, factory, new NoopEventSink()); await session.StartAsync(workerProcessId: 1234); MxCommandReply reply = await session.DispatchAsync(new StaCommand( "session-1", "missing-advise-payload", new MxCommand { Kind = MxCommandKind.Advise, })); Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); Assert.Null(factory.FakeComObject.AdviseServerHandle); } private static StaCommand CreateRegisterCommand( string correlationId, string clientName) { return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.Register, Register = new RegisterCommand { ClientName = clientName, }, }); } private static StaCommand CreateUnregisterCommand( string correlationId, int serverHandle) { return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.Unregister, Unregister = new UnregisterCommand { ServerHandle = serverHandle, }, }); } private static StaCommand CreateAddItemCommand( string correlationId, int serverHandle, string itemDefinition) { return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.AddItem, AddItem = new AddItemCommand { ServerHandle = serverHandle, ItemDefinition = itemDefinition, }, }); } private static StaCommand CreateAddItem2Command( string correlationId, int serverHandle, string itemDefinition, string itemContext) { return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.AddItem2, AddItem2 = new AddItem2Command { ServerHandle = serverHandle, ItemDefinition = itemDefinition, ItemContext = itemContext, }, }); } private static StaCommand CreateRemoveItemCommand( string correlationId, int serverHandle, int itemHandle) { return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.RemoveItem, RemoveItem = new RemoveItemCommand { ServerHandle = serverHandle, ItemHandle = itemHandle, }, }); } private static StaCommand CreateAdviseCommand( string correlationId, int serverHandle, int itemHandle) { return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.Advise, Advise = new AdviseCommand { ServerHandle = serverHandle, ItemHandle = itemHandle, }, }); } private static StaCommand CreateUnAdviseCommand( string correlationId, int serverHandle, int itemHandle) { return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.UnAdvise, UnAdvise = new UnAdviseCommand { ServerHandle = serverHandle, ItemHandle = itemHandle, }, }); } private static StaCommand CreateSubscribeBulkCommand( string correlationId, int serverHandle, IEnumerable tagAddresses) { SubscribeBulkCommand command = new() { ServerHandle = serverHandle, }; command.TagAddresses.Add(tagAddresses); return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.SubscribeBulk, SubscribeBulk = command, }); } private static StaCommand CreateUnsubscribeBulkCommand( string correlationId, int serverHandle, IEnumerable itemHandles) { UnsubscribeBulkCommand command = new() { ServerHandle = serverHandle, }; command.ItemHandles.Add(itemHandles); return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.UnsubscribeBulk, UnsubscribeBulk = command, }); } private static StaCommand CreateAdviseSupervisoryCommand( string correlationId, int serverHandle, int itemHandle) { return new StaCommand( "session-1", correlationId, new MxCommand { Kind = MxCommandKind.AdviseSupervisory, AdviseSupervisory = new AdviseSupervisoryCommand { ServerHandle = serverHandle, ItemHandle = itemHandle, }, }); } private static StaRuntime CreateRuntime() { return new StaRuntime( new NoopComApartmentInitializer(), new StaMessagePump(), TimeSpan.FromMilliseconds(25)); } private sealed class FakeMxAccessComObject { private readonly int registerHandle; private readonly int addItemHandle; private readonly int addItem2Handle; private readonly Exception? unregisterException; private readonly Exception? addItemException; private readonly Exception? addItem2Exception; private readonly Exception? removeItemException; private readonly Exception? adviseException; private readonly Exception? unAdviseException; private readonly Exception? adviseSupervisoryException; private readonly List operationNames = new(); public FakeMxAccessComObject( int registerHandle, int addItemHandle = 0, int addItem2Handle = 0, Exception? unregisterException = null, Exception? addItemException = null, Exception? addItem2Exception = null, Exception? removeItemException = null, Exception? adviseException = null, Exception? unAdviseException = null, Exception? adviseSupervisoryException = null) { this.registerHandle = registerHandle; this.addItemHandle = addItemHandle; this.addItem2Handle = addItem2Handle; this.unregisterException = unregisterException; this.addItemException = addItemException; this.addItem2Exception = addItem2Exception; this.removeItemException = removeItemException; this.adviseException = adviseException; this.unAdviseException = unAdviseException; this.adviseSupervisoryException = adviseSupervisoryException; } public string? RegisteredClientName { get; private set; } public int? RegisterThreadId { get; private set; } public int? UnregisteredServerHandle { get; private set; } public int? UnregisterThreadId { get; private set; } public int? AddItemServerHandle { get; private set; } public string? AddItemDefinition { get; private set; } public int? AddItemThreadId { get; private set; } public int? AddItem2ServerHandle { get; private set; } public string? AddItem2Definition { get; private set; } public string? AddItem2Context { get; private set; } public int? AddItem2ThreadId { get; private set; } public int? RemoveItemServerHandle { get; private set; } public int? RemovedItemHandle { get; private set; } public int? RemoveItemThreadId { get; private set; } public int? AdviseServerHandle { get; private set; } public int? AdvisedItemHandle { get; private set; } public int? AdviseThreadId { get; private set; } public int? UnAdviseServerHandle { get; private set; } public int? UnAdvisedItemHandle { get; private set; } public int? UnAdviseThreadId { get; private set; } public int? AdviseSupervisoryServerHandle { get; private set; } public int? AdviseSupervisoryItemHandle { get; private set; } public int? AdviseSupervisoryThreadId { get; private set; } public IReadOnlyList OperationNames => operationNames.ToArray(); public int Register(string clientName) { operationNames.Add($"Register:{clientName}"); RegisteredClientName = clientName; RegisterThreadId = Environment.CurrentManagedThreadId; return registerHandle; } public void Unregister(int serverHandle) { operationNames.Add($"Unregister:{serverHandle}"); UnregisteredServerHandle = serverHandle; UnregisterThreadId = Environment.CurrentManagedThreadId; if (unregisterException is not null) { throw unregisterException; } } public int AddItem( int serverHandle, string itemDefinition) { operationNames.Add($"AddItem:{serverHandle}:{itemDefinition}"); AddItemServerHandle = serverHandle; AddItemDefinition = itemDefinition; AddItemThreadId = Environment.CurrentManagedThreadId; if (addItemException is not null) { throw addItemException; } return addItemHandle; } public int AddItem2( int serverHandle, string itemDefinition, string itemContext) { operationNames.Add($"AddItem2:{serverHandle}:{itemDefinition}:{itemContext}"); AddItem2ServerHandle = serverHandle; AddItem2Definition = itemDefinition; AddItem2Context = itemContext; AddItem2ThreadId = Environment.CurrentManagedThreadId; if (addItem2Exception is not null) { throw addItem2Exception; } return addItem2Handle; } public void RemoveItem( int serverHandle, int itemHandle) { operationNames.Add($"RemoveItem:{serverHandle}:{itemHandle}"); RemoveItemServerHandle = serverHandle; RemovedItemHandle = itemHandle; RemoveItemThreadId = Environment.CurrentManagedThreadId; if (removeItemException is not null) { throw removeItemException; } } public void Advise( int serverHandle, int itemHandle) { operationNames.Add($"Advise:{serverHandle}:{itemHandle}"); AdviseServerHandle = serverHandle; AdvisedItemHandle = itemHandle; AdviseThreadId = Environment.CurrentManagedThreadId; if (adviseException is not null) { throw adviseException; } } public void UnAdvise( int serverHandle, int itemHandle) { operationNames.Add($"UnAdvise:{serverHandle}:{itemHandle}"); UnAdviseServerHandle = serverHandle; UnAdvisedItemHandle = itemHandle; UnAdviseThreadId = Environment.CurrentManagedThreadId; if (unAdviseException is not null) { throw unAdviseException; } } public void AdviseSupervisory( int serverHandle, int itemHandle) { operationNames.Add($"AdviseSupervisory:{serverHandle}:{itemHandle}"); AdviseSupervisoryServerHandle = serverHandle; AdviseSupervisoryItemHandle = itemHandle; AdviseSupervisoryThreadId = Environment.CurrentManagedThreadId; if (adviseSupervisoryException is not null) { throw adviseSupervisoryException; } } } private sealed class FakeMxAccessComObjectFactory : IMxAccessComObjectFactory { public FakeMxAccessComObjectFactory(FakeMxAccessComObject fakeComObject) { FakeComObject = fakeComObject; } public FakeMxAccessComObject FakeComObject { get; } public object Create() { return FakeComObject; } } private sealed class NoopEventSink : IMxAccessEventSink { public void Attach( object mxAccessComObject, string sessionId) { } public void Detach() { } } private sealed class NoopComApartmentInitializer : IStaComApartmentInitializer { public void Initialize() { } public void Uninitialize() { } } }