using System; using System.Collections.Generic; using ZB.MOM.WW.MxGateway.Worker.MxAccess; namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess; /// /// Regression tests for . The /// adapter no longer falls back to late-bound Type.InvokeMember /// reflection: a COM object must implement either the typed /// ILMXProxyServer COM interface family (production) or /// directly (test fakes). /// public sealed class MxAccessComServerTests { /// /// A COM object implementing is routed /// through the typed interface — no reflection — preserving arguments /// and return values. /// [Fact] public void Methods_WithTypedServer_RouteThroughTypedInterface() { RecordingMxAccessServer typed = new(registerHandle: 77); MxAccessComServer adapter = new(typed); int serverHandle = adapter.Register("client-a"); adapter.Advise(serverHandle, itemHandle: 9); adapter.Unregister(serverHandle); Assert.Equal(77, serverHandle); Assert.Equal("client-a", typed.RegisteredClientName); Assert.Equal(new[] { "Register:client-a", "Advise:77:9", "Unregister:77" }, typed.Calls); } /// /// The MXAccess command methods added in the worker COM commands bundle /// (Suspend/Activate/AuthenticateUser/ArchestrAUserToId/AddBufferedItem/ /// SetBufferedUpdateInterval) route through the typed interface with their /// arguments preserved, and the credential is never echoed back. /// [Fact] public void CommandMethods_WithTypedServer_RouteThroughTypedInterface() { RecordingMxAccessServer typed = new(registerHandle: 5); MxAccessComServer adapter = new(typed); adapter.Suspend(serverHandle: 5, itemHandle: 11); adapter.Activate(serverHandle: 5, itemHandle: 12); adapter.AuthenticateUser(serverHandle: 5, verifyUser: "Administrator", verifyUserPassword: "s3cret"); adapter.ArchestrAUserToId(serverHandle: 5, userIdGuid: "guid-1"); adapter.AddBufferedItem(serverHandle: 5, itemDefinition: "TestInt", itemContext: "TestChildObject"); adapter.SetBufferedUpdateInterval(serverHandle: 5, updateIntervalMilliseconds: 250); Assert.Equal( new[] { "Suspend:5:11", "Activate:5:12", "AuthenticateUser:5:Administrator", "ArchestrAUserToId:5:guid-1", "AddBufferedItem:5:TestInt:TestChildObject", "SetBufferedUpdateInterval:5:250", }, typed.Calls); Assert.DoesNotContain(typed.Calls, call => call.Contains("s3cret", StringComparison.Ordinal)); } /// /// A COM object that implements neither the typed COM interface family /// nor fails fast with a clear /// instead of a late-bound /// reflection call. /// [Fact] public void Methods_WithUntypedObject_ThrowInvalidOperation() { MxAccessComServer adapter = new(new object()); InvalidOperationException exception = Assert.Throws(() => adapter.Register("client")); Assert.Contains("does not implement", exception.Message, StringComparison.Ordinal); Assert.Contains(nameof(IMxAccessServer), exception.Message, StringComparison.Ordinal); } /// /// Exceptions thrown by the typed server propagate unchanged — no /// TargetInvocationException wrapping (reflection is gone). /// [Fact] public void Methods_WhenTypedServerThrows_PropagateOriginalException() { RecordingMxAccessServer typed = new(registerHandle: 1) { ThrowOnRegister = new InvalidOperationException("register failed"), }; MxAccessComServer adapter = new(typed); InvalidOperationException exception = Assert.Throws(() => adapter.Register("client")); Assert.Equal("register failed", exception.Message); } private sealed class RecordingMxAccessServer : IMxAccessServer { private readonly int registerHandle; private readonly List calls = new(); /// Initializes a new instance with the specified register handle. /// The initial server handle value to return from Register. public RecordingMxAccessServer(int registerHandle) { this.registerHandle = registerHandle; } /// Gets the client name passed to the most recent Register call. public string? RegisteredClientName { get; private set; } /// Gets or sets an exception to throw from the Register method. public Exception? ThrowOnRegister { get; set; } /// Gets the recorded method calls as strings. public IReadOnlyList Calls => calls.ToArray(); /// public int Register(string clientName) { calls.Add($"Register:{clientName}"); RegisteredClientName = clientName; if (ThrowOnRegister is not null) { throw ThrowOnRegister; } return registerHandle; } /// public void Unregister(int serverHandle) { calls.Add($"Unregister:{serverHandle}"); } /// public int AddItem(int serverHandle, string itemDefinition) { calls.Add($"AddItem:{serverHandle}:{itemDefinition}"); return 0; } /// public int AddItem2(int serverHandle, string itemDefinition, string itemContext) { calls.Add($"AddItem2:{serverHandle}:{itemDefinition}:{itemContext}"); return 0; } /// public void RemoveItem(int serverHandle, int itemHandle) { calls.Add($"RemoveItem:{serverHandle}:{itemHandle}"); } /// public void Advise(int serverHandle, int itemHandle) { calls.Add($"Advise:{serverHandle}:{itemHandle}"); } /// public void UnAdvise(int serverHandle, int itemHandle) { calls.Add($"UnAdvise:{serverHandle}:{itemHandle}"); } /// public void AdviseSupervisory(int serverHandle, int itemHandle) { calls.Add($"AdviseSupervisory:{serverHandle}:{itemHandle}"); } /// public void Write(int serverHandle, int itemHandle, object? value, int userId) { calls.Add($"Write:{serverHandle}:{itemHandle}:{value}:{userId}"); } /// public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId) { calls.Add($"Write2:{serverHandle}:{itemHandle}:{value}:{timestamp}:{userId}"); } /// public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value) { calls.Add($"WriteSecured:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}"); } /// public void WriteSecured2( int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp) { calls.Add($"WriteSecured2:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}:{timestamp}"); } /// public object Suspend(int serverHandle, int itemHandle) { calls.Add($"Suspend:{serverHandle}:{itemHandle}"); return new object(); } /// public object Activate(int serverHandle, int itemHandle) { calls.Add($"Activate:{serverHandle}:{itemHandle}"); return new object(); } /// public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) { calls.Add($"AuthenticateUser:{serverHandle}:{verifyUser}"); return 0; } /// public int ArchestrAUserToId(int serverHandle, string userIdGuid) { calls.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}"); return 0; } /// public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext) { calls.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}"); return 0; } /// public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) { calls.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}"); } } }