Files
UA-.NETStandard/tests/Opc.Ua.PubSub.Tests/Encoding/Json/JsonHelperCoverageTests.cs
romanett ce88955f70 Harden encoders/decoders against untrusted input and align them with Part 6 / Part 14 (#4599)
# Description

Hardens the encoders and decoders against untrusted input and aligns
them with OPC 10000-6 (1.05) and OPC 10000-14. This comes from an audit
of every codec reachable from a peer:
- Binary, JSON and XML decoders and encoders;
- built-in and complex types;
- UA-TCP/UASC framing;
- PubSub UADP and PubSub JSON.

The audit found 97 issues: 1 critical, 7 high, 39 medium and 50 low.
They are all fixed here, one commit per finding (IDs B1-, J1-, X1-, T1-,
S1-, U1-, P1-, E1-, E2- in the commit subjects). Every fix was then
checked against the spec a second time, and the deviations that re-check
found were fixed as well (`*-spec` commits).

## Security (decoders reachable from unauthenticated peers)

- **Allocation from length prefixes (B1-1, B1-5, U1-1, U1-3).**
- Array, string and byte-string lengths are checked against the bytes
left in the message before anything is allocated.
- Arrays read element by element grow as elements are decoded, so nested
length prefixes can no longer multiply memory.
- UADP discovery counts and chunk reassembly buffers are bounded the
same way.
- **Stack exhaustion.** Nesting is checked for remaining stack
(`RuntimeHelpers.EnsureSufficientExecutionStack` mapped to
`BadEncodingLimitsExceeded`), and the large Variant read/write methods
were split into scalar, array and matrix methods. Measured on 1 MB and
256 KB threads, no codec overflows at the default limits any more.
- **XML element depth (X1-1).** XmlElement values and raw
ExtensionObject bodies are copied node by node with a depth limit,
instead of with the recursive `InnerXml`, `ImportNode`, `InnerText` and
`DeepEquals` calls.
- **Limit bypasses:**
- JSON NodeId, QualifiedName and opaque strings now respect
MaxStringLength and MaxByteStringLength (J1-1).
  - Matrix rank is limited to 32.
  - Decimal size is bounded (T1-3).
  - XML base64 is checked before it is decoded.
  - XML arrays are checked before each element is read.
- **CPU and algorithmic cost:**
- UADP reassembly uses an O(1) overlap check with a per-message chunk
cap (U1-2).
  - PubSub JSON matches fields to metadata through a name index (P1-2).
  - LocalizedText "mul" text is parsed lazily (T1-5).
- Unknown SecurityTokenIds trigger a throttled background SKS refresh
instead of an inline call per datagram (U1-9).
- **PubSub metadata poisoning (P1-1).** Inbound metadata is registered
only for identities a configured reader expects, is capped, and an
unauthenticated message can no longer pin a version.
- **Exception contract.** Decoders fail with `ServiceResultException`
(BadDecodingError / BadEncodingLimitsExceeded) instead of letting
ArgumentException, InvalidOperationException, TypeLoadException and
similar escape.
- **UASC (S1-*):**
  - OPN and CLO must be single final chunks.
  - The certificate of an unsigned OPN is ignored.
  - The padding and ExtraPadding bytes are validated.
- A renewed token is activated only after a chunk has been verified with
it.
- A policy change on renewal is rejected before the channel certificate
is touched.
  - Secured chunk length arithmetic is bounded.

## Wire-visible behaviour changes

All follow the spec exactly; there is no reader for the old forms unless
noted.

- **UA Binary:**
  - Variant type ids 26-31 decode as ByteString.
- Picoseconds without their timestamp are ignored, and values of 10000
and above are clamped to 9999.
- A Null-type Variant with the array flag consumes its length and
dimensions.
- **UA JSON:**
- Messages are ExtensionObjects with the body inline; there is no
`UaBody` envelope any more (5.4.9 / 5.4.2.16).
- The Variant `Value` is omitted exactly for NULLs of nullable types
(Table 41).
  - RawData keeps nested UaType/UaTypeId and the StatusCode Symbol.
  - Duplicate member names are rejected.
  - `{}` decodes as a null ExtensionObject.
- Numbers and identifiers are parsed strictly and culture-independently.
  - QualifiedName follows 5.4.2.14.
  - Opaque bodies of known types carry the DataType id.
  - Enumerations without a literal are written as the number.
- **UA XML:**
  - The earliest DateTime is written as `0001-01-01T00:00:00Z`.
- Null typed scalars and null array elements are written with `xsi:nil`.
  - An ExtensionObject without a body has no `Body` element.
  - Strings XML cannot represent are rejected.
- Only the xs:dateTime and xs:date forms are accepted; a value without a
zone is still read as UTC, because published model files use it.
  - Only the Part 6 Guid form is accepted.
  - Booleans are case-sensitive in XmlDecoder.
- **UA-TCP:**
- OPNC/OPNA/CLOC/CLOA fault the channel. Older releases that split a
long-chain OPN into chunks are rejected, so this needs a release note.
  - The sender truncates the certificate chain to fit a single chunk.
  - The received certificate chain is sized from the chunk size.
- Hello buffer sizes down to 1024 bytes are honoured; below that, an ERR
with Bad_TcpInternalError is sent.
- A 4096-byte Error reason is accepted, and the SecurityPolicyUri limit
is 255 bytes.
- **PubSub UADP:**
- Chunk NetworkMessages use the Part 14 7.2.4.4.4 layout, and each chunk
is secured on its own with its own sequence number.
  - The Sizes array is part of the secured payload.
- Padded RawData keeps its length prefix, and null values are written as
-1.
  - DataSetMessages with the valid bit clear are skipped.
  - Null discovery arrays are accepted.
- **PubSub JSON:**
- Variants use `UaType`/`Value`, with the 7.2.5.4.2 top-level collapse.
The deprecated `Type`/`Body` form is still read.
- Status is written as a `{Code,Symbol}` object; a numeric Status is
still accepted.
  - Discovery messages follow Tables 188-192, with GUID MessageIds.
  - Enumeration fields are written as `Name_Value`.
  - Fields appended with a MinorVersion bump are accepted.

## Known deviations and limitations (documented, not changed)

- A known type whose ExtensionObject XML body fails to decode fails the
message, like a failing binary body. 5.2.2.15 only says the decoder
*should* continue, and continuing reopens the log-flooding path.
- Reserved Variant type ids 26-31 are passed to the application as
ByteString without the raw type id.
- JSON UaTypeId of an opaque body of an *unknown* type stays the
encoding id, because no type system is available to map it.
- PubSub JSON encoder:
- Structure fields without a subtype are still wrapped with a UaType,
and concrete multi-dimensional or abstract-rank fields still carry a
UaType (Table 186).
- Namespace-0 enumerations without EnumDataTypes are written as a
numeric string.
- Chunked transcoded frames have no writer group, so their sequence
numbers continue from the source number.
- Pre-existing and out of scope: the HTTPS JSON binding uses
Content-Type `application/opcua+uajson`, while 1.05 7.4.5 requires
`application/json`.

## Tests

- Every fix has a regression test.
- Tests that pinned non-conformant behaviour were updated, and their
commit messages say so.
- Fuzz seeds were regenerated where the wire changed.

Local results:
net10.0, 27 suites green (Types 11354, Core.Encoders 2933, Encoders.Fuzz
8112, Core 5955 + 1 known intermittent ActivityTraceFlags metrics test
that passes alone, Client 2732, Server 7802, Sessions 790, GDS 1234,
Subscriptions 641, SourceGeneration.Core 4266, PubSub 1490 and all
PubSub transport suites). Full UA.slnx build 0 warnings / 0 errors on
net10.0 and net48.

## Related Issues

- Follow-up: #4598 (possible throughput regression of per-element binary
array reads).

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. (net10.0
suites run; net48 build only)
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 05:12:47 +00:00

