feat(opcuaclient-browser): real AttributesAsync + certificate-auth browse (R06:O-5)
- AttributesAsync was a stub returning nothing; it now reads NodeClass / DisplayName / DataType / ValueRank / UserAccessLevel in one request and maps them through UpstreamAttributeMapping, the same mapping discovery registers with (moved into the shared .Session leaf; the driver forwards to it). Objects have no value attributes. - Certificate auth was refused "in v1". The browse now builds its identity through OpcUaUserIdentityFactory (shared .Session leaf, the driver forwards to it), including the secret: resolution of Password / UserCertificatePassword through the host's ISecretResolver (optional ctor param, DI-injected). Fail-closed: a secret: ref with no store throws instead of being sent as the literal (the browse used to send a secret: Password verbatim). The identity is built before any network I/O. Refs #743 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
+8
-7
@@ -41,10 +41,9 @@ public sealed class OpcUaClientDriverBrowserTests
|
||||
ex.Message.ShouldContain("null");
|
||||
}
|
||||
|
||||
/// <summary>Certificate auth is not supported by the browser; the failure message
|
||||
/// must say so explicitly rather than surfacing a downstream COM/SDK error.
|
||||
/// <c>OpcUaAuthType.Certificate</c> serializes as the numeric value 2 under the
|
||||
/// browser's default System.Text.Json options (no string-enum converter).</summary>
|
||||
/// <summary>Certificate auth with no certificate configured fails before any network I/O with a message
|
||||
/// naming the certificate (review 2026-10-21 O-5: Certificate auth is browsable now; it used to be refused),
|
||||
/// rather than surfacing a downstream SDK error. Numeric enum form (<c>OpcUaAuthType.Certificate</c> == 2).</summary>
|
||||
[Fact]
|
||||
public async Task OpenAsync_with_certificate_auth_throws_clear_message()
|
||||
{
|
||||
@@ -76,7 +75,7 @@ public sealed class OpcUaClientDriverBrowserTests
|
||||
{
|
||||
// "AuthType":"Certificate" — STRING form, as AdminUI emits.
|
||||
var json = """{"EndpointUrl":"opc.tcp://127.0.0.1:1","AuthType":"Certificate"}""";
|
||||
// The browser should reach its own Certificate-auth guard and throw
|
||||
// The browser should reach its own Certificate-identity check (no UserCertificatePath) and throw
|
||||
// InvalidOperationException with a message that mentions "Certificate".
|
||||
// Without JsonStringEnumConverter the deserialization itself throws JsonException,
|
||||
// which means this Should.ThrowAsync<InvalidOperationException> assertion fails.
|
||||
@@ -118,8 +117,10 @@ public sealed class OpcUaClientDriverBrowserTests
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
|
||||
// Arrange: build a minimal OpcUaClientBrowseSession without a live server.
|
||||
// AttributesAsync does not call ISession — MockBehavior.Loose is safe.
|
||||
// A loose mock answers the attribute read with no response, so nothing is known about the node.
|
||||
var mockSession = new Mock<ISession>(MockBehavior.Loose);
|
||||
mockSession.SetupGet(s => s.NamespaceUris).Returns(new NamespaceTable());
|
||||
mockSession.SetupGet(s => s.MessageContext).Returns(new ServiceMessageContext());
|
||||
var nsMap = NamespaceMap.FromTable(new NamespaceTable());
|
||||
var sut = new OpcUaClientBrowseSession(mockSession.Object, nsMap, ObjectIds.ObjectsFolder);
|
||||
|
||||
@@ -132,7 +133,7 @@ public sealed class OpcUaClientDriverBrowserTests
|
||||
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).
|
||||
// (a node not known to be a Variable has no value attributes to show).
|
||||
attrs.ShouldBeEmpty();
|
||||
sut.LastUsedUtc.ShouldBeGreaterThan(before,
|
||||
"AttributesAsync must refresh LastUsedUtc to satisfy the IBrowseSession contract");
|
||||
|
||||
Reference in New Issue
Block a user