using Opc.Ua; using ZB.MOM.WW.OtOpcUa.Client.Shared; using ZB.MOM.WW.OtOpcUa.Client.Shared.Models; using BrowseResult = ZB.MOM.WW.OtOpcUa.Client.Shared.Models.BrowseResult; namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Tests.Fakes; /// /// Fake implementation of for unit testing commands. /// Records all method calls and returns configurable results. /// public sealed class FakeOpcUaClientService : IOpcUaClientService { // Track calls /// Gets a value indicating whether ConnectAsync was called. public bool ConnectCalled { get; private set; } /// Gets the connection settings from the most recent ConnectAsync call. public ConnectionSettings? LastConnectionSettings { get; private set; } /// Gets a value indicating whether DisconnectAsync was called. public bool DisconnectCalled { get; private set; } /// Gets a value indicating whether Dispose was called. public bool DisposeCalled { get; private set; } /// Gets the list of node IDs passed to ReadValueAsync calls. public List ReadNodeIds { get; } = []; /// Gets the list of (NodeId, value) pairs passed to WriteValueAsync calls. public List<(NodeId NodeId, object Value)> WriteValues { get; } = []; /// Gets the list of parent node IDs passed to BrowseAsync calls. public List BrowseNodeIds { get; } = []; /// Gets the list of (NodeId, intervalMs) pairs from SubscribeAsync calls. public List<(NodeId NodeId, int IntervalMs)> SubscribeCalls { get; } = []; /// Gets the list of node IDs passed to UnsubscribeAsync calls. public List UnsubscribeCalls { get; } = []; /// Gets the list of (SourceNodeId, intervalMs) pairs from SubscribeAlarmsAsync calls. public List<(NodeId? SourceNodeId, int IntervalMs)> SubscribeAlarmsCalls { get; } = []; /// Gets a value indicating whether UnsubscribeAlarmsAsync was called. public bool UnsubscribeAlarmsCalled { get; private set; } /// Gets a value indicating whether RequestConditionRefreshAsync was called. public bool RequestConditionRefreshCalled { get; private set; } /// Gets the list of (NodeId, start, end, maxValues) tuples from HistoryReadRawAsync calls. public List<(NodeId NodeId, DateTime Start, DateTime End, int MaxValues)> HistoryReadRawCalls { get; } = []; /// Gets the list of history read aggregate call parameters. public List<(NodeId NodeId, DateTime Start, DateTime End, AggregateType Aggregate, double IntervalMs)> HistoryReadAggregateCalls { get; } = []; /// Gets a value indicating whether GetRedundancyInfoAsync was called. public bool GetRedundancyInfoCalled { get; private set; } // Configurable results /// Gets or sets the connection info returned by ConnectAsync. public ConnectionInfo ConnectionInfoResult { get; set; } = new( "opc.tcp://localhost:4840", "TestServer", "None", "http://opcfoundation.org/UA/SecurityPolicy#None", "session-1", "TestSession"); /// Gets or sets the data value returned by ReadValueAsync. public DataValue ReadValueResult { get; set; } = new( new Variant(42), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow); /// Gets or sets the status code returned by WriteValueAsync. public StatusCode WriteStatusCodeResult { get; set; } = StatusCodes.Good; /// Gets or sets the browse results returned by BrowseAsync. public IReadOnlyList BrowseResults { get; set; } = new List { new("ns=2;s=Node1", "Node1", "Object", true), new("ns=2;s=Node2", "Node2", "Variable", false) }; /// /// Optional per-parent-node browse results. When a key matches the requested parent's /// , this dictionary takes precedence over . /// Tests exercising recursive walks (Client.CLI-010) use it to model a real subtree whose /// child node ids do not collide on descent. /// public Dictionary> BrowseResultsByParent { get; } = new(); /// Gets or sets the history read result returned by HistoryReadRawAsync and HistoryReadAggregateAsync. public IReadOnlyList HistoryReadResult { get; set; } = new List { new(new Variant(10.0), StatusCodes.Good, DateTime.UtcNow.AddHours(-1), DateTime.UtcNow), new(new Variant(20.0), StatusCodes.Good, DateTime.UtcNow, DateTime.UtcNow) }; /// Gets or sets the redundancy info returned by GetRedundancyInfoAsync. public RedundancyInfo RedundancyInfoResult { get; set; } = new( "Warm", 200, ["urn:server1", "urn:server2"], "urn:app:test"); /// Gets or sets the exception thrown by ConnectAsync. public Exception? ConnectException { get; set; } /// Gets or sets the exception thrown by ReadValueAsync. public Exception? ReadException { get; set; } /// Gets or sets the exception thrown by WriteValueAsync. public Exception? WriteException { get; set; } /// Gets or sets the exception thrown by RequestConditionRefreshAsync. public Exception? ConditionRefreshException { get; set; } /// Gets or sets the exception thrown by SubscribeAsync. public Exception? SubscribeException { get; set; } /// public bool IsConnected => ConnectCalled && !DisconnectCalled; /// public ConnectionInfo? CurrentConnectionInfo => ConnectCalled ? ConnectionInfoResult : null; /// public event EventHandler? DataChanged; /// public event EventHandler? AlarmEvent; /// public event EventHandler? ConnectionStateChanged; /// True when at least one handler is attached to . public bool HasDataChangedSubscribers => DataChanged != null; /// True when at least one handler is attached to . public bool HasAlarmEventSubscribers => AlarmEvent != null; /// public Task ConnectAsync(ConnectionSettings settings, CancellationToken ct = default) { ConnectCalled = true; LastConnectionSettings = settings; if (ConnectException != null) throw ConnectException; return Task.FromResult(ConnectionInfoResult); } /// public Task DisconnectAsync(CancellationToken ct = default) { DisconnectCalled = true; return Task.CompletedTask; } /// public Task ReadValueAsync(NodeId nodeId, CancellationToken ct = default) { ReadNodeIds.Add(nodeId); if (ReadException != null) throw ReadException; return Task.FromResult(ReadValueResult); } /// public Task WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default) { WriteValues.Add((nodeId, value)); if (WriteException != null) throw WriteException; return Task.FromResult(WriteStatusCodeResult); } /// public Task> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default) { BrowseNodeIds.Add(parentNodeId); var key = parentNodeId?.ToString(); if (key != null && BrowseResultsByParent.TryGetValue(key, out var keyed)) return Task.FromResult(keyed); return Task.FromResult(BrowseResults); } /// public Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default) { SubscribeCalls.Add((nodeId, intervalMs)); if (SubscribeException != null) throw SubscribeException; return Task.CompletedTask; } /// public Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default) { UnsubscribeCalls.Add(nodeId); return Task.CompletedTask; } /// public Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default) { SubscribeAlarmsCalls.Add((sourceNodeId, intervalMs)); return Task.CompletedTask; } /// public Task UnsubscribeAlarmsAsync(CancellationToken ct = default) { UnsubscribeAlarmsCalled = true; return Task.CompletedTask; } /// public Task RequestConditionRefreshAsync(CancellationToken ct = default) { RequestConditionRefreshCalled = true; if (ConditionRefreshException != null) throw ConditionRefreshException; return Task.CompletedTask; } /// public Task AcknowledgeAlarmAsync(string conditionNodeId, byte[] eventId, string comment, CancellationToken ct = default) { return Task.FromResult(new StatusCode(StatusCodes.Good)); } /// public Task> HistoryReadRawAsync( NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default) { HistoryReadRawCalls.Add((nodeId, startTime, endTime, maxValues)); return Task.FromResult(HistoryReadResult); } /// public Task> HistoryReadAggregateAsync( NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default) { HistoryReadAggregateCalls.Add((nodeId, startTime, endTime, aggregate, intervalMs)); return Task.FromResult(HistoryReadResult); } /// public Task GetRedundancyInfoAsync(CancellationToken ct = default) { GetRedundancyInfoCalled = true; return Task.FromResult(RedundancyInfoResult); } /// /// Marks the fake client as disposed so CLI command tests can assert cleanup behavior. /// public void Dispose() { DisposeCalled = true; } /// Raises the DataChanged event for testing subscribe commands. /// The node ID string that changed. /// The new data value. public void RaiseDataChanged(string nodeId, DataValue value) { DataChanged?.Invoke(this, new DataChangedEventArgs(nodeId, value)); } /// Raises the AlarmEvent for testing alarm commands. /// The alarm event arguments. public void RaiseAlarmEvent(AlarmEventArgs args) { AlarmEvent?.Invoke(this, args); } /// Raises the ConnectionStateChanged event for testing. /// The previous connection state. /// The new connection state. /// The endpoint URL. public void RaiseConnectionStateChanged(ConnectionState oldState, ConnectionState newState, string endpointUrl) { ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(oldState, newState, endpointUrl)); } }