497 lines
20 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.IO;
using System.Text.Json;
using NUnit.Framework;
using Opc.Ua;
using Opc.Ua.PubSub.Encoding;
using Opc.Ua.PubSub.Encoding.Json;
using Opc.Ua.PubSub.Tests;
namespace OpcUaPubSubJsonTests
{
/// <summary>
/// Focused unit tests for helper classes inside
/// <c>Opc.Ua.PubSub.Encoding.Json</c> that aren't directly covered
/// by the higher-level encoder/decoder round-trip fixtures. These
/// exercise null-argument guards, the
/// <see cref="PubSubFieldEncoding.DataValue"/> field path, metadata
/// driven field-name resolution and the <see cref="JsonBufferWriter"/>
/// resize / disposal / boundary semantics.
/// </summary>
[TestFixture]
[Parallelizable(ParallelScope.All)]
public sealed class JsonHelperCoverageTests
{
[Test]
[TestSpec("7.2.5")]
public void WriteVariantPropertyRejectsNullArgs()
{
using var buffer = new MemoryStream();
using var writer = new Utf8JsonWriter(buffer);
writer.WriteStartObject();
IServiceMessageContext ctx = ServiceMessageContext.CreateEmpty(null!);
Assert.That(() => JsonVariantEncoder.WriteVariantProperty(
null!, "x", new Variant(1), JsonEncodingMode.Verbose, ctx),
Throws.ArgumentNullException);
Assert.That(() => JsonVariantEncoder.WriteVariantProperty(
writer, null!, new Variant(1), JsonEncodingMode.Verbose, ctx),
Throws.ArgumentNullException);
Assert.That(() => JsonVariantEncoder.WriteVariantProperty(
writer, "x", new Variant(1), JsonEncodingMode.Verbose, null!),
Throws.ArgumentNullException);
}
[Test]
[TestSpec("7.2.5")]
public void WriteVariantPropertyEmitsNullForNullVariant()
{
using var buffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(buffer))
{
writer.WriteStartObject();
JsonVariantEncoder.WriteVariantProperty(
writer, "x", Variant.Null,
JsonEncodingMode.Verbose,
ServiceMessageContext.CreateEmpty(null!));
writer.WriteEndObject();
}
string text = System.Text.Encoding.UTF8.GetString(buffer.ToArray());
Assert.That(text, Is.EqualTo("{\"x\":null}"));
}
[Test]
[TestSpec("7.2.5")]
public void WriteDataValuePropertyRejectsNullArgs()
{
using var buffer = new MemoryStream();
using var writer = new Utf8JsonWriter(buffer);
writer.WriteStartObject();
IServiceMessageContext ctx = ServiceMessageContext.CreateEmpty(null!);
DataValue dv = new(new Variant(1));
Assert.That(() => JsonVariantEncoder.WriteDataValueProperty(
null!, "x", dv, JsonEncodingMode.Verbose, ctx),
Throws.ArgumentNullException);
Assert.That(() => JsonVariantEncoder.WriteDataValueProperty(
writer, null!, dv, JsonEncodingMode.Verbose, ctx),
Throws.ArgumentNullException);
Assert.That(() => JsonVariantEncoder.WriteDataValueProperty(
writer, "x", dv, JsonEncodingMode.Verbose, null!),
Throws.ArgumentNullException);
}
[Test]
[TestSpec("7.2.5")]
public void WriteDataValuePropertyEmitsNullForNullDataValue()
{
using var buffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(buffer))
{
writer.WriteStartObject();
JsonVariantEncoder.WriteDataValueProperty(
writer, "x", DataValue.Null,
JsonEncodingMode.Verbose,
ServiceMessageContext.CreateEmpty(null!));
writer.WriteEndObject();
}
string text = System.Text.Encoding.UTF8.GetString(buffer.ToArray());
Assert.That(text, Is.EqualTo("{\"x\":null}"));
}
[Test]
[TestCase(JsonEncodingMode.Verbose)]
[TestCase(JsonEncodingMode.Compact)]
[TestCase(JsonEncodingMode.RawData)]
[TestSpec("7.2.5")]
public void WriteDataValuePropertyEmitsObjectForEveryMode(JsonEncodingMode mode)
{
using var buffer = new MemoryStream();
DataValue dv = new(
new Variant(123),
StatusCodes.Good,
new DateTimeUtc(2026, 1, 1, 0, 0, 0),
DateTimeUtc.MinValue);
using (var writer = new Utf8JsonWriter(buffer))
{
writer.WriteStartObject();
JsonVariantEncoder.WriteDataValueProperty(
writer, "v", dv, mode,
ServiceMessageContext.CreateEmpty(null!));
writer.WriteEndObject();
}
using var document = JsonDocument.Parse(buffer.ToArray());
JsonElement payload = document.RootElement.GetProperty("v");
Assert.That(payload.ValueKind, Is.EqualTo(JsonValueKind.Object));
Assert.That(payload.TryGetProperty("Value", out _), Is.True);
}
[Test]
[TestSpec("7.2.5.4")]
public void EncodeFieldsRejectsNullArgs()
{
using var buffer = new MemoryStream();
using var writer = new Utf8JsonWriter(buffer);
writer.WriteStartObject();
IServiceMessageContext ctx = ServiceMessageContext.CreateEmpty(null!);
Assert.That(() => JsonFieldEncoder.EncodeFields(
null!, JsonTestUtilities.CreateFields(),
null, JsonEncodingMode.Verbose, ctx),
Throws.ArgumentNullException);
Assert.That(() => JsonFieldEncoder.EncodeFields(
writer, JsonTestUtilities.CreateFields(),
null, JsonEncodingMode.Verbose, null!),
Throws.ArgumentNullException);
}
[Test]
[TestSpec("7.2.5.4")]
public void EncodeFieldsResolvesNameFromMetaDataAndAutoIndex()
{
DataSetField[] fields =
[
new DataSetField { Value = new Variant(1) },
new DataSetField { Value = new Variant(2) },
new DataSetField { Value = new Variant(3) },
new DataSetField { Value = new Variant(4) }
];
DataSetMetaDataType meta = JsonTestUtilities.CreateMetaData();
using var buffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(buffer))
{
writer.WriteStartObject();
JsonFieldEncoder.EncodeFields(
writer, fields, meta,
JsonEncodingMode.Verbose,
ServiceMessageContext.CreateEmpty(null!));
writer.WriteEndObject();
}
using var document = JsonDocument.Parse(buffer.ToArray());
JsonElement payload = document.RootElement.GetProperty("Payload");
Assert.That(payload.TryGetProperty("BoolField", out _), Is.True);
Assert.That(payload.TryGetProperty("IntField", out _), Is.True);
Assert.That(payload.TryGetProperty("StringField", out _), Is.True);
Assert.That(payload.TryGetProperty("Field3", out _), Is.True);
}
[Test]
[TestSpec("7.2.5.4")]
public void EncodeFieldsHandlesDataValueAndRawDataEncodings()
{
DataSetField[] fields =
[
new DataSetField
{
Name = "raw",
Value = new Variant(7),
Encoding = PubSubFieldEncoding.RawData
},
new DataSetField
{
Name = "dv",
Value = new Variant(8),
Encoding = PubSubFieldEncoding.DataValue,
StatusCode = StatusCodes.Good,
SourceTimestamp = new DateTimeUtc(
2026, 1, 1, 0, 0, 0)
}
];
using var buffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(buffer))
{
writer.WriteStartObject();
JsonFieldEncoder.EncodeFields(
writer, fields, null,
JsonEncodingMode.Verbose,
ServiceMessageContext.CreateEmpty(null!));
writer.WriteEndObject();
}
using var document = JsonDocument.Parse(buffer.ToArray());
JsonElement payload = document.RootElement.GetProperty("Payload");
Assert.That(payload.GetProperty("raw").ValueKind,
Is.EqualTo(JsonValueKind.Number));
JsonElement dv = payload.GetProperty("dv");
Assert.That(dv.ValueKind, Is.EqualTo(JsonValueKind.Object));
Assert.That(dv.TryGetProperty("Value", out _), Is.True);
}
[Test]
[TestSpec("7.2.5.5")]
public void WriteMetaDataRejectsNullArgs()
{
using var buffer = new MemoryStream();
using var writer = new Utf8JsonWriter(buffer);
writer.WriteStartObject();
IServiceMessageContext ctx = ServiceMessageContext.CreateEmpty(null!);
DataSetMetaDataType meta = JsonTestUtilities.CreateMetaData();
Assert.That(() => JsonMetaDataEncoder.WriteMetaData(
null!, "M", meta, JsonEncodingMode.Verbose, ctx),
Throws.ArgumentNullException);
Assert.That(() => JsonMetaDataEncoder.WriteMetaData(
writer, null!, meta, JsonEncodingMode.Verbose, ctx),
Throws.ArgumentNullException);
Assert.That(() => JsonMetaDataEncoder.WriteMetaData(
writer, "M", null!, JsonEncodingMode.Verbose, ctx),
Throws.ArgumentNullException);
Assert.That(() => JsonMetaDataEncoder.WriteMetaData(
writer, "M", meta, JsonEncodingMode.Verbose, null!),
Throws.ArgumentNullException);
}
[Test]
[TestSpec("7.2.5.5")]
public void WriteMetaDataProducesObject()
{
using var buffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(buffer))
{
writer.WriteStartObject();
JsonMetaDataEncoder.WriteMetaData(
writer, "M",
JsonTestUtilities.CreateMetaData(),
JsonEncodingMode.Verbose,
ServiceMessageContext.CreateEmpty(null!));
writer.WriteEndObject();
}
using var document = JsonDocument.Parse(buffer.ToArray());
Assert.That(document.RootElement.GetProperty("M").ValueKind,
Is.EqualTo(JsonValueKind.Object));
}
[Test]
[TestSpec("7.2.5")]
public void JsonBufferWriterAdvanceRejectsNegative()
{
using var writer = new JsonBufferWriter(64);
Assert.That(() => writer.Advance(-1),
Throws.InstanceOf<ArgumentOutOfRangeException>());
}
[Test]
[TestSpec("7.2.5")]
public void JsonBufferWriterAdvanceRejectsOverflow()
{
using var writer = new JsonBufferWriter(16);
int spanLength = writer.GetSpan(16).Length;
Assert.That(spanLength, Is.GreaterThanOrEqualTo(16));
Assert.That(() => writer.Advance(spanLength + 1),
Throws.InstanceOf<InvalidOperationException>());
}
[Test]
[TestSpec("7.2.5")]
public void JsonBufferWriterGetSpanRejectsNegativeSizeHint()
{
using var writer = new JsonBufferWriter(16);
Assert.That(() => writer.GetSpan(-1),
Throws.InstanceOf<ArgumentOutOfRangeException>());
}
[Test]
[TestSpec("7.2.5")]
public void JsonBufferWriterGrowsToFitLargePayload()
{
using var writer = new JsonBufferWriter(8);
Memory<byte> memory = writer.GetMemory(4096);
Assert.That(memory.Length, Is.GreaterThanOrEqualTo(4096));
memory.Span[..4096].Fill(0x41);
writer.Advance(4096);
Assert.That(writer.WrittenCount, Is.EqualTo(4096));
Assert.That(writer.WrittenSpan.Length, Is.EqualTo(4096));
Assert.That(writer.WrittenMemory.Length, Is.EqualTo(4096));
byte[] copied = writer.GetWritten();
Assert.That(copied, Has.Length.EqualTo(4096));
Assert.That(copied[0], Is.EqualTo((byte)0x41));
}
[Test]
[TestSpec("7.2.5")]
public void JsonBufferWriterUsesDefaultCapacityForZeroOrNegative()
{
using var writer = new JsonBufferWriter(-5);
Span<byte> span = writer.GetSpan();
Assert.That(span.Length, Is.GreaterThanOrEqualTo(1));
}
[Test]
[TestSpec("7.2.5")]
public void JsonBufferWriterDisposeIsIdempotent()
{
var writer = new JsonBufferWriter(32);
writer.Dispose();
Assert.That(writer.Dispose, Throws.Nothing);
}
[Test]
[TestSpec("7.2.5.4")]
public void DecodeFieldsRejectsNullContext()
{
using var document = JsonDocument.Parse("{}");
Assert.That(() => JsonFieldDecoder.DecodeFields(
document.RootElement, null,
JsonEncodingMode.Verbose, null!),
Throws.ArgumentNullException);
}
[Test]
[TestSpec("7.2.5.4")]
public void DecodeFieldsReturnsEmptyForNonObjectPayload()
{
using var document = JsonDocument.Parse("[1,2,3]");
ArrayOf<DataSetField> fields = JsonFieldDecoder.DecodeFields(
document.RootElement, null,
JsonEncodingMode.Verbose,
ServiceMessageContext.CreateEmpty(null!));
Assert.That(fields, Is.Empty);
}
[Test]
[TestSpec("7.2.5.4")]
public void DecodeFieldsRecognisesDataValueEnvelope()
{
const string json = """
{
"field": {
"UaType": 6,
"Value": 42,
"SourceTimestamp": "2026-01-01T00:00:00Z"
}
}
""";
using var document = JsonDocument.Parse(json);
ArrayOf<DataSetField> fields = JsonFieldDecoder.DecodeFields(
document.RootElement, null,
JsonEncodingMode.Verbose,
ServiceMessageContext.CreateEmpty(null!));
Assert.That(fields.Count, Is.EqualTo(1));
Assert.That(fields[0].Encoding,
Is.EqualTo(PubSubFieldEncoding.DataValue));
Assert.That(fields[0].Value, Is.EqualTo(new Variant(42)));
}
[Test]
[TestSpec("7.2.5.4")]
public void DecodeFieldsRecognisesPlainValueObject()
{
const string nonDataValueObject = """
{
"field": { "UaType": 6, "Value": 42 }
}
""";
using var document = JsonDocument.Parse(nonDataValueObject);
ArrayOf<DataSetField> fields = JsonFieldDecoder.DecodeFields(
document.RootElement, null,
JsonEncodingMode.Verbose,
ServiceMessageContext.CreateEmpty(null!));
Assert.That(fields.Count, Is.EqualTo(1));
Assert.That(fields[0].Encoding,
Is.EqualTo(PubSubFieldEncoding.Variant));
Assert.That(fields[0].Value, Is.EqualTo(new Variant(42)));
}
[Test]
[TestSpec("7.2.5")]
public void DecodeVariantHandlesNullElement()
{
using var document = JsonDocument.Parse("null");
Variant value = JsonVariantDecoder.DecodeVariant(
document.RootElement,
JsonEncodingMode.Verbose,
null,
ServiceMessageContext.CreateEmpty(null!));
Assert.That(value.IsNull, Is.True);
}
[Test]
[TestSpec("7.2.5")]
public void DecodeVariantRejectsNullContext()
{
using var document = JsonDocument.Parse("1");
Assert.That(() => JsonVariantDecoder.DecodeVariant(
document.RootElement,
JsonEncodingMode.Verbose,
null, null!),
Throws.ArgumentNullException);
}
[Test]
[TestSpec("7.2.5")]
public void DecodeDataValueRejectsNullContext()
{
using var document = JsonDocument.Parse("{}");
Assert.That(() => JsonVariantDecoder.DecodeDataValue(
document.RootElement, null!),
Throws.ArgumentNullException);
}
[Test]
[TestSpec("7.2.5")]
public void DecodeDataValueReturnsNullForNullElement()
{
using var document = JsonDocument.Parse("null");
DataValue value = JsonVariantDecoder.DecodeDataValue(
document.RootElement,
ServiceMessageContext.CreateEmpty(null!));
Assert.That(value.IsNull, Is.True);
}
[Test]
[TestSpec("7.2.5")]
public void DecodeVariantCompactWithoutTypeInfoReturnsNull()
{
using var document = JsonDocument.Parse("42");
Variant value = JsonVariantDecoder.DecodeVariant(
document.RootElement,
JsonEncodingMode.Compact,
null,
ServiceMessageContext.CreateEmpty(null!));
Assert.That(value.IsNull, Is.True);
}
[Test]
[TestSpec("7.2.5")]
public void DecodeVariantRawDataWithoutTypeInfoReturnsNull()
{
using var document = JsonDocument.Parse("42");
Variant value = JsonVariantDecoder.DecodeVariant(
document.RootElement,
JsonEncodingMode.RawData,
null,
ServiceMessageContext.CreateEmpty(null!));
Assert.That(value.IsNull, Is.True);
}
}
}