using Opc.Ua; using ZB.MOM.WW.LmxOpcUa.Client.Shared.Adapters; namespace ZB.MOM.WW.LmxOpcUa.Client.Shared.Tests.Fakes; /// /// Test double for used to simulate reads, writes, browsing, history, and failover callbacks. /// internal sealed class FakeSessionAdapter : ISessionAdapter { private readonly List _createdSubscriptions = []; private Action? _keepAliveCallback; /// /// Gets a value indicating whether the fake session has been closed through the client disconnect path. /// public bool Closed { get; private set; } /// /// Gets a value indicating whether the fake session has been disposed. /// public bool Disposed { get; private set; } public int ReadCount { get; private set; } public int WriteCount { get; private set; } public int BrowseCount { get; private set; } public int BrowseNextCount { get; private set; } public int HasChildrenCount { get; private set; } public int HistoryReadRawCount { get; private set; } public int HistoryReadAggregateCount { get; private set; } // Configurable responses public DataValue? ReadResponse { get; set; } public Func? ReadResponseFunc { get; set; } public StatusCode WriteResponse { get; set; } = StatusCodes.Good; public bool ThrowOnRead { get; set; } public bool ThrowOnWrite { get; set; } public bool ThrowOnBrowse { get; set; } public ReferenceDescriptionCollection BrowseResponse { get; set; } = []; public byte[]? BrowseContinuationPoint { get; set; } public ReferenceDescriptionCollection BrowseNextResponse { get; set; } = []; public byte[]? BrowseNextContinuationPoint { get; set; } public bool HasChildrenResponse { get; set; } = false; public List HistoryReadRawResponse { get; set; } = []; public List HistoryReadAggregateResponse { get; set; } = []; public bool ThrowOnHistoryReadRaw { get; set; } public bool ThrowOnHistoryReadAggregate { get; set; } /// /// Gets or sets the next fake subscription returned when the client creates a monitored-item subscription. /// If unset, the fake builds a new subscription automatically. /// public FakeSubscriptionAdapter? NextSubscription { get; set; } /// /// Gets the fake subscriptions created by this session so tests can inspect replay and cleanup behavior. /// public IReadOnlyList CreatedSubscriptions => _createdSubscriptions; /// public bool Connected { get; set; } = true; /// public string SessionId { get; set; } = "ns=0;i=12345"; /// public string SessionName { get; set; } = "FakeSession"; /// public string EndpointUrl { get; set; } = "opc.tcp://localhost:4840"; /// public string ServerName { get; set; } = "FakeServer"; /// public string SecurityMode { get; set; } = "None"; /// public string SecurityPolicyUri { get; set; } = "http://opcfoundation.org/UA/SecurityPolicy#None"; /// public NamespaceTable NamespaceUris { get; set; } = new(); /// public void RegisterKeepAliveHandler(Action callback) { _keepAliveCallback = callback; } /// public Task ReadValueAsync(NodeId nodeId, CancellationToken ct) { ReadCount++; if (ThrowOnRead) throw new ServiceResultException(StatusCodes.BadNodeIdUnknown, "Node not found"); if (ReadResponseFunc != null) return Task.FromResult(ReadResponseFunc(nodeId)); return Task.FromResult(ReadResponse ?? new DataValue(new Variant(0), StatusCodes.Good)); } /// public Task WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct) { WriteCount++; if (ThrowOnWrite) throw new ServiceResultException(StatusCodes.BadNodeIdUnknown, "Node not found"); return Task.FromResult(WriteResponse); } /// public Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync( NodeId nodeId, uint nodeClassMask, CancellationToken ct) { BrowseCount++; if (ThrowOnBrowse) throw new ServiceResultException(StatusCodes.BadNodeIdUnknown, "Node not found"); return Task.FromResult((BrowseContinuationPoint, BrowseResponse)); } /// public Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync( byte[] continuationPoint, CancellationToken ct) { BrowseNextCount++; return Task.FromResult((BrowseNextContinuationPoint, BrowseNextResponse)); } /// public Task HasChildrenAsync(NodeId nodeId, CancellationToken ct) { HasChildrenCount++; return Task.FromResult(HasChildrenResponse); } /// public Task> HistoryReadRawAsync( NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues, CancellationToken ct) { HistoryReadRawCount++; if (ThrowOnHistoryReadRaw) throw new ServiceResultException(StatusCodes.BadHistoryOperationUnsupported, "History not supported"); return Task.FromResult>(HistoryReadRawResponse); } /// public Task> HistoryReadAggregateAsync( NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs, CancellationToken ct) { HistoryReadAggregateCount++; if (ThrowOnHistoryReadAggregate) throw new ServiceResultException(StatusCodes.BadHistoryOperationUnsupported, "History not supported"); return Task.FromResult>(HistoryReadAggregateResponse); } /// public Task CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct) { var sub = NextSubscription ?? new FakeSubscriptionAdapter(); NextSubscription = null; _createdSubscriptions.Add(sub); return Task.FromResult(sub); } /// public Task?> CallMethodAsync(NodeId objectId, NodeId methodId, object[] inputArguments, CancellationToken ct = default) { return Task.FromResult?>(null); } /// public Task CloseAsync(CancellationToken ct) { Closed = true; Connected = false; return Task.CompletedTask; } /// /// Marks the fake session as disposed so tests can verify cleanup after disconnect or failover. /// public void Dispose() { Disposed = true; Connected = false; } /// /// Simulates a keep-alive event. /// public void SimulateKeepAlive(bool isGood) { _keepAliveCallback?.Invoke(isGood); } }