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 }; 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"); 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 Acknowledge_WhenAdviseThrows_RejectsCleansUpAndReAddsNextTime() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5, AdviseException = new COMException("x", unchecked((int)0x80004005)) }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(10), "MxGateway"); AlarmAckResult first = writer.Acknowledge("T.A.AckMsg", "one", Alice()); server.AdviseException = null; writer.Acknowledge("T.A.AckMsg", "two", Alice()); Assert.Equal(AlarmAckOutcome.WriteRejected, first.Outcome); Assert.Equal(unchecked((int)0x80004005), first.NativeStatus); Assert.DoesNotContain("pw-secret", first.Diagnostic, StringComparison.Ordinal); Assert.Contains("RemoveItem", server.Operations); Assert.Equal(2, server.Operations.Count(op => op.StartsWith("AddItem", StringComparison.Ordinal))); } [Fact] public void Acknowledge_WhenCachedWriteThrows_RejectsAndReAddsNextTime() { 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"); writer.Acknowledge("T.A.AckMsg", "one", Alice()); server.WriteException = new COMException("x", unchecked((int)0x80004005)); AlarmAckResult failed = writer.Acknowledge("T.A.AckMsg", "two", Alice()); server.WriteException = null; writer.Acknowledge("T.A.AckMsg", "three", Alice()); Assert.Equal(AlarmAckOutcome.WriteRejected, failed.Outcome); Assert.Equal(2, server.Operations.Count(op => op.StartsWith("AddItem", StringComparison.Ordinal))); } [Fact] public void Acknowledge_WhenWriteDisconnected_TearsDownServer() { // The test seam has no COM factory, so re-registration itself is not // exercised here; this verifies the teardown that makes it possible. RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5, WriteException = new COMException("x", unchecked((int)0x80010108)) }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(10), "MxGateway"); AlarmAckResult result = writer.Acknowledge("T.A.AckMsg", "one", Alice()); Assert.Equal(AlarmAckOutcome.WriteRejected, result.Outcome); Assert.Equal(unchecked((int)0x80010108), result.NativeStatus); Assert.Contains("Unregister", server.Operations); } [Fact] public void Acknowledge_WhenAuthenticateUserDisconnected_TearsDownAndRejects() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5, AuthenticateException = new COMException("gone pw-secret", unchecked((int)0x80010108)), }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(10), "MxGateway"); AlarmAckResult result = writer.Acknowledge("T.A.AckMsg", "one", Alice()); Assert.Equal(AlarmAckOutcome.WriteRejected, result.Outcome); Assert.Equal(unchecked((int)0x80010108), result.NativeStatus); Assert.Contains("AuthenticateUser", result.Diagnostic, StringComparison.Ordinal); Assert.Contains("0x80010108", result.Diagnostic, StringComparison.Ordinal); Assert.DoesNotContain("pw-secret", result.Diagnostic, StringComparison.Ordinal); Assert.Contains("Unregister", server.Operations); Assert.DoesNotContain(server.Operations, op => op.StartsWith("AddItem", StringComparison.Ordinal)); } [Fact] public void Acknowledge_WhenAuthenticateUserThrowsArgumentException_ReturnsAuthenticationFailed() { // Live MXAccess raises E_INVALIDARG for a bad user or wrong password, which the // interop surfaces as ArgumentException rather than COMException. RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5, AuthenticateException = new ArgumentException("Value does not fall within the expected range. pw-secret"), }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(10), "MxGateway"); AlarmAckResult result = writer.Acknowledge("T.A.AckMsg", "one", Alice()); Assert.Equal(AlarmAckOutcome.AuthenticationFailed, result.Outcome); Assert.Equal(unchecked((int)0x80070057), result.NativeStatus); Assert.DoesNotContain("pw-secret", result.Diagnostic, StringComparison.Ordinal); Assert.DoesNotContain(server.Operations, op => op.StartsWith("Write", StringComparison.Ordinal)); Assert.DoesNotContain("Unregister", server.Operations); } [Fact] public void Acknowledge_WhenAuthenticateUserThrowsArgumentNullException_Propagates() { // A programming error is a bug, not an operator authentication failure. RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5, AuthenticateException = new ArgumentNullException("verifyUser"), }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(10), "MxGateway"); Assert.Throws(() => writer.Acknowledge("T.A.AckMsg", "one", Alice())); } [Fact] public void Acknowledge_WhenOwnCodeOutsideComCallThrowsArgumentException_Propagates() { // The pump runs between Advise and Write. An ArgumentException from it carries // the same default HRESULT (0x80070057) as an interop E_INVALIDARG, so only // catching at the COM call itself keeps it from being reported as WriteRejected. RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => throw new ArgumentException("bug"), TimeSpan.FromMilliseconds(10), "MxGateway"); ArgumentException thrown = Assert.Throws(() => writer.Acknowledge("T.A.AckMsg", "one", Alice())); Assert.Equal("bug", thrown.Message); Assert.DoesNotContain(server.Operations, op => op.StartsWith("Write", StringComparison.Ordinal)); } [Fact] public void Acknowledge_WhenWriteThrowsArgumentOutOfRangeException_Propagates() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5, WriteException = new ArgumentOutOfRangeException("value") }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(10), "MxGateway"); Assert.Throws(() => writer.Acknowledge("T.A.AckMsg", "one", Alice())); } [Fact] public void Acknowledge_WhenOperatorUserNameIsNull_ThrowsBeforeAuthenticating() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(10), "MxGateway"); Assert.Throws(() => writer.Acknowledge("T.A.AckMsg", "one", new AlarmAckOperator { UserName = null! })); Assert.DoesNotContain(server.Operations, op => op.StartsWith("AuthenticateUser", StringComparison.Ordinal)); } [Fact] public void Acknowledge_WhenWriteThrowsArgumentException_RejectsAndDropsItem() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5, WriteException = new ArgumentException("bad") }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(10), "MxGateway"); AlarmAckResult result = writer.Acknowledge("T.A.AckMsg", "one", Alice()); Assert.Equal(AlarmAckOutcome.WriteRejected, result.Outcome); Assert.Equal(unchecked((int)0x80070057), result.NativeStatus); Assert.Contains("Write", result.Diagnostic, StringComparison.Ordinal); Assert.Equal(new[] { 100 }, server.RemovedItemHandles); Assert.DoesNotContain("Unregister", server.Operations); } [Fact] public void Acknowledge_PastCacheCap_EvictsLeastRecentlyUsedItem() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; using AlarmAckWriter writer = new AlarmAckWriter(server, new MxAccessWriteCompletionCache(), () => { }, TimeSpan.FromMilliseconds(1), "MxGateway", maxCachedItems: 2); writer.Acknowledge("A.X.AckMsg", "1", Alice()); // handle 100 writer.Acknowledge("B.X.AckMsg", "2", Alice()); // handle 101 writer.Acknowledge("C.X.AckMsg", "3", Alice()); // handle 102, evicts A (100) Assert.Equal(new[] { 100 }, server.RemovedItemHandles); Assert.Equal(3, server.Operations.Count(op => op.StartsWith("AddItem", StringComparison.Ordinal))); } [Fact] public void Acknowledge_ReusedItem_IsMostRecentlyUsedForEviction() { 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", maxCachedItems: 2); writer.Acknowledge("A.X.AckMsg", "1", Alice()); // handle 100 writer.Acknowledge("B.X.AckMsg", "2", Alice()); // handle 101 writer.Acknowledge("A.X.AckMsg", "3", Alice()); // reuse 100, now most recent writer.Acknowledge("C.X.AckMsg", "4", Alice()); // handle 102, evicts B (101) Assert.Equal(new[] { 101 }, server.RemovedItemHandles); Assert.Equal(3, server.Operations.Count(op => op.StartsWith("AddItem", StringComparison.Ordinal))); Assert.Contains("UnAdvise", server.Operations); } [Fact] public void Acknowledge_AfterUnconfirmed_UsesFreshHandleSoStaleCompletionIsNotCredited() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; MxAccessWriteCompletionCache cache = new MxAccessWriteCompletionCache(); using AlarmAckWriter writer = new AlarmAckWriter(server, cache, () => { }, TimeSpan.FromMilliseconds(50), "MxGateway"); AlarmAckResult first = writer.Acknowledge("T.A.AckMsg", "one", Alice()); // handle 100, no completion // The first write's late OnWriteComplete (handle 100) lands while the // second write is in flight; the second write's own completion never comes. server.OnWrite = (handle, item) => cache.Record(handle, 100, Rows(MxStatusCategory.Ok, 0)); AlarmAckResult second = writer.Acknowledge("T.A.AckMsg", "two", Alice()); Assert.Equal(AlarmAckOutcome.Unconfirmed, first.Outcome); Assert.Equal(AlarmAckOutcome.Unconfirmed, second.Outcome); Assert.Contains(100, server.RemovedItemHandles); Assert.Equal(2, server.Operations.Count(op => op.StartsWith("AddItem", StringComparison.Ordinal))); Assert.Equal(new[] { 100, 101 }, server.WrittenItemHandles); } [Fact] public void Acknowledge_AfterConfirmed_ReusesHandle() { 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"); writer.Acknowledge("T.A.AckMsg", "one", Alice()); writer.Acknowledge("T.A.AckMsg", "two", Alice()); Assert.Equal(new[] { 100, 100 }, server.WrittenItemHandles); Assert.Empty(server.RemovedItemHandles); } [Fact] public void Acknowledge_AfterDirtyReAdd_ForgetsDroppedHandleCompletion() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; MxAccessWriteCompletionCache cache = new MxAccessWriteCompletionCache(); using AlarmAckWriter writer = new AlarmAckWriter(server, cache, () => { }, TimeSpan.FromMilliseconds(20), "MxGateway"); writer.Acknowledge("T.A.AckMsg", "one", Alice()); // handle 100, Unconfirmed cache.Record(7, 100, Rows(MxStatusCategory.Ok, 0)); // its late completion Assert.Equal(1, cache.Count); server.OnWrite = (handle, item) => cache.Record(handle, item, Rows(MxStatusCategory.Ok, 0)); AlarmAckResult second = writer.Acknowledge("T.A.AckMsg", "two", Alice()); // drops 100, adds 101 Assert.Equal(AlarmAckOutcome.Applied, second.Outcome); Assert.Equal(1, cache.Count); Assert.Equal(0UL, cache.CurrentVersion(7, 100)); Assert.Equal(1UL, cache.CurrentVersion(7, 101)); } [Fact] public void LateCompletionForDroppedHandle_DoesNotRecreateCacheEntry() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; MxAccessWriteCompletionCache cache = new MxAccessWriteCompletionCache(); using AlarmAckWriter writer = new AlarmAckWriter(server, cache, () => { }, TimeSpan.FromMilliseconds(20), "MxGateway"); writer.Acknowledge("T.A.AckMsg", "one", Alice()); // handle 100, Unconfirmed server.OnWrite = (handle, item) => writer.RecordWriteCompletion(handle, item, Rows(MxStatusCategory.Ok, 0)); writer.Acknowledge("T.A.AckMsg", "two", Alice()); // drops 100, adds 101, applied writer.RecordWriteCompletion(7, 100, Rows(MxStatusCategory.Ok, 0)); // 100's late completion writer.RecordWriteCompletion(8, 101, Rows(MxStatusCategory.Ok, 0)); // another registration Assert.Equal(1, cache.Count); Assert.Equal(0UL, cache.CurrentVersion(7, 100)); Assert.Equal(1UL, cache.CurrentVersion(7, 101)); } [Fact] public void Acknowledge_PastCacheCap_ForgetsEvictedHandleCompletion() { 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", maxCachedItems: 2); writer.Acknowledge("A.X.AckMsg", "1", Alice()); // handle 100 writer.Acknowledge("B.X.AckMsg", "2", Alice()); // handle 101 Assert.Equal(2, cache.Count); writer.Acknowledge("C.X.AckMsg", "3", Alice()); // handle 102, evicts A (100) Assert.Equal(2, cache.Count); Assert.Equal(0UL, cache.CurrentVersion(7, 100)); } [Fact] public void Dispose_ForgetsServerCompletions() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; MxAccessWriteCompletionCache cache = new MxAccessWriteCompletionCache(); server.OnWrite = (handle, item) => cache.Record(handle, item, Rows(MxStatusCategory.Ok, 0)); AlarmAckWriter writer = new AlarmAckWriter(server, cache, () => { }, TimeSpan.FromSeconds(1), "MxGateway"); writer.Acknowledge("A.X.AckMsg", "1", Alice()); writer.Acknowledge("B.X.AckMsg", "2", Alice()); cache.Record(8, 100, Rows(MxStatusCategory.Ok, 0)); // another server's entry survives writer.Dispose(); Assert.Equal(1, cache.Count); Assert.Equal(1UL, cache.CurrentVersion(8, 100)); } [Fact] public void Acknowledge_WhenWriteDisconnected_ForgetsServerCompletions() { 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"); writer.Acknowledge("A.X.AckMsg", "1", Alice()); // handle 100 recorded server.WriteException = new COMException("x", unchecked((int)0x80010108)); writer.Acknowledge("A.X.AckMsg", "2", Alice()); Assert.Contains("Unregister", server.Operations); Assert.Equal(0, cache.Count); } [Fact] public void Acknowledge_WhenNonOkCompletionHasNoDetail_ReturnsMinusOne() { RecordingAckServer server = new RecordingAckServer { AuthenticateResult = 5 }; MxAccessWriteCompletionCache cache = new MxAccessWriteCompletionCache(); server.OnWrite = (handle, item) => cache.Record(handle, item, Rows(MxStatusCategory.SecurityError, 0)); 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(-1, result.NativeStatus); } [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 Exception? AdviseException { get; set; } public Exception? WriteException { get; set; } public string? LastPassword { get; private set; } public Action? OnWrite { get; set; } public List RemovedItemHandles { get; } = new(); public List WrittenItemHandles { get; } = new(); 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"); RemovedItemHandles.Add(itemHandle); } public void Advise(int serverHandle, int itemHandle) { Operations.Add("Advise"); if (AdviseException is not null) { throw AdviseException; } } 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); WrittenItemHandles.Add(itemHandle); if (WriteException is not null) { throw WriteException; } 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) { } }