diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs index 08995f9..57c0f81 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs @@ -259,6 +259,21 @@ public sealed class LmxSubtagAlarmSourceTests { } + public object Suspend(int serverHandle, int itemHandle) => new object(); + + public object Activate(int serverHandle, int itemHandle) => new object(); + + public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) => 0; + + public int ArchestrAUserToId(int serverHandle, string userIdGuid) => 0; + + public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext) + => AddItem(serverHandle, itemDefinition); + + public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) + { + } + internal sealed class WriteRecord { public WriteRecord(int serverHandle, int itemHandle, object? value, int userId) diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs index e821cac..5a795b0 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs @@ -33,6 +33,39 @@ public sealed class MxAccessComServerTests 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 @@ -207,5 +240,60 @@ public sealed class MxAccessComServerTests { calls.Add($"WriteSecured2:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}:{timestamp}"); } + + /// Records a Suspend call and returns a canned status. + /// The MXAccess server handle. + /// The MXAccess item handle. + public object Suspend(int serverHandle, int itemHandle) + { + calls.Add($"Suspend:{serverHandle}:{itemHandle}"); + return new object(); + } + + /// Records an Activate call and returns a canned status. + /// The MXAccess server handle. + /// The MXAccess item handle. + public object Activate(int serverHandle, int itemHandle) + { + calls.Add($"Activate:{serverHandle}:{itemHandle}"); + return new object(); + } + + /// Records an AuthenticateUser call and returns zero. + /// The MXAccess server handle. + /// The user name to authenticate. + /// The credential; recorded only as a fixed marker, never echoed. + public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) + { + calls.Add($"AuthenticateUser:{serverHandle}:{verifyUser}"); + return 0; + } + + /// Records an ArchestrAUserToId call and returns zero. + /// The MXAccess server handle. + /// The ArchestrA user GUID to resolve. + public int ArchestrAUserToId(int serverHandle, string userIdGuid) + { + calls.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}"); + return 0; + } + + /// Records an AddBufferedItem call and returns zero. + /// The MXAccess server handle. + /// The item definition string to record. + /// The item context string to record. + public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext) + { + calls.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}"); + return 0; + } + + /// Records a SetBufferedUpdateInterval call. + /// The MXAccess server handle. + /// The buffered update interval in milliseconds. + public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) + { + calls.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}"); + } } } diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs index f9d19fa..2bcf8c1 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs @@ -952,6 +952,295 @@ public sealed class MxAccessCommandExecutorTests Assert.Null(fakeComObject.WriteServerHandle); } + /// Verifies Suspend calls MXAccess on the STA and maps the native status to MxStatusProxy. + [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); + } + + /// Verifies Activate calls MXAccess on the STA and maps the native status to MxStatusProxy. + [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); + } + + /// Verifies AuthenticateUser passes credentials to MXAccess on the STA and returns the user id. + [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); + } + + /// + /// 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. + /// + [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)); + } + + /// Verifies ArchestrAUserToId calls MXAccess on the STA and returns the resolved user id. + [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); + } + + /// Verifies AddBufferedItem calls MXAccess on the STA and tracks the buffered item handle. + [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); + } + + /// Verifies SetBufferedUpdateInterval calls MXAccess on the STA and returns a base OK reply. + [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); + } + + 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) @@ -1810,6 +2099,151 @@ public sealed class MxAccessCommandExecutorTests throw exception; } } + + /// Gets the server handle passed to Suspend, if called. + public int? SuspendServerHandle { get; private set; } + + /// Gets the item handle passed to Suspend, if called. + public int? SuspendItemHandle { get; private set; } + + /// Gets the thread ID on which Suspend was called. + public int? SuspendThreadId { get; private set; } + + /// Gets the server handle passed to Activate, if called. + public int? ActivateServerHandle { get; private set; } + + /// Gets the item handle passed to Activate, if called. + public int? ActivateItemHandle { get; private set; } + + /// Gets the thread ID on which Activate was called. + public int? ActivateThreadId { get; private set; } + + /// Gets the server handle passed to AuthenticateUser, if called. + public int? AuthenticateServerHandle { get; private set; } + + /// Gets the user name passed to AuthenticateUser, if called. + public string? AuthenticateUserName { get; private set; } + + /// Gets the credential passed to AuthenticateUser, if called. Used only to prove non-logging. + public string? AuthenticatePassword { get; private set; } + + /// Gets the thread ID on which AuthenticateUser was called. + public int? AuthenticateThreadId { get; private set; } + + /// Gets the server handle passed to ArchestrAUserToId, if called. + public int? ArchestrAUserToIdServerHandle { get; private set; } + + /// Gets the GUID passed to ArchestrAUserToId, if called. + public string? ArchestrAUserToIdGuid { get; private set; } + + /// Gets the server handle passed to AddBufferedItem, if called. + public int? AddBufferedItemServerHandle { get; private set; } + + /// Gets the item definition passed to AddBufferedItem, if called. + public string? AddBufferedItemDefinition { get; private set; } + + /// Gets the item context passed to AddBufferedItem, if called. + public string? AddBufferedItemContext { get; private set; } + + /// Gets the server handle passed to SetBufferedUpdateInterval, if called. + public int? SetBufferedUpdateIntervalServerHandle { get; private set; } + + /// Gets the interval passed to SetBufferedUpdateInterval, if called. + public int? SetBufferedUpdateIntervalValue { get; private set; } + + /// Suspends an item and returns a canned status whose fields drive MxStatusProxy conversion. + /// Server handle for the suspend. + /// Item handle to suspend. + /// A status stand-in with all-OK fields. + 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 }; + } + + /// Activates an item and returns a canned status whose fields drive MxStatusProxy conversion. + /// Server handle for the activate. + /// Item handle to activate. + /// A status stand-in with all-OK fields. + 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 }; + } + + /// Authenticates a user and returns a canned user id. + /// Server handle for the authentication. + /// User name to authenticate. + /// Credential; recorded only to assert it is never logged. + /// The canned MXAccess user id (1). + 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; + } + + /// Resolves an ArchestrA user GUID and returns a canned user id. + /// Server handle for the resolution. + /// ArchestrA user GUID to resolve. + /// The canned MXAccess user id (7). + public int ArchestrAUserToId(int serverHandle, string userIdGuid) + { + operationNames.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}"); + ArchestrAUserToIdServerHandle = serverHandle; + ArchestrAUserToIdGuid = userIdGuid; + return 7; + } + + /// Adds a buffered item and returns a canned item handle. + /// Server handle to add the item to. + /// Item definition string. + /// Item context string. + /// The canned buffered item handle (1). + public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext) + { + operationNames.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}"); + AddBufferedItemServerHandle = serverHandle; + AddBufferedItemDefinition = itemDefinition; + AddBufferedItemContext = itemContext; + return 1; + } + + /// Sets the buffered update interval and tracks the operation. + /// Server handle for the interval change. + /// Buffered update interval in milliseconds. + public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) + { + operationNames.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}"); + SetBufferedUpdateIntervalServerHandle = serverHandle; + SetBufferedUpdateIntervalValue = updateIntervalMilliseconds; + } + + /// Status stand-in reflected over by the worker's MxStatusProxy converter. + internal sealed class FakeMxStatus + { + /// Success indicator read by the status converter. + public int success; + + /// Status category read by the status converter. + public int category; + + /// Status detected-by read by the status converter. + public int detectedBy; + + /// Status detail read by the status converter. + public int detail; + } } /// Factory for creating fake MXAccess COM objects in tests. diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs index 1754ed2..b2964a3 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs @@ -1,3 +1,4 @@ +using ZB.MOM.WW.MxGateway.Worker.Conversion; using ZB.MOM.WW.MxGateway.Worker.MxAccess; namespace ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport; @@ -55,14 +56,10 @@ internal sealed class NoopMxAccessServer : IMxAccessServer } /// - public void Suspend(int serverHandle, int itemHandle) - { - } + public object Suspend(int serverHandle, int itemHandle) => new FakeMxStatus(); /// - public void Activate(int serverHandle, int itemHandle) - { - } + public object Activate(int serverHandle, int itemHandle) => new FakeMxStatus(); /// public void Write(int serverHandle, int itemHandle, object? value, int userId) @@ -85,8 +82,29 @@ internal sealed class NoopMxAccessServer : IMxAccessServer } /// - public int AuthenticateUser(string userName, string password) => 0; + public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) => 0; /// - public int ArchestrAUserToId(string userName) => 0; + public int ArchestrAUserToId(int serverHandle, string userIdGuid) => 0; +} + +/// +/// Minimal stand-in for the native ArchestrA.MxAccess.MxStatus struct. +/// reflects over the public +/// success, category, detectedBy, and detail +/// fields, so this fake exposes the same field shape with all-OK values. +/// +internal sealed class FakeMxStatus +{ + /// Success indicator field read by the status converter. + public int success; + + /// Status category field read by the status converter. + public int category; + + /// Status detected-by field read by the status converter. + public int detectedBy; + + /// Status detail field read by the status converter. + public int detail; } diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessServer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessServer.cs index ed5beb3..62f7d0d 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessServer.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessServer.cs @@ -57,6 +57,68 @@ public interface IMxAccessServer int serverHandle, int itemHandle); + /// Suspends data acquisition for an advised item (ILMXProxyServer4). + /// Server handle identifying the registration. + /// Item handle to suspend. + /// + /// The native MXAccess MxStatus value (boxed) produced by the call. + /// Callers convert it to a protobuf MxStatusProxy via the worker's + /// status converter; the underlying type is reflected over, not cast. + /// + object Suspend( + int serverHandle, + int itemHandle); + + /// Reactivates data acquisition for a suspended item (ILMXProxyServer4). + /// Server handle identifying the registration. + /// Item handle to activate. + /// + /// The native MXAccess MxStatus value (boxed) produced by the call. + /// Callers convert it to a protobuf MxStatusProxy via the worker's + /// status converter; the underlying type is reflected over, not cast. + /// + object Activate( + int serverHandle, + int itemHandle); + + /// Authenticates an MXAccess user and returns its user id (base ILMXProxyServer). + /// Server handle identifying the registration. + /// MXAccess user name to authenticate. + /// + /// Raw MXAccess credential. Implementations must keep this value out of + /// logs, metrics, command lines, and diagnostics. + /// + /// The MXAccess user id for the authenticated user. + int AuthenticateUser( + int serverHandle, + string verifyUser, + string verifyUserPassword); + + /// Resolves an ArchestrA user GUID to an MXAccess user id (ILMXProxyServer2). + /// Server handle identifying the registration. + /// ArchestrA user GUID to resolve. + /// The MXAccess user id for the resolved user. + int ArchestrAUserToId( + int serverHandle, + string userIdGuid); + + /// Adds a buffered item to a server and returns an item handle (ILMXProxyServer5). + /// Server handle identifying the registration. + /// Item definition string. + /// Item context string. + /// Item handle for the added buffered item. + int AddBufferedItem( + int serverHandle, + string itemDefinition, + string itemContext); + + /// Sets the buffered-update interval for a server (ILMXProxyServer5). + /// Server handle identifying the registration. + /// Buffered update interval in milliseconds. + void SetBufferedUpdateInterval( + int serverHandle, + int updateIntervalMilliseconds); + /// Writes a value to an item. /// Server handle identifying the registration. /// Item handle to write to. diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessComServer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessComServer.cs index db159c1..117cfdf 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessComServer.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessComServer.cs @@ -140,6 +140,89 @@ public sealed class MxAccessComServer : IMxAccessServer AsProxyServer4().AdviseSupervisory(serverHandle, itemHandle); } + /// + public object Suspend( + int serverHandle, + int itemHandle) + { + if (mxAccessComObject is IMxAccessServer typedFake) + { + return typedFake.Suspend(serverHandle, itemHandle); + } + + AsProxyServer4().Suspend(serverHandle, itemHandle, out MxStatus status); + return status; + } + + /// + public object Activate( + int serverHandle, + int itemHandle) + { + if (mxAccessComObject is IMxAccessServer typedFake) + { + return typedFake.Activate(serverHandle, itemHandle); + } + + AsProxyServer4().Activate(serverHandle, itemHandle, out MxStatus status); + return status; + } + + /// + public int AuthenticateUser( + int serverHandle, + string verifyUser, + string verifyUserPassword) + { + if (mxAccessComObject is IMxAccessServer typedFake) + { + return typedFake.AuthenticateUser(serverHandle, verifyUser, verifyUserPassword); + } + + return AsProxyServer().AuthenticateUser(serverHandle, verifyUser, verifyUserPassword); + } + + /// + public int ArchestrAUserToId( + int serverHandle, + string userIdGuid) + { + if (mxAccessComObject is IMxAccessServer typedFake) + { + return typedFake.ArchestrAUserToId(serverHandle, userIdGuid); + } + + return AsProxyServer2().ArchestrAUserToId(serverHandle, userIdGuid); + } + + /// + public int AddBufferedItem( + int serverHandle, + string itemDefinition, + string itemContext) + { + if (mxAccessComObject is IMxAccessServer typedFake) + { + return typedFake.AddBufferedItem(serverHandle, itemDefinition, itemContext); + } + + return AsProxyServer5().AddBufferedItem(serverHandle, itemDefinition, itemContext); + } + + /// + public void SetBufferedUpdateInterval( + int serverHandle, + int updateIntervalMilliseconds) + { + if (mxAccessComObject is IMxAccessServer typedFake) + { + typedFake.SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds); + return; + } + + AsProxyServer5().SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds); + } + /// public void Write( int serverHandle, @@ -216,6 +299,14 @@ public sealed class MxAccessComServer : IMxAccessServer + $"{nameof(ILMXProxyServer)} or {nameof(IMxAccessServer)}."); } + private ILMXProxyServer2 AsProxyServer2() + { + return mxAccessComObject as ILMXProxyServer2 + ?? throw new InvalidOperationException( + $"MXAccess COM object of type '{mxAccessComObject.GetType().FullName}' does not implement " + + $"{nameof(ILMXProxyServer2)} or {nameof(IMxAccessServer)}."); + } + private ILMXProxyServer3 AsProxyServer3() { return mxAccessComObject as ILMXProxyServer3 @@ -231,4 +322,12 @@ public sealed class MxAccessComServer : IMxAccessServer $"MXAccess COM object of type '{mxAccessComObject.GetType().FullName}' does not implement " + $"{nameof(ILMXProxyServer4)} or {nameof(IMxAccessServer)}."); } + + private ILMXProxyServer5 AsProxyServer5() + { + return mxAccessComObject as ILMXProxyServer5 + ?? throw new InvalidOperationException( + $"MXAccess COM object of type '{mxAccessComObject.GetType().FullName}' does not implement " + + $"{nameof(ILMXProxyServer5)} or {nameof(IMxAccessServer)}."); + } } diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs index 1dfd718..325a9cc 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs @@ -16,6 +16,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor private readonly MxAccessSession session; private readonly VariantConverter variantConverter; + private readonly MxStatusProxyConverter statusProxyConverter; private readonly IAlarmCommandHandler? alarmCommandHandler; private readonly Action pumpStep; @@ -78,6 +79,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor { this.session = session ?? throw new ArgumentNullException(nameof(session)); this.variantConverter = variantConverter ?? throw new ArgumentNullException(nameof(variantConverter)); + this.statusProxyConverter = new MxStatusProxyConverter(); this.alarmCommandHandler = alarmCommandHandler; this.pumpStep = pumpStep ?? (static () => { }); } @@ -104,6 +106,12 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor MxCommandKind.Advise => ExecuteAdvise(command), MxCommandKind.UnAdvise => ExecuteUnAdvise(command), MxCommandKind.AdviseSupervisory => ExecuteAdviseSupervisory(command), + MxCommandKind.Suspend => ExecuteSuspend(command), + MxCommandKind.Activate => ExecuteActivate(command), + MxCommandKind.AuthenticateUser => ExecuteAuthenticateUser(command), + MxCommandKind.ArchestraUserToId => ExecuteArchestrAUserToId(command), + MxCommandKind.AddBufferedItem => ExecuteAddBufferedItem(command), + MxCommandKind.SetBufferedUpdateInterval => ExecuteSetBufferedUpdateInterval(command), MxCommandKind.Write => ExecuteWrite(command), MxCommandKind.Write2 => ExecuteWrite2(command), MxCommandKind.WriteSecured => ExecuteWriteSecured(command), @@ -262,6 +270,134 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor return CreateOkReply(command); } + private MxCommandReply ExecuteSuspend(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Suspend) + { + return CreateInvalidRequestReply(command, "Suspend command payload is required."); + } + + SuspendCommand suspendCommand = command.Command.Suspend; + object nativeStatus = session.Suspend( + suspendCommand.ServerHandle, + suspendCommand.ItemHandle); + + MxCommandReply reply = CreateOkReply(command); + reply.Suspend = new SuspendReply + { + Status = statusProxyConverter.Convert(nativeStatus), + }; + + return reply; + } + + private MxCommandReply ExecuteActivate(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Activate) + { + return CreateInvalidRequestReply(command, "Activate command payload is required."); + } + + ActivateCommand activateCommand = command.Command.Activate; + object nativeStatus = session.Activate( + activateCommand.ServerHandle, + activateCommand.ItemHandle); + + MxCommandReply reply = CreateOkReply(command); + reply.Activate = new ActivateReply + { + Status = statusProxyConverter.Convert(nativeStatus), + }; + + return reply; + } + + private MxCommandReply ExecuteAuthenticateUser(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AuthenticateUser) + { + return CreateInvalidRequestReply(command, "AuthenticateUser command payload is required."); + } + + AuthenticateUserCommand authenticateUserCommand = command.Command.AuthenticateUser; + + // The credential (verify_user_password) is passed straight to MXAccess + // and is never written to logs, diagnostics, or the reply. MXAccess is + // allowed to fail authentication; the native HResult is surfaced by the + // dispatcher's exception path. + int userId = session.AuthenticateUser( + authenticateUserCommand.ServerHandle, + authenticateUserCommand.VerifyUser, + authenticateUserCommand.VerifyUserPassword); + + MxCommandReply reply = CreateOkReply(command); + reply.AuthenticateUser = new AuthenticateUserReply + { + UserId = userId, + }; + + return reply; + } + + private MxCommandReply ExecuteArchestrAUserToId(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.ArchestraUserToId) + { + return CreateInvalidRequestReply(command, "ArchestrAUserToId command payload is required."); + } + + ArchestrAUserToIdCommand archestrAUserToIdCommand = command.Command.ArchestraUserToId; + int userId = session.ArchestrAUserToId( + archestrAUserToIdCommand.ServerHandle, + archestrAUserToIdCommand.UserIdGuid); + + MxCommandReply reply = CreateOkReply(command); + reply.ArchestraUserToId = new ArchestrAUserToIdReply + { + UserId = userId, + }; + + return reply; + } + + private MxCommandReply ExecuteAddBufferedItem(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AddBufferedItem) + { + return CreateInvalidRequestReply(command, "AddBufferedItem command payload is required."); + } + + AddBufferedItemCommand addBufferedItemCommand = command.Command.AddBufferedItem; + int itemHandle = session.AddBufferedItem( + addBufferedItemCommand.ServerHandle, + addBufferedItemCommand.ItemDefinition, + addBufferedItemCommand.ItemContext); + + MxCommandReply reply = CreateOkReply(command); + reply.ReturnValue = variantConverter.Convert(itemHandle); + reply.AddBufferedItem = new AddBufferedItemReply + { + ItemHandle = itemHandle, + }; + + return reply; + } + + private MxCommandReply ExecuteSetBufferedUpdateInterval(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.SetBufferedUpdateInterval) + { + return CreateInvalidRequestReply(command, "SetBufferedUpdateInterval command payload is required."); + } + + SetBufferedUpdateIntervalCommand setBufferedUpdateIntervalCommand = command.Command.SetBufferedUpdateInterval; + session.SetBufferedUpdateInterval( + setBufferedUpdateIntervalCommand.ServerHandle, + setBufferedUpdateIntervalCommand.UpdateIntervalMilliseconds); + + return CreateOkReply(command); + } + private MxCommandReply ExecuteWrite(StaCommand command) { if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Write) diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs index c89c667..77940b1 100644 --- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs +++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs @@ -300,6 +300,94 @@ public sealed class MxAccessSession : IDisposable MxAccessAdviceKind.Supervisory); } + /// Suspends data acquisition for an advised item and returns the native MXAccess status. + /// Handle returned by the worker. + /// Handle returned by the worker. + /// The boxed native MXAccess status produced by the call. + public object Suspend( + int serverHandle, + int itemHandle) + { + ThrowIfDisposed(); + + return mxAccessServer.Suspend(serverHandle, itemHandle); + } + + /// Reactivates data acquisition for a suspended item and returns the native MXAccess status. + /// Handle returned by the worker. + /// Handle returned by the worker. + /// The boxed native MXAccess status produced by the call. + public object Activate( + int serverHandle, + int itemHandle) + { + ThrowIfDisposed(); + + return mxAccessServer.Activate(serverHandle, itemHandle); + } + + /// Authenticates an MXAccess user and returns its user id. + /// Handle returned by the worker. + /// MXAccess user name to authenticate. + /// Raw MXAccess credential; never logged. + /// The MXAccess user id for the authenticated user. + public int AuthenticateUser( + int serverHandle, + string verifyUser, + string verifyUserPassword) + { + ThrowIfDisposed(); + + return mxAccessServer.AuthenticateUser(serverHandle, verifyUser, verifyUserPassword); + } + + /// Resolves an ArchestrA user GUID to an MXAccess user id. + /// Handle returned by the worker. + /// ArchestrA user GUID to resolve. + /// The MXAccess user id for the resolved user. + public int ArchestrAUserToId( + int serverHandle, + string userIdGuid) + { + ThrowIfDisposed(); + + return mxAccessServer.ArchestrAUserToId(serverHandle, userIdGuid); + } + + /// Adds a buffered item to an MXAccess server and returns the item handle. + /// Handle returned by the worker. + /// Definition or address of the item to add. + /// Context string for the item. + public int AddBufferedItem( + int serverHandle, + string itemDefinition, + string itemContext) + { + ThrowIfDisposed(); + + int itemHandle = mxAccessServer.AddBufferedItem(serverHandle, itemDefinition, itemContext); + handleRegistry.RegisterItemHandle( + serverHandle, + itemHandle, + itemDefinition, + itemContext, + hasItemContext: true); + + return itemHandle; + } + + /// Sets the buffered-update interval for an MXAccess server. + /// Handle returned by the worker. + /// Buffered update interval in milliseconds. + public void SetBufferedUpdateInterval( + int serverHandle, + int updateIntervalMilliseconds) + { + ThrowIfDisposed(); + + mxAccessServer.SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds); + } + /// Writes a value to an item. /// Handle returned by the worker. /// Handle returned by the worker.