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");
}
}