using Moq; using Opc.Ua; using Opc.Ua.Client; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient; using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests; /// /// Unit-only coverage of 's pre-connect /// validation. These tests do not require a live OPC UA endpoint and are safe to /// run without the opc-plc Docker fixture. /// [Trait("Category", "Unit")] public sealed class OpcUaClientDriverBrowserTests { private readonly OpcUaClientDriverBrowser _sut = new(); /// The DriverType key must match the AdminUI's persisted value. [Fact] public void DriverType_is_OpcUaClient() => _sut.DriverType.ShouldBe("OpcUaClient"); /// An empty endpoint must fail fast with a clear EndpointUrl-mentioning message. [Fact] public async Task OpenAsync_with_empty_endpoint_throws() { var json = """{"EndpointUrl":"","EndpointUrls":[]}"""; var ex = await Should.ThrowAsync( () => _sut.OpenAsync(json, TestContext.Current.CancellationToken)); ex.Message.ShouldContain("EndpointUrl"); } /// A JSON literal that deserializes to null must fail fast. [Fact] public async Task OpenAsync_with_null_json_throws() { var ex = await Should.ThrowAsync( () => _sut.OpenAsync("null", TestContext.Current.CancellationToken)); ex.Message.ShouldContain("null"); } /// Certificate auth is not supported by the browser; the failure message /// must say so explicitly rather than surfacing a downstream COM/SDK error. /// OpcUaAuthType.Certificate serializes as the numeric value 2 under the /// browser's default System.Text.Json options (no string-enum converter). [Fact] public async Task OpenAsync_with_certificate_auth_throws_clear_message() { var json = """{"EndpointUrl":"opc.tcp://127.0.0.1:1","AuthType":2}"""; var ex = await Should.ThrowAsync( () => _sut.OpenAsync(json, TestContext.Current.CancellationToken)); ex.Message.ShouldContain("Certificate"); } // ---- Driver.OpcUaClient.Browser-001: AttributesAsync must refresh LastUsedUtc ---- /// /// must be updated on every call /// including , which the /// uses for idle eviction. Before the fix /// the method returned immediately without touching the property, causing a /// session that only received AttributesAsync calls to be evicted early. /// [Fact] public async Task AttributesAsync_updates_LastUsedUtc() { var ct = TestContext.Current.CancellationToken; // Arrange: build a minimal OpcUaClientBrowseSession without a live server. // AttributesAsync does not call ISession — MockBehavior.Loose is safe. var mockSession = new Mock(MockBehavior.Loose); var nsMap = NamespaceMap.FromTable(new NamespaceTable()); var sut = new OpcUaClientBrowseSession(mockSession.Object, nsMap, ObjectIds.ObjectsFolder); var before = sut.LastUsedUtc; // Introduce a small delay so clock advances at least one tick. await Task.Delay(5, ct); // Act var attrs = await sut.AttributesAsync("nsu=http://opcfoundation.org/UA/;i=85", ct); // Assert: LastUsedUtc must have been refreshed, and the result must be empty // (OPC UA picker treats variables as leaves, no attribute side-panel). attrs.ShouldBeEmpty(); sut.LastUsedUtc.ShouldBeGreaterThan(before, "AttributesAsync must refresh LastUsedUtc to satisfy the IBrowseSession contract"); } }