using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Google.Protobuf.Collections; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.MxGateway.Worker.MxAccess; namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess; public sealed class AlarmAckWriterTests { private static AlarmAckOperator Alice() => new AlarmAckOperator { UserName = "alice", Domain = "CORP", Password = "pw-secret" }; private static RepeatedField Rows(MxStatusCategory category, int detail) => new RepeatedField { new MxStatusProxy { Success = category == MxStatusCategory.Ok ? 1 : 0, Category = category, Detail = detail } }; [Fact] public void Acknowledge_AuthenticatesThenWritesCommentWithOperatorUserId() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; MxAccessWriteCompletionCache cache = new MxAccessWriteCompletionCache(); server.OnWrite = (handle, item) => cache.Record(handle, item, Rows(MxStatusCategory.Ok, 0)); using AlarmAckWriter writer = new AlarmAckWriter(server, cache, () => { }, TimeSpan.FromSeconds(1), "MxGateway"); AlarmAckResult result = writer.Acknowledge("T.A.AckMsg", "checked", Alice()); Assert.Equal(AlarmAckOutcome.Applied, result.Outcome); Assert.Equal(0, result.NativeStatus); Assert.Equal(new[] { "Register:MxGateway", "AuthenticateUser:CORP\\alice", "AddItem:T.A.AckMsg", "Advise", "Write:checked:5" }, server.Operations); Assert.Equal("pw-secret", server.LastPassword); } [Fact] public void Acknowledge_WhenWriteRejected_ReturnsDetailAndStatuses() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; MxAccessWriteCompletionCache cache = new MxAccessWriteCompletionCache(); server.OnWrite = (handle, item) => cache.Record(handle, item, Rows(MxStatusCategory.SecurityError, 1008)); using AlarmAckWriter writer = new AlarmAckWriter(server, cache, () => { }, TimeSpan.FromSeconds(1), "MxGateway"); AlarmAckResult result = writer.Acknowledge("T.A.AckMsg", "checked", Alice()); Assert.Equal(AlarmAckOutcome.WriteRejected, result.Outcome); Assert.Equal(1008, result.NativeStatus); Assert.Single(result.Statuses); } [Fact] public void Acknowledge_WhenNoCompletion_ReturnsUnconfirmed() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(50), "MxGateway"); AlarmAckResult result = writer.Acknowledge("T.A.AckMsg", "checked", Alice()); Assert.Equal(AlarmAckOutcome.Unconfirmed, result.Outcome); Assert.Equal(0, result.NativeStatus); } [Theory] [InlineData(true)] [InlineData(false)] public void Acknowledge_WhenAuthenticationFails_DoesNotWriteOrLeakPassword(bool throwComException) { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 0, AuthenticateException = throwComException ? new COMException("bad credentials", unchecked((int)0x80070005)) : null, }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(50), "MxGateway"); AlarmAckResult result = writer.Acknowledge("T.A.AckMsg", "checked", Alice()); Assert.Equal(AlarmAckOutcome.AuthenticationFailed, result.Outcome); Assert.NotEqual(0, result.NativeStatus); Assert.DoesNotContain(server.Operations, op => op.StartsWith("Write", StringComparison.Ordinal)); Assert.DoesNotContain("pw-secret", result.Diagnostic, StringComparison.Ordinal); Assert.DoesNotContain(server.Operations, op => op.Contains("pw-secret")); } [Fact] public void Acknowledge_Twice_ReusesItemHandle() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(10), "MxGateway"); writer.Acknowledge("T.A.AckMsg", "one", Alice()); writer.Acknowledge("T.A.AckMsg", "two", Alice()); Assert.Equal(1, server.Operations.Count(op => op.StartsWith("AddItem", StringComparison.Ordinal))); Assert.Equal(1, server.Operations.Count(op => op == "Advise")); } [Fact] public void AlarmAckOperator_ToString_OmitsPassword() { Assert.DoesNotContain("pw-secret", Alice().ToString(), StringComparison.Ordinal); } } internal sealed class RecordingAckServer : IMxAccessServer { private int nextItemHandle = 100; public List Operations { get; } = new(); public int AuthenticateResult { get; set; } public Exception? AuthenticateException { get; set; } public string? LastPassword { get; private set; } public Action? OnWrite { get; set; } public int Register(string clientName) { Operations.Add("Register:" + clientName); return 7; } public void Unregister(int serverHandle) => Operations.Add("Unregister"); public int AddItem(int serverHandle, string itemDefinition) { Operations.Add("AddItem:" + itemDefinition); return nextItemHandle++; } public int AddItem2(int serverHandle, string itemDefinition, string itemContext) => AddItem(serverHandle, itemDefinition); public void RemoveItem(int serverHandle, int itemHandle) => Operations.Add("RemoveItem"); public void Advise(int serverHandle, int itemHandle) => Operations.Add("Advise"); public void UnAdvise(int serverHandle, int itemHandle) => Operations.Add("UnAdvise"); public void AdviseSupervisory(int serverHandle, int itemHandle) { } public void Write(int serverHandle, int itemHandle, object? value, int userId) { Operations.Add("Write:" + value + ":" + userId); OnWrite?.Invoke(serverHandle, itemHandle); } public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId) { } public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value) { } public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp) { } 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) { Operations.Add("AuthenticateUser:" + verifyUser); LastPassword = verifyUserPassword; if (AuthenticateException is not null) { throw AuthenticateException; } return AuthenticateResult; } 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) { } }