b0e65d4f31
OtOpcUa's dominant FreeAccess write path goes out as MX_COMMAND_KIND_WRITE, not WriteSecured — the original 06/S-1 brief mis-scoped the correlation, so a refused plain write was invisible on the unary reply (verified live on windev 2026-08-09). ExecuteWrite/ExecuteWrite2 now use the same pre-call version baseline + bounded pump-wait as the secured kinds. Bulk writes stay fire-and-forget.
2585 lines
112 KiB
C#
2585 lines
112 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading.Tasks;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
|
using ZB.MOM.WW.MxGateway.Worker.Sta;
|
|
using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
|
|
|
|
public sealed class MxAccessCommandExecutorTests
|
|
{
|
|
/// <summary>Verifies that Register command calls MXAccess on the STA thread and preserves the server handle.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that Unregister command calls MXAccess on the STA thread and removes the tracked server handle.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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());
|
|
}
|
|
|
|
/// <summary>Verifies that Unregister preserves the HResult when MXAccess throws and does not rewrite the failure.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that AddItem command calls MXAccess on the STA thread and tracks the item handle.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that AddItem2 command passes the context exactly and tracks the item handle.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that RemoveItem command calls MXAccess on the STA thread and removes the tracked item handle.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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());
|
|
}
|
|
|
|
/// <summary>Verifies that RemoveItem removes tracked advice after MXAccess succeeds on an advised handle.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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());
|
|
}
|
|
|
|
/// <summary>Verifies that RemoveItem preserves the HResult and keeps the tracked item handle when using a cross-server handle.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that AddItem2 preserves the HResult when MXAccess throws and does not track the item handle.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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());
|
|
}
|
|
|
|
/// <summary>Verifies that Advise command calls MXAccess on the STA thread and tracks plain advice.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that AdviseSupervisory calls a distinct MXAccess method and tracks supervisory advice.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that UnAdvise command calls MXAccess on the STA thread and removes the tracked advice.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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());
|
|
}
|
|
|
|
/// <summary>Verifies that Advise preserves the HResult when MXAccess throws and does not track the advice.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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());
|
|
}
|
|
|
|
/// <summary>Verifies that UnAdvise preserves the HResult when MXAccess throws and keeps the tracked advice.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that SubscribeBulk runs sequential MXAccess calls and returns per-item results.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that WriteBulk runs MXAccess Write per entry on the STA and returns
|
|
/// one BulkWriteResult per entry in input order, including a per-entry COM
|
|
/// failure surfaced as <c>WasSuccessful=false</c> with the underlying HRESULT.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteBulk_RunsSequentialWritesAndReturnsPerEntryResults()
|
|
{
|
|
const int hresult = unchecked((int)0x80070057);
|
|
FakeMxAccessComObject fakeComObject = new(
|
|
registerHandle: 80,
|
|
writeExceptionByItemHandle: new Dictionary<int, Exception>
|
|
{
|
|
[802] = 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(CreateWriteBulkCommand(
|
|
"write-bulk",
|
|
serverHandle: 80,
|
|
new[]
|
|
{
|
|
(itemHandle: 801, value: 11, userId: 5),
|
|
(itemHandle: 802, value: 22, userId: 5),
|
|
(itemHandle: 803, value: 33, userId: 5),
|
|
}));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(MxCommandKind.WriteBulk, reply.Kind);
|
|
Assert.Equal(3, reply.WriteBulk.Results.Count);
|
|
|
|
BulkWriteResult success1 = reply.WriteBulk.Results[0];
|
|
Assert.True(success1.WasSuccessful);
|
|
Assert.Equal(801, success1.ItemHandle);
|
|
Assert.Equal(string.Empty, success1.ErrorMessage);
|
|
|
|
BulkWriteResult failure = reply.WriteBulk.Results[1];
|
|
Assert.False(failure.WasSuccessful);
|
|
Assert.Equal(802, failure.ItemHandle);
|
|
Assert.True(failure.HasHresult);
|
|
Assert.Equal(hresult, failure.Hresult);
|
|
|
|
BulkWriteResult success3 = reply.WriteBulk.Results[2];
|
|
Assert.True(success3.WasSuccessful);
|
|
Assert.Equal(803, success3.ItemHandle);
|
|
|
|
// Each Write hit the fake COM object on the STA thread.
|
|
Assert.Equal(runtime.StaThreadId, fakeComObject.WriteThreadId);
|
|
Assert.Contains("Write:80:801", fakeComObject.OperationNames);
|
|
Assert.Contains("Write:80:802", fakeComObject.OperationNames);
|
|
Assert.Contains("Write:80:803", fakeComObject.OperationNames);
|
|
}
|
|
|
|
/// <summary>Verifies that Write2Bulk forwards value AND timestamp to each per-entry Write2.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_Write2Bulk_ForwardsValueAndTimestampPerEntry()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 81);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
DateTime timestamp = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWrite2BulkCommand(
|
|
"write2-bulk",
|
|
serverHandle: 81,
|
|
new[] { (itemHandle: 811, value: 100, timestamp, userId: 7) }));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
BulkWriteResult result = Assert.Single(reply.Write2Bulk.Results);
|
|
Assert.True(result.WasSuccessful);
|
|
Assert.Equal(811, result.ItemHandle);
|
|
Assert.Equal(100, fakeComObject.WriteValue);
|
|
Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
|
|
Assert.Equal(7, fakeComObject.WriteUserId);
|
|
Assert.Contains("Write2:81:811", fakeComObject.OperationNames);
|
|
}
|
|
|
|
/// <summary>Verifies that WriteSecuredBulk forwards both user ids per entry.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteSecuredBulk_ForwardsUserIdsPerEntry()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 82);
|
|
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(CreateWriteSecuredBulkCommand(
|
|
"write-secured-bulk",
|
|
serverHandle: 82,
|
|
new[] { (itemHandle: 821, currentUserId: 11, verifierUserId: 22, value: 555) }));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
BulkWriteResult result = Assert.Single(reply.WriteSecuredBulk.Results);
|
|
Assert.True(result.WasSuccessful);
|
|
Assert.Equal(11, fakeComObject.WriteCurrentUserId);
|
|
Assert.Equal(22, fakeComObject.WriteVerifierUserId);
|
|
Assert.Equal(555, fakeComObject.WriteValue);
|
|
Assert.Contains("WriteSecured:82:821", fakeComObject.OperationNames);
|
|
}
|
|
|
|
/// <summary>Verifies that WriteSecured2Bulk forwards user ids, value, and timestamp per entry.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteSecured2Bulk_ForwardsUserIdsValueAndTimestampPerEntry()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 83);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
DateTime timestamp = new(2026, 5, 19, 13, 30, 0, DateTimeKind.Utc);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecured2BulkCommand(
|
|
"write-secured2-bulk",
|
|
serverHandle: 83,
|
|
new[] { (itemHandle: 831, currentUserId: 33, verifierUserId: 44, value: 999, timestamp) }));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
BulkWriteResult result = Assert.Single(reply.WriteSecured2Bulk.Results);
|
|
Assert.True(result.WasSuccessful);
|
|
Assert.Equal(33, fakeComObject.WriteCurrentUserId);
|
|
Assert.Equal(44, fakeComObject.WriteVerifierUserId);
|
|
Assert.Equal(999, fakeComObject.WriteValue);
|
|
Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
|
|
Assert.Contains("WriteSecured2:83:831", fakeComObject.OperationNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies ReadBulk's snapshot path: with no cached value, the worker takes
|
|
/// the AddItem + Advise + wait + UnAdvise + RemoveItem lifecycle itself, and
|
|
/// surfaces a timeout as a per-tag failure when no OnDataChange arrives.
|
|
/// The fake COM object never fires events so the wait always times out — but
|
|
/// the lifecycle calls must still happen, in order, on the STA.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_ReadBulk_WhenTagNotCached_TakesSnapshotLifecycleAndTimesOut()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(
|
|
registerHandle: 90,
|
|
addItemHandle: 900);
|
|
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-read-bulk", "client-a"));
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateReadBulkCommand(
|
|
"read-bulk-snapshot",
|
|
serverHandle: 90,
|
|
tagAddresses: new[] { "Galaxy.Tag.Value" },
|
|
timeoutMs: 80));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(MxCommandKind.ReadBulk, reply.Kind);
|
|
BulkReadResult result = Assert.Single(reply.ReadBulk.Results);
|
|
Assert.False(result.WasSuccessful);
|
|
Assert.False(result.WasCached);
|
|
Assert.Equal("Galaxy.Tag.Value", result.TagAddress);
|
|
Assert.Equal(900, result.ItemHandle);
|
|
Assert.Contains("timed out", result.ErrorMessage, StringComparison.OrdinalIgnoreCase);
|
|
|
|
// The snapshot lifecycle must call AddItem → Advise → UnAdvise → RemoveItem
|
|
// in order on the STA. We don't assert exact ordering of UnAdvise vs.
|
|
// RemoveItem here because both are best-effort cleanup in a finally
|
|
// block; the operation list confirms both happened.
|
|
Assert.Contains("AddItem:90:Galaxy.Tag.Value", fakeComObject.OperationNames);
|
|
Assert.Contains("Advise:90:900", fakeComObject.OperationNames);
|
|
Assert.Contains("UnAdvise:90:900", fakeComObject.OperationNames);
|
|
Assert.Contains("RemoveItem:90:900", fakeComObject.OperationNames);
|
|
}
|
|
|
|
/// <summary>Verifies that ReadBulk with no payload returns an invalid request error.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_ReadBulkWithoutPayload_ReturnsInvalidRequest()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 91);
|
|
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(new StaCommand(
|
|
"session-1",
|
|
"missing-read-bulk-payload",
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.ReadBulk,
|
|
}));
|
|
|
|
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
|
}
|
|
|
|
/// <summary>Verifies that UnsubscribeBulk removes items after UnAdvise failure.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that ShutdownGracefullyAsync cleans up handles in advice, item, server order.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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)));
|
|
}
|
|
|
|
/// <summary>Verifies that ShutdownGracefullyAsync records cleanup failures and continues.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that Register without payload returns an invalid request error.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that AddItem without payload returns an invalid request error.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that Advise without payload returns an invalid request error.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>Verifies that Write dispatches the converted value to MXAccess on the STA thread.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_Write_CallsMxAccessOnStaWithConvertedValue()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 70);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
|
// No completion source in this test — disable the bounded reply wait
|
|
// so the forwarding assertions don't pay the default 1.5 s timeout.
|
|
session.WriteCompletionTimeout = TimeSpan.Zero;
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteCommand(
|
|
"write", serverHandle: 70, itemHandle: 700, value: 123, userId: 5));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(MxCommandKind.Write, reply.Kind);
|
|
Assert.Equal(70, fakeComObject.WriteServerHandle);
|
|
Assert.Equal(700, fakeComObject.WriteItemHandle);
|
|
Assert.Equal(123, fakeComObject.WriteValue);
|
|
Assert.Equal(5, fakeComObject.WriteUserId);
|
|
Assert.Equal(runtime.StaThreadId, fakeComObject.WriteThreadId);
|
|
}
|
|
|
|
/// <summary>Verifies that Write2 forwards the converted value and timestamp to MXAccess.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_Write2_ForwardsValueAndTimestamp()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 71);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
|
// Same rationale as the Write forwarding test above.
|
|
session.WriteCompletionTimeout = TimeSpan.Zero;
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
DateTime timestamp = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWrite2Command(
|
|
"write2", serverHandle: 71, itemHandle: 710, value: 456, timestamp: timestamp, userId: 6));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(MxCommandKind.Write2, reply.Kind);
|
|
Assert.Equal(710, fakeComObject.WriteItemHandle);
|
|
Assert.Equal(456, fakeComObject.WriteValue);
|
|
Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
|
|
Assert.Equal(6, fakeComObject.WriteUserId);
|
|
}
|
|
|
|
/// <summary>Verifies that WriteSecured forwards the operator and verifier user ids to MXAccess.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteSecured_ForwardsUserIds()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 72);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
|
// No completion source in this test — disable the bounded reply wait
|
|
// so the forwarding assertions don't pay the default 1.5 s timeout.
|
|
session.WriteCompletionTimeout = TimeSpan.Zero;
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
|
|
"write-secured", serverHandle: 72, itemHandle: 720, value: 789, currentUserId: 11, verifierUserId: 22));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(MxCommandKind.WriteSecured, reply.Kind);
|
|
Assert.Equal(720, fakeComObject.WriteItemHandle);
|
|
Assert.Equal(789, fakeComObject.WriteValue);
|
|
Assert.Equal(11, fakeComObject.WriteCurrentUserId);
|
|
Assert.Equal(22, fakeComObject.WriteVerifierUserId);
|
|
}
|
|
|
|
/// <summary>Verifies that WriteSecured2 forwards user ids, value, and timestamp to MXAccess.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteSecured2_ForwardsUserIdsValueAndTimestamp()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 73);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
|
// Same rationale as the WriteSecured forwarding test above.
|
|
session.WriteCompletionTimeout = TimeSpan.Zero;
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
DateTime timestamp = new(2026, 5, 19, 13, 30, 0, DateTimeKind.Utc);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecured2Command(
|
|
"write-secured2", serverHandle: 73, itemHandle: 730, value: 1011,
|
|
timestamp: timestamp, currentUserId: 33, verifierUserId: 44));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(MxCommandKind.WriteSecured2, reply.Kind);
|
|
Assert.Equal(1011, fakeComObject.WriteValue);
|
|
Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
|
|
Assert.Equal(33, fakeComObject.WriteCurrentUserId);
|
|
Assert.Equal(44, fakeComObject.WriteVerifierUserId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the fast-completion ordering edge: a completion recorded while
|
|
/// the WriteSecured COM call is still on the stack (MXAccess committing
|
|
/// synchronously) is newer than the pre-call baseline and lands on the
|
|
/// reply — the wait never misses a callback that beat it.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteSecured_WhenCompletionArrivesDuringComCall_ReturnsStatuses()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 82);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
CompletionCacheEventSink sink = new();
|
|
fakeComObject.OnWriteCallback = () =>
|
|
sink.WriteCompletionCache.Record(82, 820, CreateCompletionRows(detail: 4321));
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, sink);
|
|
// Hermetic: don't inherit MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS
|
|
// from the test runner's environment.
|
|
session.WriteCompletionTimeout = TimeSpan.FromSeconds(10);
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
|
|
"write-secured-fast", serverHandle: 82, itemHandle: 820, value: 1, currentUserId: 11, verifierUserId: 22));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.True(reply.HasHresult);
|
|
Assert.Equal(0, reply.Hresult);
|
|
MxStatusProxy row = Assert.Single(reply.Statuses);
|
|
Assert.Equal(4321, row.Detail);
|
|
Assert.Equal(MxStatusCategory.Ok, row.Category);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the pump-wait path: the completion arrives after the COM call
|
|
/// returned, while the executor is pump-waiting, and still lands on the
|
|
/// reply.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteSecured_WhenCompletionArrivesWhileWaiting_ReturnsStatuses()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 83);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
CompletionCacheEventSink sink = new();
|
|
// Deterministic ordering: the executor captures its version baseline
|
|
// BEFORE the COM call, so once the fake's WriteSecured has run the
|
|
// baseline is committed and a Record from the test thread is
|
|
// guaranteed to be "newer" — no fixed sleep racing the STA thread.
|
|
using System.Threading.ManualResetEventSlim comCallReached = new(initialState: false);
|
|
fakeComObject.OnWriteCallback = () => comCallReached.Set();
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, sink);
|
|
session.WriteCompletionTimeout = TimeSpan.FromSeconds(10);
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
Task<MxCommandReply> pending = session.DispatchAsync(CreateWriteSecuredCommand(
|
|
"write-secured-waiting", serverHandle: 83, itemHandle: 830, value: 1, currentUserId: 11, verifierUserId: 22));
|
|
Assert.True(comCallReached.Wait(TimeSpan.FromSeconds(5)));
|
|
sink.WriteCompletionCache.Record(83, 830, CreateCompletionRows(detail: 99));
|
|
|
|
MxCommandReply reply = await pending;
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(99, Assert.Single(reply.Statuses).Detail);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the timeout fallback: no completion within the bounded wait
|
|
/// returns today's reply shape — protocol OK with EMPTY statuses (the
|
|
/// consumer's honest-unconfirmed path), never a synthesized failure row.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteSecured_WhenNoCompletion_TimesOutWithEmptyStatusesAndOkProtocol()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 84);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
CompletionCacheEventSink sink = new();
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, sink);
|
|
session.WriteCompletionTimeout = TimeSpan.FromMilliseconds(100);
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
|
|
"write-secured-timeout", serverHandle: 84, itemHandle: 840, value: 1, currentUserId: 11, verifierUserId: 22));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.True(reply.HasHresult);
|
|
Assert.Equal(0, reply.Hresult);
|
|
Assert.Empty(reply.Statuses);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the version-baseline rule end to end: a completion recorded
|
|
/// BEFORE the write was dispatched is stale and must not be misattributed
|
|
/// to this write — the reply times out empty instead.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteSecured_IgnoresStaleCompletionFromBeforeTheCall()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 85);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
CompletionCacheEventSink sink = new();
|
|
sink.WriteCompletionCache.Record(85, 850, CreateCompletionRows(detail: 1111));
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, sink);
|
|
session.WriteCompletionTimeout = TimeSpan.FromMilliseconds(100);
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
|
|
"write-secured-stale", serverHandle: 85, itemHandle: 850, value: 1, currentUserId: 11, verifierUserId: 22));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Empty(reply.Statuses);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that WriteSecured2 correlates the same way as WriteSecured
|
|
/// (fast-completion edge).
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteSecured2_WhenCompletionArrivesDuringComCall_ReturnsStatuses()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 86);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
CompletionCacheEventSink sink = new();
|
|
fakeComObject.OnWriteCallback = () =>
|
|
sink.WriteCompletionCache.Record(86, 860, CreateCompletionRows(detail: 2222));
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, sink);
|
|
// Hermetic: same rationale as the WriteSecured fast-completion test.
|
|
session.WriteCompletionTimeout = TimeSpan.FromSeconds(10);
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecured2Command(
|
|
"write-secured2-fast", serverHandle: 86, itemHandle: 860, value: 1,
|
|
timestamp: new DateTime(2026, 8, 9, 12, 0, 0, DateTimeKind.Utc), currentUserId: 33, verifierUserId: 44));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(2222, Assert.Single(reply.Statuses).Detail);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies plain Write correlates the same way as WriteSecured
|
|
/// (fast-completion edge): OtOpcUa's dominant FreeAccess path goes out
|
|
/// as MX_COMMAND_KIND_WRITE, so its reply must carry the OnWriteComplete
|
|
/// rows too.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_Write_WhenCompletionArrivesDuringComCall_ReturnsStatuses()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 87);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
CompletionCacheEventSink sink = new();
|
|
fakeComObject.OnWriteCallback = () =>
|
|
sink.WriteCompletionCache.Record(87, 870, CreateCompletionRows(detail: 5555));
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, sink);
|
|
// Hermetic: don't inherit MXGATEWAY_WORKER_WRITE_COMPLETION_WAIT_MS
|
|
// from the test runner's environment.
|
|
session.WriteCompletionTimeout = TimeSpan.FromSeconds(10);
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteCommand(
|
|
"plain-write-fast", serverHandle: 87, itemHandle: 870, value: 1, userId: 5));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.True(reply.HasHresult);
|
|
Assert.Equal(0, reply.Hresult);
|
|
MxStatusProxy row = Assert.Single(reply.Statuses);
|
|
Assert.Equal(5555, row.Detail);
|
|
Assert.Equal(MxStatusCategory.Ok, row.Category);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the plain-Write timeout fallback mirrors the secured one:
|
|
/// no completion within the bounded wait returns protocol OK with EMPTY
|
|
/// statuses — unconfirmed, never a synthesized failure row.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_Write_WhenNoCompletion_TimesOutWithEmptyStatusesAndOkProtocol()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 88);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
CompletionCacheEventSink sink = new();
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, sink);
|
|
session.WriteCompletionTimeout = TimeSpan.FromMilliseconds(100);
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteCommand(
|
|
"plain-write-timeout", serverHandle: 88, itemHandle: 880, value: 1, userId: 5));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.True(reply.HasHresult);
|
|
Assert.Equal(0, reply.Hresult);
|
|
Assert.Empty(reply.Statuses);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies Write2 correlates the same way as Write (fast-completion
|
|
/// edge).
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_Write2_WhenCompletionArrivesDuringComCall_ReturnsStatuses()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 89);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
CompletionCacheEventSink sink = new();
|
|
fakeComObject.OnWriteCallback = () =>
|
|
sink.WriteCompletionCache.Record(89, 890, CreateCompletionRows(detail: 6666));
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, sink);
|
|
session.WriteCompletionTimeout = TimeSpan.FromSeconds(10);
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateWrite2Command(
|
|
"plain-write2-fast", serverHandle: 89, itemHandle: 890, value: 1,
|
|
timestamp: new DateTime(2026, 8, 9, 12, 0, 0, DateTimeKind.Utc), userId: 6));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(6666, Assert.Single(reply.Statuses).Detail);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies WriteBulk stays fire-and-forget: even with a huge completion
|
|
/// timeout configured and no completion source, the reply returns
|
|
/// immediately with per-entry results — bulk writes never enter the
|
|
/// bounded wait (latency for high-rate loops).
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteBulk_DoesNotWaitForCompletion()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 90);
|
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
|
CompletionCacheEventSink sink = new();
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, sink);
|
|
session.WriteCompletionTimeout = TimeSpan.FromSeconds(30);
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
Task<MxCommandReply> pending = session.DispatchAsync(CreateWriteBulkCommand(
|
|
"bulk-write-no-wait", serverHandle: 90, entries: new[] { (itemHandle: 900, value: 1, userId: 5) }));
|
|
Task completed = await Task.WhenAny(pending, Task.Delay(TimeSpan.FromSeconds(5)));
|
|
|
|
Assert.Same(pending, completed);
|
|
MxCommandReply reply = await pending;
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Empty(reply.Statuses);
|
|
}
|
|
|
|
private static Google.Protobuf.Collections.RepeatedField<MxStatusProxy> CreateCompletionRows(int detail)
|
|
{
|
|
return new Google.Protobuf.Collections.RepeatedField<MxStatusProxy>
|
|
{
|
|
new MxStatusProxy
|
|
{
|
|
Success = 1,
|
|
Category = MxStatusCategory.Ok,
|
|
Detail = detail,
|
|
},
|
|
};
|
|
}
|
|
|
|
/// <summary>Verifies that Write without a payload returns an invalid request error.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteWithoutPayload_ReturnsInvalidRequest()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 74);
|
|
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(new StaCommand(
|
|
"session-1",
|
|
"missing-write-payload",
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write,
|
|
}));
|
|
|
|
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
|
Assert.Null(fakeComObject.WriteServerHandle);
|
|
}
|
|
|
|
/// <summary>Verifies that Write without a value returns an invalid request error.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WriteWithoutValue_ReturnsInvalidRequest()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 75);
|
|
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(new StaCommand(
|
|
"session-1",
|
|
"missing-write-value",
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write,
|
|
Write = new WriteCommand
|
|
{
|
|
ServerHandle = 75,
|
|
ItemHandle = 750,
|
|
},
|
|
}));
|
|
|
|
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
|
Assert.Null(fakeComObject.WriteServerHandle);
|
|
}
|
|
|
|
/// <summary>Verifies Suspend calls MXAccess on the STA and maps the native status to MxStatusProxy.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_Suspend_CallsMxAccessOnStaAndMapsStatus()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 200);
|
|
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-suspend", "client-a"));
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateSuspendCommand("suspend-1", 200, 21));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(0, reply.Hresult);
|
|
Assert.NotNull(reply.Suspend);
|
|
Assert.NotNull(reply.Suspend.Status);
|
|
Assert.Equal(1, reply.Suspend.Status.Success);
|
|
Assert.Equal(MxStatusCategory.Ok, reply.Suspend.Status.Category);
|
|
Assert.Equal(200, fakeComObject.SuspendServerHandle);
|
|
Assert.Equal(21, fakeComObject.SuspendItemHandle);
|
|
Assert.Equal(runtime.StaThreadId, fakeComObject.SuspendThreadId);
|
|
}
|
|
|
|
/// <summary>Verifies Activate calls MXAccess on the STA and maps the native status to MxStatusProxy.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_Activate_CallsMxAccessOnStaAndMapsStatus()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 201);
|
|
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-activate", "client-a"));
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(CreateActivateCommand("activate-1", 201, 22));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.NotNull(reply.Activate);
|
|
Assert.NotNull(reply.Activate.Status);
|
|
Assert.Equal(1, reply.Activate.Status.Success);
|
|
Assert.Equal(MxStatusCategory.Ok, reply.Activate.Status.Category);
|
|
Assert.Equal(201, fakeComObject.ActivateServerHandle);
|
|
Assert.Equal(22, fakeComObject.ActivateItemHandle);
|
|
Assert.Equal(runtime.StaThreadId, fakeComObject.ActivateThreadId);
|
|
}
|
|
|
|
/// <summary>Verifies AuthenticateUser passes credentials to MXAccess on the STA and returns the user id.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_AuthenticateUser_CallsMxAccessOnStaAndReturnsUserId()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 202);
|
|
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-auth", "client-a"));
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(
|
|
CreateAuthenticateUserCommand("auth-1", 202, "Administrator", string.Empty));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.NotNull(reply.AuthenticateUser);
|
|
Assert.Equal(1, reply.AuthenticateUser.UserId);
|
|
Assert.Equal(202, fakeComObject.AuthenticateServerHandle);
|
|
Assert.Equal("Administrator", fakeComObject.AuthenticateUserName);
|
|
Assert.Equal(runtime.StaThreadId, fakeComObject.AuthenticateThreadId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the AuthenticateUser path never surfaces the credential into the
|
|
/// command reply or any recorded diagnostic — the password is only ever
|
|
/// handed straight to the MXAccess wrapper.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_AuthenticateUser_DoesNotLeakPassword()
|
|
{
|
|
const string secret = "sup3r-secret-pw";
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 203);
|
|
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-auth-leak", "client-a"));
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(
|
|
CreateAuthenticateUserCommand("auth-leak", 203, "Administrator", secret));
|
|
|
|
// The wrapper still receives the credential verbatim...
|
|
Assert.Equal(secret, fakeComObject.AuthenticatePassword);
|
|
|
|
// ...but the reply (diagnostics, status text) and the fake's operation
|
|
// log must never contain it.
|
|
Assert.DoesNotContain(secret, reply.DiagnosticMessage ?? string.Empty, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(secret, reply.ProtocolStatus.Message ?? string.Empty, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(fakeComObject.OperationNames, name => name.Contains(secret, StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>Verifies ArchestrAUserToId calls MXAccess on the STA and returns the resolved user id.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_ArchestrAUserToId_CallsMxAccessOnStaAndReturnsUserId()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 204);
|
|
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-user-to-id", "client-a"));
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(
|
|
CreateArchestrAUserToIdCommand("user-to-id-1", 204, "11112222-3333-4444-5555-666677778888"));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.NotNull(reply.ArchestraUserToId);
|
|
Assert.Equal(7, reply.ArchestraUserToId.UserId);
|
|
Assert.Equal(204, fakeComObject.ArchestrAUserToIdServerHandle);
|
|
Assert.Equal("11112222-3333-4444-5555-666677778888", fakeComObject.ArchestrAUserToIdGuid);
|
|
}
|
|
|
|
/// <summary>Verifies AddBufferedItem calls MXAccess on the STA and tracks the buffered item handle.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_AddBufferedItem_CallsMxAccessOnStaAndTracksItemHandle()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 205);
|
|
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-buffered", "client-a"));
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(
|
|
CreateAddBufferedItemCommand("buffered-1", 205, "TestInt", "TestChildObject"));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.NotNull(reply.AddBufferedItem);
|
|
Assert.Equal(1, reply.AddBufferedItem.ItemHandle);
|
|
Assert.Equal(MxDataType.Integer, reply.ReturnValue.DataType);
|
|
Assert.Equal(1, reply.ReturnValue.Int32Value);
|
|
Assert.Equal(205, fakeComObject.AddBufferedItemServerHandle);
|
|
Assert.Equal("TestInt", fakeComObject.AddBufferedItemDefinition);
|
|
Assert.Equal("TestChildObject", fakeComObject.AddBufferedItemContext);
|
|
|
|
RegisteredItemHandle registeredItemHandle = Assert.Single(
|
|
await session.GetRegisteredItemHandlesAsync());
|
|
Assert.Equal(205, registeredItemHandle.ServerHandle);
|
|
Assert.Equal(1, registeredItemHandle.ItemHandle);
|
|
Assert.Equal("TestInt", registeredItemHandle.ItemDefinition);
|
|
Assert.Equal("TestChildObject", registeredItemHandle.ItemContext);
|
|
Assert.True(registeredItemHandle.HasItemContext);
|
|
}
|
|
|
|
/// <summary>Verifies SetBufferedUpdateInterval calls MXAccess on the STA and returns a base OK reply.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_SetBufferedUpdateInterval_CallsMxAccessOnStaAndReturnsOk()
|
|
{
|
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 206);
|
|
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-interval", "client-a"));
|
|
|
|
MxCommandReply reply = await session.DispatchAsync(
|
|
CreateSetBufferedUpdateIntervalCommand("interval-1", 206, 500));
|
|
|
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
|
Assert.Equal(0, reply.Hresult);
|
|
Assert.Equal(206, fakeComObject.SetBufferedUpdateIntervalServerHandle);
|
|
Assert.Equal(500, fakeComObject.SetBufferedUpdateIntervalValue);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that a command with an unknown <see cref="MxCommandKind"/> value returns an
|
|
/// <see cref="ProtocolStatusCode.InvalidRequest"/> reply whose diagnostic contains "Unsupported".
|
|
/// This pins the <c>_ => CreateInvalidRequestReply(...)</c> discard arm in
|
|
/// <c>MxAccessCommandExecutor.Execute</c>: a regression that changed the arm to
|
|
/// <c>throw</c> would propagate an unhandled exception through <c>WorkerPipeSession</c>
|
|
/// and no other test would catch it.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
[Fact]
|
|
public async Task DispatchAsync_WithUnknownCommandKind_ReturnsInvalidRequestWithUnsupportedDiagnostic()
|
|
{
|
|
FakeMxAccessComObjectFactory factory = new(new FakeMxAccessComObject(registerHandle: 999));
|
|
using StaRuntime runtime = CreateRuntime();
|
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
|
await session.StartAsync(workerProcessId: 1234);
|
|
|
|
// Cast an integer outside the defined MxCommandKind range to an unknown kind value.
|
|
MxCommandKind unknownKind = (MxCommandKind)int.MaxValue;
|
|
MxCommandReply reply = await session.DispatchAsync(new StaCommand(
|
|
"session-1",
|
|
"unknown-kind-correlation",
|
|
new MxCommand
|
|
{
|
|
Kind = unknownKind,
|
|
}));
|
|
|
|
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
|
Assert.Contains("Unsupported", reply.DiagnosticMessage, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static StaCommand CreateSuspendCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
int itemHandle)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Suspend,
|
|
Suspend = new SuspendCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateActivateCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
int itemHandle)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Activate,
|
|
Activate = new ActivateCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
},
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateAuthenticateUserCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
string verifyUser,
|
|
string verifyUserPassword)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AuthenticateUser,
|
|
AuthenticateUser = new AuthenticateUserCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
VerifyUser = verifyUser,
|
|
VerifyUserPassword = verifyUserPassword,
|
|
},
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateArchestrAUserToIdCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
string userIdGuid)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.ArchestraUserToId,
|
|
ArchestraUserToId = new ArchestrAUserToIdCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
UserIdGuid = userIdGuid,
|
|
},
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateAddBufferedItemCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
string itemContext)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.AddBufferedItem,
|
|
AddBufferedItem = new AddBufferedItemCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemDefinition = itemDefinition,
|
|
ItemContext = itemContext,
|
|
},
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateSetBufferedUpdateIntervalCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
int updateIntervalMilliseconds)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.SetBufferedUpdateInterval,
|
|
SetBufferedUpdateInterval = new SetBufferedUpdateIntervalCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
UpdateIntervalMilliseconds = updateIntervalMilliseconds,
|
|
},
|
|
});
|
|
}
|
|
|
|
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 MxValue CreateIntegerValue(int value)
|
|
{
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Integer,
|
|
VariantType = "VT_I4",
|
|
Int32Value = value,
|
|
};
|
|
}
|
|
|
|
private static MxValue CreateTimestampValue(DateTime timestamp)
|
|
{
|
|
return new MxValue
|
|
{
|
|
DataType = MxDataType.Time,
|
|
VariantType = "VT_DATE",
|
|
TimestampValue = Timestamp.FromDateTime(timestamp),
|
|
};
|
|
}
|
|
|
|
private static StaCommand CreateWriteCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int value,
|
|
int userId)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write,
|
|
Write = new WriteCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
Value = CreateIntegerValue(value),
|
|
UserId = userId,
|
|
},
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateWrite2Command(
|
|
string correlationId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int value,
|
|
DateTime timestamp,
|
|
int userId)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write2,
|
|
Write2 = new Write2Command
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
Value = CreateIntegerValue(value),
|
|
TimestampValue = CreateTimestampValue(timestamp),
|
|
UserId = userId,
|
|
},
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateWriteSecuredCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int value,
|
|
int currentUserId,
|
|
int verifierUserId)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecured,
|
|
WriteSecured = new WriteSecuredCommand
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
CurrentUserId = currentUserId,
|
|
VerifierUserId = verifierUserId,
|
|
Value = CreateIntegerValue(value),
|
|
},
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateWriteSecured2Command(
|
|
string correlationId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int value,
|
|
DateTime timestamp,
|
|
int currentUserId,
|
|
int verifierUserId)
|
|
{
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecured2,
|
|
WriteSecured2 = new WriteSecured2Command
|
|
{
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
CurrentUserId = currentUserId,
|
|
VerifierUserId = verifierUserId,
|
|
Value = CreateIntegerValue(value),
|
|
TimestampValue = CreateTimestampValue(timestamp),
|
|
},
|
|
});
|
|
}
|
|
|
|
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<string> 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<int> 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 CreateWriteBulkCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
IEnumerable<(int itemHandle, int value, int userId)> entries)
|
|
{
|
|
WriteBulkCommand command = new()
|
|
{
|
|
ServerHandle = serverHandle,
|
|
};
|
|
foreach ((int itemHandle, int value, int userId) in entries)
|
|
{
|
|
command.Entries.Add(new WriteBulkEntry
|
|
{
|
|
ItemHandle = itemHandle,
|
|
Value = CreateIntegerValue(value),
|
|
UserId = userId,
|
|
});
|
|
}
|
|
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteBulk,
|
|
WriteBulk = command,
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateWrite2BulkCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
IEnumerable<(int itemHandle, int value, DateTime timestamp, int userId)> entries)
|
|
{
|
|
Write2BulkCommand command = new()
|
|
{
|
|
ServerHandle = serverHandle,
|
|
};
|
|
foreach ((int itemHandle, int value, DateTime timestamp, int userId) in entries)
|
|
{
|
|
command.Entries.Add(new Write2BulkEntry
|
|
{
|
|
ItemHandle = itemHandle,
|
|
Value = CreateIntegerValue(value),
|
|
TimestampValue = CreateTimestampValue(timestamp),
|
|
UserId = userId,
|
|
});
|
|
}
|
|
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.Write2Bulk,
|
|
Write2Bulk = command,
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateWriteSecuredBulkCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
IEnumerable<(int itemHandle, int currentUserId, int verifierUserId, int value)> entries)
|
|
{
|
|
WriteSecuredBulkCommand command = new()
|
|
{
|
|
ServerHandle = serverHandle,
|
|
};
|
|
foreach ((int itemHandle, int currentUserId, int verifierUserId, int value) in entries)
|
|
{
|
|
command.Entries.Add(new WriteSecuredBulkEntry
|
|
{
|
|
ItemHandle = itemHandle,
|
|
CurrentUserId = currentUserId,
|
|
VerifierUserId = verifierUserId,
|
|
Value = CreateIntegerValue(value),
|
|
});
|
|
}
|
|
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecuredBulk,
|
|
WriteSecuredBulk = command,
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateWriteSecured2BulkCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
IEnumerable<(int itemHandle, int currentUserId, int verifierUserId, int value, DateTime timestamp)> entries)
|
|
{
|
|
WriteSecured2BulkCommand command = new()
|
|
{
|
|
ServerHandle = serverHandle,
|
|
};
|
|
foreach ((int itemHandle, int currentUserId, int verifierUserId, int value, DateTime timestamp) in entries)
|
|
{
|
|
command.Entries.Add(new WriteSecured2BulkEntry
|
|
{
|
|
ItemHandle = itemHandle,
|
|
CurrentUserId = currentUserId,
|
|
VerifierUserId = verifierUserId,
|
|
Value = CreateIntegerValue(value),
|
|
TimestampValue = CreateTimestampValue(timestamp),
|
|
});
|
|
}
|
|
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.WriteSecured2Bulk,
|
|
WriteSecured2Bulk = command,
|
|
});
|
|
}
|
|
|
|
private static StaCommand CreateReadBulkCommand(
|
|
string correlationId,
|
|
int serverHandle,
|
|
IEnumerable<string> tagAddresses,
|
|
uint timeoutMs)
|
|
{
|
|
ReadBulkCommand command = new()
|
|
{
|
|
ServerHandle = serverHandle,
|
|
TimeoutMs = timeoutMs,
|
|
};
|
|
command.TagAddresses.Add(tagAddresses);
|
|
|
|
return new StaCommand(
|
|
"session-1",
|
|
correlationId,
|
|
new MxCommand
|
|
{
|
|
Kind = MxCommandKind.ReadBulk,
|
|
ReadBulk = 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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test sink that owns a real write-completion cache without touching the
|
|
/// MXAccess COM RCW (Attach is a no-op). Implements the provider seam so
|
|
/// <see cref="MxAccessSession.Create"/> shares this cache with the write
|
|
/// executor, letting tests record completions the executor's bounded
|
|
/// wait then observes.
|
|
/// </summary>
|
|
private sealed class CompletionCacheEventSink : IMxAccessEventSink, IWriteCompletionCacheProvider
|
|
{
|
|
public MxAccessWriteCompletionCache WriteCompletionCache { get; } = new MxAccessWriteCompletionCache();
|
|
|
|
public void Attach(
|
|
object mxAccessComObject,
|
|
string sessionId)
|
|
{
|
|
}
|
|
|
|
public void Detach()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class FakeMxAccessComObject : IMxAccessServer
|
|
{
|
|
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 IReadOnlyDictionary<int, Exception> writeExceptionByItemHandle;
|
|
private readonly List<string> operationNames = new();
|
|
|
|
/// <summary>
|
|
/// Invoked at the end of a successful Write/Write2/WriteSecured/
|
|
/// WriteSecured2 — stands in for MXAccess committing synchronously
|
|
/// and delivering OnWriteComplete while the COM call is still on
|
|
/// the stack, so tests can exercise the fast-completion ordering
|
|
/// edge on every correlated write kind.
|
|
/// </summary>
|
|
public Action? OnWriteCallback { get; set; }
|
|
|
|
/// <summary>Initializes a fake MXAccess COM object with the given handles and optional exceptions.</summary>
|
|
/// <param name="registerHandle">Return value for Register method.</param>
|
|
/// <param name="addItemHandle">Return value for AddItem method.</param>
|
|
/// <param name="addItem2Handle">Return value for AddItem2 method.</param>
|
|
/// <param name="unregisterException">Exception to throw from Unregister, if any.</param>
|
|
/// <param name="addItemException">Exception to throw from AddItem, if any.</param>
|
|
/// <param name="addItem2Exception">Exception to throw from AddItem2, if any.</param>
|
|
/// <param name="removeItemException">Exception to throw from RemoveItem, if any.</param>
|
|
/// <param name="adviseException">Exception to throw from Advise, if any.</param>
|
|
/// <param name="unAdviseException">Exception to throw from UnAdvise, if any.</param>
|
|
/// <param name="adviseSupervisoryException">Exception to throw from AdviseSupervisory, if any.</param>
|
|
/// <param name="writeExceptionByItemHandle">Map of item handles to exceptions thrown on write.</param>
|
|
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,
|
|
IReadOnlyDictionary<int, Exception>? writeExceptionByItemHandle = 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;
|
|
this.writeExceptionByItemHandle = writeExceptionByItemHandle
|
|
?? new Dictionary<int, Exception>();
|
|
}
|
|
|
|
/// <summary>Gets the client name passed to Register, if called.</summary>
|
|
public string? RegisteredClientName { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which Register was called.</summary>
|
|
public int? RegisterThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to Unregister, if called.</summary>
|
|
public int? UnregisteredServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which Unregister was called.</summary>
|
|
public int? UnregisterThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to AddItem, if called.</summary>
|
|
public int? AddItemServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item definition passed to AddItem, if called.</summary>
|
|
public string? AddItemDefinition { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which AddItem was called.</summary>
|
|
public int? AddItemThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to AddItem2, if called.</summary>
|
|
public int? AddItem2ServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item definition passed to AddItem2, if called.</summary>
|
|
public string? AddItem2Definition { get; private set; }
|
|
|
|
/// <summary>Gets the item context passed to AddItem2, if called.</summary>
|
|
public string? AddItem2Context { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which AddItem2 was called.</summary>
|
|
public int? AddItem2ThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to RemoveItem, if called.</summary>
|
|
public int? RemoveItemServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item handle passed to RemoveItem, if called.</summary>
|
|
public int? RemovedItemHandle { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which RemoveItem was called.</summary>
|
|
public int? RemoveItemThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to Advise, if called.</summary>
|
|
public int? AdviseServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item handle passed to Advise, if called.</summary>
|
|
public int? AdvisedItemHandle { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which Advise was called.</summary>
|
|
public int? AdviseThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to UnAdvise, if called.</summary>
|
|
public int? UnAdviseServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item handle passed to UnAdvise, if called.</summary>
|
|
public int? UnAdvisedItemHandle { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which UnAdvise was called.</summary>
|
|
public int? UnAdviseThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to AdviseSupervisory, if called.</summary>
|
|
public int? AdviseSupervisoryServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item handle passed to AdviseSupervisory, if called.</summary>
|
|
public int? AdviseSupervisoryItemHandle { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which AdviseSupervisory was called.</summary>
|
|
public int? AdviseSupervisoryThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the list of operations performed on this fake object.</summary>
|
|
public IReadOnlyList<string> OperationNames => operationNames.ToArray();
|
|
|
|
/// <inheritdoc />
|
|
public int Register(string clientName)
|
|
{
|
|
operationNames.Add($"Register:{clientName}");
|
|
RegisteredClientName = clientName;
|
|
RegisterThreadId = Environment.CurrentManagedThreadId;
|
|
|
|
return registerHandle;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Unregister(int serverHandle)
|
|
{
|
|
operationNames.Add($"Unregister:{serverHandle}");
|
|
UnregisteredServerHandle = serverHandle;
|
|
UnregisterThreadId = Environment.CurrentManagedThreadId;
|
|
|
|
if (unregisterException is not null)
|
|
{
|
|
throw unregisterException;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets the server handle passed to the most recent write, if called.</summary>
|
|
public int? WriteServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item handle passed to the most recent write, if called.</summary>
|
|
public int? WriteItemHandle { get; private set; }
|
|
|
|
/// <summary>Gets the value passed to the most recent write, if called.</summary>
|
|
public object? WriteValue { get; private set; }
|
|
|
|
/// <summary>Gets the timestamp passed to the most recent timestamped write, if called.</summary>
|
|
public object? WriteTimestamp { get; private set; }
|
|
|
|
/// <summary>Gets the user id passed to the most recent Write/Write2, if called.</summary>
|
|
public int? WriteUserId { get; private set; }
|
|
|
|
/// <summary>Gets the current user id passed to the most recent secured write, if called.</summary>
|
|
public int? WriteCurrentUserId { get; private set; }
|
|
|
|
/// <summary>Gets the verifier user id passed to the most recent secured write, if called.</summary>
|
|
public int? WriteVerifierUserId { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which the most recent write was called.</summary>
|
|
public int? WriteThreadId { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public void Write(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
object? value,
|
|
int userId)
|
|
{
|
|
operationNames.Add($"Write:{serverHandle}:{itemHandle}");
|
|
WriteServerHandle = serverHandle;
|
|
WriteItemHandle = itemHandle;
|
|
WriteValue = value;
|
|
WriteUserId = userId;
|
|
WriteThreadId = Environment.CurrentManagedThreadId;
|
|
ThrowIfWriteFailureConfigured(itemHandle);
|
|
OnWriteCallback?.Invoke();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Write2(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
object? value,
|
|
object? timestamp,
|
|
int userId)
|
|
{
|
|
operationNames.Add($"Write2:{serverHandle}:{itemHandle}");
|
|
WriteServerHandle = serverHandle;
|
|
WriteItemHandle = itemHandle;
|
|
WriteValue = value;
|
|
WriteTimestamp = timestamp;
|
|
WriteUserId = userId;
|
|
WriteThreadId = Environment.CurrentManagedThreadId;
|
|
ThrowIfWriteFailureConfigured(itemHandle);
|
|
OnWriteCallback?.Invoke();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void WriteSecured(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int currentUserId,
|
|
int verifierUserId,
|
|
object? value)
|
|
{
|
|
operationNames.Add($"WriteSecured:{serverHandle}:{itemHandle}");
|
|
WriteServerHandle = serverHandle;
|
|
WriteItemHandle = itemHandle;
|
|
WriteCurrentUserId = currentUserId;
|
|
WriteVerifierUserId = verifierUserId;
|
|
WriteValue = value;
|
|
WriteThreadId = Environment.CurrentManagedThreadId;
|
|
ThrowIfWriteFailureConfigured(itemHandle);
|
|
OnWriteCallback?.Invoke();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void WriteSecured2(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int currentUserId,
|
|
int verifierUserId,
|
|
object? value,
|
|
object? timestamp)
|
|
{
|
|
operationNames.Add($"WriteSecured2:{serverHandle}:{itemHandle}");
|
|
WriteServerHandle = serverHandle;
|
|
WriteItemHandle = itemHandle;
|
|
WriteCurrentUserId = currentUserId;
|
|
WriteVerifierUserId = verifierUserId;
|
|
WriteValue = value;
|
|
WriteTimestamp = timestamp;
|
|
WriteThreadId = Environment.CurrentManagedThreadId;
|
|
ThrowIfWriteFailureConfigured(itemHandle);
|
|
OnWriteCallback?.Invoke();
|
|
}
|
|
|
|
private void ThrowIfWriteFailureConfigured(int itemHandle)
|
|
{
|
|
// Per-item write-failure injection — used by the bulk-write tests to
|
|
// exercise the "one bad entry surfaces as was_successful=false but
|
|
// the loop keeps going" contract on BulkWriteResult.
|
|
if (writeExceptionByItemHandle.TryGetValue(itemHandle, out Exception? exception))
|
|
{
|
|
throw exception;
|
|
}
|
|
}
|
|
|
|
/// <summary>Gets the server handle passed to Suspend, if called.</summary>
|
|
public int? SuspendServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item handle passed to Suspend, if called.</summary>
|
|
public int? SuspendItemHandle { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which Suspend was called.</summary>
|
|
public int? SuspendThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to Activate, if called.</summary>
|
|
public int? ActivateServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item handle passed to Activate, if called.</summary>
|
|
public int? ActivateItemHandle { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which Activate was called.</summary>
|
|
public int? ActivateThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to AuthenticateUser, if called.</summary>
|
|
public int? AuthenticateServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the user name passed to AuthenticateUser, if called.</summary>
|
|
public string? AuthenticateUserName { get; private set; }
|
|
|
|
/// <summary>Gets the credential passed to AuthenticateUser, if called. Used only to prove non-logging.</summary>
|
|
public string? AuthenticatePassword { get; private set; }
|
|
|
|
/// <summary>Gets the thread ID on which AuthenticateUser was called.</summary>
|
|
public int? AuthenticateThreadId { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to ArchestrAUserToId, if called.</summary>
|
|
public int? ArchestrAUserToIdServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the GUID passed to ArchestrAUserToId, if called.</summary>
|
|
public string? ArchestrAUserToIdGuid { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to AddBufferedItem, if called.</summary>
|
|
public int? AddBufferedItemServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the item definition passed to AddBufferedItem, if called.</summary>
|
|
public string? AddBufferedItemDefinition { get; private set; }
|
|
|
|
/// <summary>Gets the item context passed to AddBufferedItem, if called.</summary>
|
|
public string? AddBufferedItemContext { get; private set; }
|
|
|
|
/// <summary>Gets the server handle passed to SetBufferedUpdateInterval, if called.</summary>
|
|
public int? SetBufferedUpdateIntervalServerHandle { get; private set; }
|
|
|
|
/// <summary>Gets the interval passed to SetBufferedUpdateInterval, if called.</summary>
|
|
public int? SetBufferedUpdateIntervalValue { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public object Suspend(int serverHandle, int itemHandle)
|
|
{
|
|
operationNames.Add($"Suspend:{serverHandle}:{itemHandle}");
|
|
SuspendServerHandle = serverHandle;
|
|
SuspendItemHandle = itemHandle;
|
|
SuspendThreadId = Environment.CurrentManagedThreadId;
|
|
return new FakeMxStatus { success = 1, category = 0, detectedBy = 0, detail = 0 };
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public object Activate(int serverHandle, int itemHandle)
|
|
{
|
|
operationNames.Add($"Activate:{serverHandle}:{itemHandle}");
|
|
ActivateServerHandle = serverHandle;
|
|
ActivateItemHandle = itemHandle;
|
|
ActivateThreadId = Environment.CurrentManagedThreadId;
|
|
return new FakeMxStatus { success = 1, category = 0, detectedBy = 0, detail = 0 };
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
|
|
{
|
|
// Deliberately does NOT include the password in the operation log.
|
|
operationNames.Add($"AuthenticateUser:{serverHandle}:{verifyUser}");
|
|
AuthenticateServerHandle = serverHandle;
|
|
AuthenticateUserName = verifyUser;
|
|
AuthenticatePassword = verifyUserPassword;
|
|
AuthenticateThreadId = Environment.CurrentManagedThreadId;
|
|
return 1;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public int ArchestrAUserToId(int serverHandle, string userIdGuid)
|
|
{
|
|
operationNames.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}");
|
|
ArchestrAUserToIdServerHandle = serverHandle;
|
|
ArchestrAUserToIdGuid = userIdGuid;
|
|
return 7;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
|
|
{
|
|
operationNames.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}");
|
|
AddBufferedItemServerHandle = serverHandle;
|
|
AddBufferedItemDefinition = itemDefinition;
|
|
AddBufferedItemContext = itemContext;
|
|
return 1;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
|
|
{
|
|
operationNames.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}");
|
|
SetBufferedUpdateIntervalServerHandle = serverHandle;
|
|
SetBufferedUpdateIntervalValue = updateIntervalMilliseconds;
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>Factory for creating fake MXAccess COM objects in tests.</summary>
|
|
private sealed class FakeMxAccessComObjectFactory : IMxAccessComObjectFactory
|
|
{
|
|
/// <summary>Initializes a new instance of the FakeMxAccessComObjectFactory class.</summary>
|
|
/// <param name="fakeComObject">The fake COM object to return from Create.</param>
|
|
public FakeMxAccessComObjectFactory(FakeMxAccessComObject fakeComObject)
|
|
{
|
|
FakeComObject = fakeComObject;
|
|
}
|
|
|
|
/// <summary>Gets the fake COM object.</summary>
|
|
public FakeMxAccessComObject FakeComObject { get; }
|
|
|
|
/// <inheritdoc />
|
|
public object Create()
|
|
{
|
|
return FakeComObject;
|
|
}
|
|
}
|
|
|
|
}
|