using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging.Abstractions; using Opc.Ua; using Opc.Ua.Server; using Shouldly; using Xunit; using Opc.Ua.Security.Certificates; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; public sealed class SessionHistoryContinuationStoreTests : IAsyncLifetime { private readonly string _pki = Path.Combine(Path.GetTempPath(), $"otua-sdk2-cp-{Guid.NewGuid():N}"); private OpcUaApplicationHost _host = null!; private OtOpcUaSdkServer _server = null!; private Certificate _certificate = null!; private Nonce _nonce = null!; private readonly SessionHistoryContinuationStore _store = new(); public async ValueTask InitializeAsync() { var started = await OpcUaHostStart.StartAsync(port => new OpcUaApplicationHost(new OpcUaApplicationHostOptions { ApplicationName = "SDK2.Continuation", ApplicationUri = "urn:SDK2.Continuation", PublicHostname = "localhost", OpcUaPort = port, PkiStoreRoot = _pki, }, NullLogger.Instance), () => new OtOpcUaSdkServer(), TestContext.Current.CancellationToken); _host = started.Host; _server = started.Server; using var key = RSA.Create(2048); var request = new CertificateRequest("CN=SDK2.Continuation.Unit", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); _certificate = Certificate.From(request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1))); _nonce = Nonce.CreateNonce(32); } public async ValueTask DisposeAsync() { _nonce.Dispose(); _certificate.Dispose(); await _host.DisposeAsync(); Directory.Delete(_pki, true); } [Fact] public void Real_session_store_transfers_an_owned_single_use_wrapper() { using var session = Session(); var state = State(); var token = _store.Save(session, state)!; token.Length.ShouldBe(16); using var point = session.ContinuationPoints.RestoreHistory(ByteString.From(token)); point.ShouldNotBeNull(); point.Id.ShouldBe(new Guid(token)); _store.TryTake(session, token).ShouldBeNull(); var next = _store.Save(session, state)!; _store.TryTake(session, next).ShouldBeSameAs(state); _store.TryTake(session, next).ShouldBeNull(); } [Fact] public void Real_sessions_isolate_points_evict_oldest_and_release_explicitly() { using var first = Session(); using var second = Session(); var oldest = _store.Save(first, State())!; var middle = _store.Save(first, State())!; var latest = _store.Save(first, State())!; _store.TryTake(first, oldest).ShouldBeNull(); _store.TryTake(second, middle).ShouldBeNull(); _store.TryTake(first, middle).ShouldNotBeNull(); _store.Release(first, latest); _store.TryTake(first, latest).ShouldBeNull(); _store.Release(first, latest); _store.TryTake(first, [1, 2, 3]).ShouldBeNull(); } [Fact] public void Closing_the_real_session_drops_abandoned_points() { var session = Session(); var token = _store.Save(session, State())!; session.Dispose(); _store.TryTake(session, token).ShouldBeNull(); } private Session Session() { var endpoint = new EndpointDescription { EndpointUrl = "opc.tcp://localhost:4840", SecurityMode = MessageSecurityMode.None }; using var context = new OperationContext(new RequestHeader(), new SecureChannelContext(Guid.NewGuid().ToString(), endpoint, RequestEncoding.Binary), RequestType.CreateSession, RequestLifetime.None); return new Session(context, _server.CurrentInstance, _certificate, new NodeId(Guid.NewGuid()), default, _nonce, "SDK2 continuation unit", new ApplicationDescription(), endpoint.EndpointUrl, null!, null!, 60_000, 2, 2); } private static HistoryContinuationState State() => new("tag", DateTime.UtcNow, DateTime.UtcNow.AddHours(1), 0, 2); }