fix(commons): resolve Commons-013,014 — integral JSON index handling, distinguish Malformed vs Legacy OPC UA config

This commit is contained in:
Joseph Doherty
2026-05-17 03:18:17 -04:00
parent 21856a4be7
commit a78c3bcb6f
5 changed files with 190 additions and 18 deletions

View File

@@ -168,6 +168,47 @@ public class OpcUaEndpointConfigSerializerTests
Assert.True(result.IsLegacy);
}
// ── Commons-014 regression: a corrupt typed row must not be mislabelled Legacy ──
[Fact]
public void Deserialize_TypedShapeWithInvalidEnum_ReportsMalformedNotLegacy()
{
// The row IS the current typed shape (it has endpointUrl) but an enum-valued
// field holds an unrecognised string. Typed deserialization throws JsonException;
// it must NOT fall through to the legacy path and be reported as Legacy.
var json = """{"endpointUrl":"opc.tcp://x:4840","securityMode":"NotARealMode"}""";
var result = OpcUaEndpointConfigSerializer.Deserialize(json);
Assert.Equal(OpcUaConfigParseStatus.Malformed, result.Status);
Assert.False(result.IsLegacy);
}
[Fact]
public void Deserialize_TypedShapeWithWrongTypeField_ReportsMalformedNotLegacy()
{
// endpointUrl present (typed shape) but a numeric field holds a non-numeric token.
var json = """{"endpointUrl":"opc.tcp://x:4840","sessionTimeoutMs":"not-a-number"}""";
var result = OpcUaEndpointConfigSerializer.Deserialize(json);
Assert.Equal(OpcUaConfigParseStatus.Malformed, result.Status);
Assert.False(result.IsLegacy);
}
[Fact]
public void Deserialize_ValidTypedRow_StillReportsTyped()
{
// Guard: a clean typed row is still classified Typed after the Commons-014 fix.
var json = """{"endpointUrl":"opc.tcp://x:4840","securityMode":"sign"}""";
var result = OpcUaEndpointConfigSerializer.Deserialize(json);
Assert.Equal(OpcUaConfigParseStatus.Typed, result.Status);
Assert.Equal("opc.tcp://x:4840", result.Config.EndpointUrl);
Assert.Equal(OpcUaSecurityMode.Sign, result.Config.SecurityMode);
}
[Fact]
public void Deserialize_TwoElementDeconstruction_StillWorks()
{

View File

@@ -144,4 +144,48 @@ public class DynamicJsonElementTests
Assert.False(strWrapper.TryConvert(convBinder, out var result));
Assert.Null(result);
}
// ── Commons-013 regression: integral index values other than int must work ──
[Fact]
public void IndexAccess_WithLongIndex_Works()
{
// DynamicJsonElement.Wrap surfaces JSON numbers as long; an index computed
// from a wrapped JSON number (obj.items[obj.count - 1]) arrives as a long.
dynamic obj = Wrap("""{ "items": [ "a", "b", "c" ] }""");
long idx = 1L;
Assert.Equal("b", obj.items[idx]);
}
[Fact]
public void IndexAccess_WithIndexDerivedFromWrappedJsonNumber_Works()
{
// The exact failing case from Commons-013: count is a wrapped JSON number
// (unwrapped as long), so count - 1 is a long.
dynamic obj = Wrap("""{ "items": [ "a", "b", "c" ], "count": 3 }""");
Assert.Equal("c", obj.items[obj.count - 1]);
}
[Theory]
[InlineData((byte)0, "a")]
[InlineData((short)1, "b")]
public void IndexAccess_WithWideningIntegralIndex_Works(object index, string expected)
{
dynamic obj = Wrap("""{ "items": [ "a", "b", "c" ] }""");
Assert.Equal(expected, obj.items[index]);
}
[Fact]
public void IndexAccess_WithLongIndexOutOfRange_Throws()
{
// An out-of-range long index is still rejected (binder surfaces the error).
dynamic obj = Wrap("""{ "items": [ "a" ] }""");
long idx = 5L;
Assert.Throws<Microsoft.CSharp.RuntimeBinder.RuntimeBinderException>(
() => { var _ = obj.items[idx]; });
}
}