review(Driver.OpcUaClient.Browser): add JsonStringEnumConverter (systemic enum bug)

Cross-module fix from the review sweep. -003 (Medium): the browser's JsonOpts lacked
JsonStringEnumConverter (the factory+probe both carry it), so AdminUI string-enum configs
(AuthType/SecurityPolicy/SecurityMode/TargetNamespaceKind) threw on deserialize. Added the
converter (accepts string AND numeric) + TDD.
This commit is contained in:
Joseph Doherty
2026-06-19 12:29:39 -04:00
parent 7e1f34da7d
commit 298bd4bfe5
4 changed files with 86 additions and 5 deletions
@@ -19,10 +19,10 @@ public sealed class OpcUaClientBrowseSessionTests
private static string Endpoint =>
Environment.GetEnvironmentVariable("OPCUA_SIM_ENDPOINT") ?? "opc.tcp://10.100.0.35:50000";
// Enum values use their numeric ordinal because the browser uses default
// System.Text.Json with no JsonStringEnumConverter. SecurityPolicy.None,
// SecurityMode.None, OpcUaAuthType.Anonymous are all index 0, so omitting them
// also works — keeping them explicit for documentation value.
// SecurityPolicy.None, SecurityMode.None, OpcUaAuthType.Anonymous are all
// index 0 and can also be written as string names now that the browser carries
// JsonStringEnumConverter (Driver.OpcUaClient.Browser-003). Numeric ordinals are
// kept here as they were before the fix — both forms are accepted.
private static string ConfigJson => $$"""
{
"EndpointUrl":"{{Endpoint}}",
@@ -54,6 +54,55 @@ public sealed class OpcUaClientDriverBrowserTests
ex.Message.ShouldContain("Certificate");
}
// ---- Driver.OpcUaClient.Browser-003: JsonStringEnumConverter must be present ----
/// <summary>
/// AdminUI emits enum-valued driver-config fields (SecurityPolicy, SecurityMode,
/// AuthType, TargetNamespaceKind) as their <b>string</b> names (e.g.
/// <c>"SecurityPolicy":"Basic256Sha256"</c>). Without a
/// <see cref="System.Text.Json.Serialization.JsonStringEnumConverter"/> on the browser's
/// <c>JsonSerializerOptions</c> the deserialization throws a
/// <see cref="System.Text.Json.JsonException"/> before the browser's own validation can
/// run, giving the caller a confusing parse error instead of the expected
/// domain-specific message.
///
/// This test passes a JSON blob with <c>"AuthType":"Certificate"</c> (string form) and
/// asserts that the browser's validation fires — i.e. deserialization succeeds and the
/// resulting <see cref="InvalidOperationException"/> mentions "Certificate". Before the
/// fix, a raw <see cref="System.Text.Json.JsonException"/> surfaces instead.
/// </summary>
[Fact]
public async Task OpenAsync_with_string_enum_AuthType_deserializes_correctly()
{
// "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
// InvalidOperationException with a message that mentions "Certificate".
// Without JsonStringEnumConverter the deserialization itself throws JsonException,
// which means this Should.ThrowAsync<InvalidOperationException> assertion fails.
var ex = await Should.ThrowAsync<InvalidOperationException>(
() => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
// The browser's own guard must fire (not a JsonException from the deserializer).
ex.Message.ShouldContain("Certificate");
}
/// <summary>
/// Companion to <see cref="OpenAsync_with_string_enum_AuthType_deserializes_correctly"/>:
/// confirms that <see cref="System.Text.Json.Serialization.JsonStringEnumConverter"/>
/// still accepts the integer/numeric ordinal form that the live-fixture tests currently
/// rely on, so no regression is introduced for existing numeric-authored configs.
/// </summary>
[Fact]
public async Task OpenAsync_with_numeric_enum_AuthType_still_works()
{
// "AuthType":2 — numeric ordinal form (OpcUaAuthType.Certificate == 2).
var json = """{"EndpointUrl":"opc.tcp://127.0.0.1:1","AuthType":2}""";
var ex = await Should.ThrowAsync<InvalidOperationException>(
() => _sut.OpenAsync(json, TestContext.Current.CancellationToken));
// JsonStringEnumConverter accepts both string and numeric ordinal forms.
ex.Message.ShouldContain("Certificate");
}
// ---- Driver.OpcUaClient.Browser-001: AttributesAsync must refresh LastUsedUtc ----
/// <summary>