mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 09:06:37 +08:00
ce88955f70
# 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>
835 lines
39 KiB
C#
835 lines
39 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.Text;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
using Opc.Ua;
|
|
using Opc.Ua.PubSub.Diagnostics;
|
|
using Opc.Ua.PubSub.Encoding;
|
|
using Opc.Ua.PubSub.Encoding.Json;
|
|
using Opc.Ua.PubSub.MetaData;
|
|
using Opc.Ua.PubSub.Tests;
|
|
|
|
namespace OpcUaPubSubJsonTests
|
|
{
|
|
/// <summary>
|
|
/// Regression tests for hostile JSON frames: the decoder must
|
|
/// reject them through the diagnostics counters and never throw.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("PubSub")]
|
|
[TestSpec("7.2.5")]
|
|
public sealed class JsonDecoderHardeningTests
|
|
{
|
|
[TestCase("{\"MessageType\":\"ua-data\",\"Messages\":[{\"DataSetWriterId\":1," +
|
|
"\"MetaDataVersion\":{\"MajorVersion\":\"x\",\"MinorVersion\":[]}}]}")]
|
|
[TestCase("{\"MessageType\":\"ua-data\",\"Messages\":[{\"Status\":{\"Code\":\"x\"}}]}")]
|
|
[TestCase("{\"MessageType\":\"ua-status\",\"WriterConfiguration\":{}," +
|
|
"\"DataSetWriterIds\":[\"x\"]}")]
|
|
public void DecodeNonNumericHeaderValuesDoesNotThrow(string json)
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
|
|
Assert.DoesNotThrowAsync(async () => await DecodeAsync(json, ctx).ConfigureAwait(false));
|
|
}
|
|
|
|
[Test]
|
|
public async Task DecodeFieldBeyondStringLimitCountsFailedDataSetMessageAsync()
|
|
{
|
|
string json =
|
|
"{\"MessageType\":\"ua-data\",\"Messages\":[{\"DataSetWriterId\":1,\"Payload\":{" +
|
|
"\"f\":{\"UaType\":12,\"Value\":\"" + new string('a', 70000) + "\"}}}]}";
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.Zero);
|
|
Assert.That(JsonTestUtilities.Read(ctx,
|
|
PubSubDiagnosticsCounterKind.FailedDataSetMessages),
|
|
Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public async Task DecodeFrameAboveMaxMessageSizeIsRejectedAsync()
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
((ServiceMessageContext)ctx.MessageContext).MaxMessageSize = 64;
|
|
string json = "{\"MessageType\":\"ua-data\",\"MessageId\":\"" +
|
|
new string('m', 64) + "\",\"Messages\":[]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Null);
|
|
Assert.That(JsonTestUtilities.Read(ctx,
|
|
PubSubDiagnosticsCounterKind.ReceivedInvalidNetworkMessages),
|
|
Is.EqualTo(1));
|
|
}
|
|
|
|
[TestCase("{\"MessageType\":\"ua-data\",\"PublisherId\":\"0123456789abcdefX\",\"Messages\":[]}")]
|
|
[TestCase("{\"MessageType\":\"ua-data\",\"MessageId\":\"0123456789abcdefX\",\"Messages\":[]}")]
|
|
// 9 characters but 18 UTF-8 bytes: MaxStringLength counts bytes.
|
|
[TestCase("{\"MessageType\":\"ua-data\",\"MessageId\":\"ééééééééé\",\"Messages\":[]}")]
|
|
[TestCase("{\"MessageType\":\"ua-data\",\"ReplyTo\":[\"0123456789abcdefX\"],\"Messages\":[]}")]
|
|
[TestCase("{\"MessageType\":\"ua-data\",\"ReplyTo\":[\"\",\"\",\"\",\"\",\"\"],\"Messages\":[]}")]
|
|
[TestCase("{\"MessageType\":\"ua-data\",\"Messages\":[{},{},{},{},{}]}")]
|
|
[TestCase("[{},{},{},{},{}]")]
|
|
[TestCase("{\"MessageType\":\"ua-metadata\",\"PublisherId\":\"0123456789abcdefX\"," +
|
|
"\"DataSetWriterId\":1,\"MetaData\":{}}")]
|
|
public async Task DecodeEnvelopeAboveStringOrArrayLimitIsRejectedAsync(string json)
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
var messageContext = (ServiceMessageContext)ctx.MessageContext;
|
|
messageContext.MaxStringLength = 16;
|
|
messageContext.MaxArrayLength = 4;
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Null);
|
|
Assert.That(JsonTestUtilities.Read(ctx,
|
|
PubSubDiagnosticsCounterKind.ReceivedInvalidNetworkMessages),
|
|
Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public async Task DecodePayloadAboveMaxArrayLengthFailsDataSetMessageAsync()
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
((ServiceMessageContext)ctx.MessageContext).MaxArrayLength = 4;
|
|
const string json =
|
|
"{\"MessageType\":\"ua-data\",\"Messages\":[{\"DataSetWriterId\":1,\"Payload\":{" +
|
|
"\"a\":null,\"b\":null,\"c\":null,\"d\":null,\"e\":null}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.Zero);
|
|
Assert.That(JsonTestUtilities.Read(ctx,
|
|
PubSubDiagnosticsCounterKind.FailedDataSetMessages),
|
|
Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public async Task DecodeVerboseFieldsWithinLimitsSucceedsAsync()
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
const string json =
|
|
"{\"MessageType\":\"ua-data\",\"PublisherId\":\"P\",\"Messages\":[{\"DataSetWriterId\":1," +
|
|
"\"Payload\":{\"a\":{\"UaType\":6,\"Value\":7},\"b\":{\"UaType\":12,\"Value\":\"x\"}}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.EqualTo(1));
|
|
Assert.That(result.DataSetMessages[0].Fields[0].Value, Is.EqualTo(new Variant(7)));
|
|
Assert.That(result.DataSetMessages[0].Fields[1].Value, Is.EqualTo(new Variant("x")));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("6.2.3.2.6")]
|
|
public async Task DecodePayloadWithFieldsAppendedByMinorVersionIsAcceptedAsync()
|
|
{
|
|
// Table 11: appending fields only bumps the MinorVersion, so the
|
|
// Subscriber's older metadata of the same MajorVersion still applies.
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(
|
|
JsonTestUtilities.CreateMetaData());
|
|
const string json =
|
|
"{\"MessageType\":\"ua-data\",\"PublisherId\":\"P\",\"Messages\":[{\"DataSetWriterId\":1," +
|
|
"\"MetaDataVersion\":{\"MajorVersion\":1,\"MinorVersion\":5},\"Payload\":{" +
|
|
"\"BoolField\":true,\"IntField\":1,\"StringField\":\"s\",\"Appended\":2.5}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.EqualTo(1));
|
|
ArrayOf<DataSetField> fields = result.DataSetMessages[0].Fields;
|
|
Assert.That(fields, Has.Count.EqualTo(4));
|
|
Assert.That(fields[1].Value, Is.EqualTo(new Variant(1)));
|
|
Assert.That(fields[2].Value, Is.EqualTo(new Variant("s")));
|
|
Assert.That(fields[3].Name, Is.EqualTo("Appended"));
|
|
Assert.That(JsonTestUtilities.Read(ctx,
|
|
PubSubDiagnosticsCounterKind.FailedDataSetMessages),
|
|
Is.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public async Task DecodeRawPayloadResolvesLargeMetaDataByNameAsync()
|
|
{
|
|
const int fieldCount = 5000;
|
|
var fields = new FieldMetaData[fieldCount];
|
|
var payload = new StringBuilder();
|
|
for (int i = 0; i < fieldCount; i++)
|
|
{
|
|
fields[i] = new FieldMetaData
|
|
{
|
|
Name = "Field" + i,
|
|
BuiltInType = (byte)BuiltInType.Int32,
|
|
ValueRank = ValueRanks.Scalar
|
|
};
|
|
}
|
|
// Members arrive in reverse order so the ordinal fallback cannot match.
|
|
for (int i = fieldCount - 1; i >= 0; i--)
|
|
{
|
|
payload.Append(i == fieldCount - 1 ? string.Empty : ",")
|
|
.Append("\"Field").Append(i).Append("\":").Append(i);
|
|
}
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(new DataSetMetaDataType
|
|
{
|
|
Name = "Large",
|
|
Fields = fields,
|
|
ConfigurationVersion = new ConfigurationVersionDataType { MajorVersion = 1 }
|
|
});
|
|
string json =
|
|
"{\"MessageType\":\"ua-data\",\"PublisherId\":\"P\",\"Messages\":[{\"DataSetWriterId\":1," +
|
|
"\"MetaDataVersion\":{\"MajorVersion\":1,\"MinorVersion\":0},\"Payload\":{" +
|
|
payload + "}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.EqualTo(1));
|
|
ArrayOf<DataSetField> decoded = result.DataSetMessages[0].Fields;
|
|
Assert.That(decoded, Has.Count.EqualTo(fieldCount));
|
|
Assert.That(decoded[0].Name, Is.EqualTo("Field4999"));
|
|
Assert.That(decoded[0].Value, Is.EqualTo(new Variant(4999)));
|
|
Assert.That(decoded[fieldCount - 1].Value, Is.EqualTo(new Variant(0)));
|
|
}
|
|
|
|
[TestCase(PubSubDataSetMessageType.KeyFrame)]
|
|
[TestCase(PubSubDataSetMessageType.DeltaFrame)]
|
|
[TestCase(PubSubDataSetMessageType.KeepAlive)]
|
|
[TestSpec("7.2.5.4.1")]
|
|
public async Task HeaderlessSingleDataSetMessageWithMessageTypeAndPublisherIdRoundTripsAsync(
|
|
PubSubDataSetMessageType messageType)
|
|
{
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(
|
|
JsonTestUtilities.CreateMetaData());
|
|
var dsm = new Opc.Ua.PubSub.Encoding.Json.JsonDataSetMessage
|
|
{
|
|
DataSetWriterId = 1,
|
|
PublisherId = PublisherId.FromString("P"),
|
|
SequenceNumber = 3,
|
|
MessageType = messageType,
|
|
MetaDataVersion = new ConfigurationVersionDataType { MajorVersion = 1 },
|
|
ContentMask = JsonDataSetMessageContentMask.DataSetWriterId |
|
|
JsonDataSetMessageContentMask.PublisherId |
|
|
JsonDataSetMessageContentMask.SequenceNumber |
|
|
JsonDataSetMessageContentMask.MetaDataVersion |
|
|
JsonDataSetMessageContentMask.MessageType,
|
|
Fields = JsonTestUtilities.CreateFields()
|
|
};
|
|
var message = new Opc.Ua.PubSub.Encoding.Json.JsonNetworkMessage
|
|
{
|
|
ContentMask = JsonNetworkMessageContentMask.DataSetMessageHeader |
|
|
JsonNetworkMessageContentMask.SingleDataSetMessage,
|
|
DataSetMessages = [dsm]
|
|
};
|
|
ReadOnlyMemory<byte> bytes = await new Opc.Ua.PubSub.Encoding.Json.JsonEncoder()
|
|
.EncodeAsync(message, ctx).ConfigureAwait(false);
|
|
|
|
PubSubNetworkMessage? result = await new Opc.Ua.PubSub.Encoding.Json.JsonDecoder()
|
|
.TryDecodeAsync(bytes, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null, JsonTestUtilities.ToText(bytes));
|
|
Assert.That(result!.PublisherId, Is.EqualTo(PublisherId.FromString("P")));
|
|
Assert.That(result.DataSetMessages, Has.Count.EqualTo(1));
|
|
Assert.That(result.DataSetMessages[0].MessageType, Is.EqualTo(messageType));
|
|
Assert.That(result.DataSetMessages[0].Fields,
|
|
Has.Count.EqualTo(messageType == PubSubDataSetMessageType.KeepAlive ? 0 : 3));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.5.4.2")]
|
|
public async Task DataSetMessageWithOnlyMinorVersionResolvesRegisteredMetaDataAsync()
|
|
{
|
|
var registry = new DataSetMetaDataRegistry();
|
|
var key = new DataSetMetaDataKey(
|
|
PublisherId.FromString("MyPublisher"), 0, 102, Uuid.Empty, 672341762);
|
|
registry.Register(in key, new DataSetMetaDataType
|
|
{
|
|
Name = "Location",
|
|
Fields =
|
|
[
|
|
new FieldMetaData
|
|
{
|
|
Name = "LocationName",
|
|
BuiltInType = (byte)BuiltInType.String,
|
|
ValueRank = ValueRanks.Scalar
|
|
}
|
|
],
|
|
ConfigurationVersion = new ConfigurationVersionDataType
|
|
{
|
|
MajorVersion = 672341762,
|
|
MinorVersion = 672341762
|
|
}
|
|
});
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext(registry);
|
|
const string json =
|
|
"{\"PublisherId\":\"MyPublisher\",\"DataSetWriterId\":102,\"SequenceNumber\":25460," +
|
|
"\"MinorVersion\":672341762,\"Timestamp\":\"2021-09-27T18:45:19.555Z\"," +
|
|
"\"Payload\":{\"LocationName\":\"Building A\"}}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.EqualTo(1));
|
|
Assert.That(result.DataSetMessages[0].Fields[0].Value, Is.EqualTo(new Variant("Building A")));
|
|
}
|
|
|
|
[TestCase(9u, false)]
|
|
[TestCase(10u, true)]
|
|
[TestCase(12u, true)]
|
|
[TestSpec("6.2.3.2.6")]
|
|
public async Task MinorVersionOnlyMessageOlderThanRegisteredMajorVersionIsNotDecodedAsync(
|
|
uint minorVersion,
|
|
bool decoded)
|
|
{
|
|
// Table 11: a MajorVersion change sets the MinorVersion to the same
|
|
// value, so a message MinorVersion below the registered MajorVersion
|
|
// was produced with an older layout.
|
|
DataSetMetaDataType meta = JsonTestUtilities.CreateMetaData();
|
|
meta.ConfigurationVersion = new ConfigurationVersionDataType
|
|
{
|
|
MajorVersion = 10,
|
|
MinorVersion = 10
|
|
};
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(meta);
|
|
string json =
|
|
"{\"MessageType\":\"ua-data\",\"PublisherId\":\"P\",\"Messages\":[{\"DataSetWriterId\":1," +
|
|
"\"MinorVersion\":" + minorVersion.ToString(System.Globalization.CultureInfo.InvariantCulture) +
|
|
",\"Payload\":{\"BoolField\":true,\"IntField\":1,\"StringField\":\"s\"}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.EqualTo(decoded ? 1 : 0));
|
|
Assert.That(JsonTestUtilities.Read(ctx,
|
|
PubSubDiagnosticsCounterKind.ResolverErrors),
|
|
Is.EqualTo(decoded ? 0 : 1));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.5.4.2")]
|
|
public async Task VerboseCollapsedAndEnvelopedVariantsRoundTripAsync()
|
|
{
|
|
DataSetMetaDataType meta = JsonTestUtilities.CreateMetaData();
|
|
meta.Fields =
|
|
[
|
|
.. meta.Fields.ToArray()!,
|
|
new FieldMetaData
|
|
{
|
|
Name = "AnyField",
|
|
BuiltInType = (byte)BuiltInType.Variant,
|
|
ValueRank = ValueRanks.Scalar
|
|
}
|
|
];
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(meta);
|
|
var dsm = new Opc.Ua.PubSub.Encoding.Json.JsonDataSetMessage
|
|
{
|
|
DataSetWriterId = 1,
|
|
MetaDataVersion = new ConfigurationVersionDataType { MajorVersion = 1 },
|
|
Fields =
|
|
[
|
|
.. JsonTestUtilities.CreateFields().ToArray()!,
|
|
new DataSetField { Name = "AnyField", Value = new Variant(2.5) }
|
|
]
|
|
};
|
|
var message = new Opc.Ua.PubSub.Encoding.Json.JsonNetworkMessage
|
|
{
|
|
PublisherId = PublisherId.FromString("P"),
|
|
DataSetMessages = [dsm]
|
|
};
|
|
ReadOnlyMemory<byte> bytes = await new Opc.Ua.PubSub.Encoding.Json.JsonEncoder()
|
|
.EncodeAsync(message, ctx).ConfigureAwait(false);
|
|
string text = JsonTestUtilities.ToText(bytes);
|
|
|
|
PubSubNetworkMessage? result = await new Opc.Ua.PubSub.Encoding.Json.JsonDecoder()
|
|
.TryDecodeAsync(bytes, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(text, Does.Contain("\"IntField\":42"), text);
|
|
Assert.That(text, Does.Contain("\"AnyField\":{\"UaType\":11,\"Value\":2.5}"), text);
|
|
Assert.That(result, Is.Not.Null);
|
|
ArrayOf<DataSetField> fields = result!.DataSetMessages[0].Fields;
|
|
Assert.That(fields[0].Value, Is.EqualTo(new Variant(true)));
|
|
Assert.That(fields[1].Value, Is.EqualTo(new Variant(42)));
|
|
Assert.That(fields[2].Value, Is.EqualTo(new Variant("hello")));
|
|
Assert.That(fields[3].Value, Is.EqualTo(new Variant(2.5)));
|
|
Assert.That(fields[3].Encoding, Is.EqualTo(PubSubFieldEncoding.Variant));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("6.3.2.3.1")]
|
|
public void DeprecatedReversibleTypeBodyVariantIsDecoded()
|
|
{
|
|
// Table 112 FieldEncoding1=True/FieldEncoding2=False still defines
|
|
// the deprecated ReversibleFieldEncoding.
|
|
using var document = System.Text.Json.JsonDocument.Parse(
|
|
"{\"field\":{\"Type\":6,\"Body\":42},\"dv\":{\"Value\":{\"Type\":12,\"Body\":\"x\"}," +
|
|
"\"SourceTimestamp\":\"2021-09-27T11:32:38.349Z\"}}");
|
|
|
|
ArrayOf<DataSetField> fields = JsonFieldDecoder.DecodeFields(
|
|
document.RootElement,
|
|
metaData: null,
|
|
JsonEncodingMode.Verbose,
|
|
ServiceMessageContext.CreateEmpty(null!));
|
|
|
|
Assert.That(fields[0].Value, Is.EqualTo(new Variant(42)));
|
|
Assert.That(fields[1].Value, Is.EqualTo(new Variant("x")));
|
|
Assert.That(fields[1].Encoding, Is.EqualTo(PubSubFieldEncoding.DataValue));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("6.3.2.3.1")]
|
|
public async Task DeprecatedReversibleTypeBodyPayloadIsDecodedAsync()
|
|
{
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(
|
|
JsonTestUtilities.CreateMetaData());
|
|
const string json =
|
|
"{\"MessageType\":\"ua-data\",\"PublisherId\":\"P\",\"Messages\":[{\"DataSetWriterId\":1," +
|
|
"\"Payload\":{\"BoolField\":{\"Type\":1,\"Body\":true},\"IntField\":{\"Type\":6,\"Body\":5}," +
|
|
"\"StringField\":{\"Type\":12,\"Body\":\"s\"}}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.EqualTo(1));
|
|
Assert.That(result.DataSetMessages[0].Fields[1].Value, Is.EqualTo(new Variant(5)));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.5.4.2")]
|
|
public async Task VerboseEnumerationFieldIsEncodedAsNameValueStringAsync()
|
|
{
|
|
var enumType = new NodeId(3001, 1);
|
|
DataSetMetaDataType meta = JsonTestUtilities.CreateMetaData();
|
|
meta.Fields =
|
|
[
|
|
.. meta.Fields.ToArray()!,
|
|
new FieldMetaData
|
|
{
|
|
Name = "State",
|
|
BuiltInType = (byte)BuiltInType.Int32,
|
|
DataType = enumType,
|
|
ValueRank = ValueRanks.Scalar
|
|
},
|
|
new FieldMetaData
|
|
{
|
|
Name = "ServerState",
|
|
BuiltInType = (byte)BuiltInType.Int32,
|
|
DataType = DataTypeIds.ServerState,
|
|
ValueRank = ValueRanks.Scalar
|
|
},
|
|
new FieldMetaData
|
|
{
|
|
Name = "States",
|
|
BuiltInType = (byte)BuiltInType.Int32,
|
|
DataType = enumType,
|
|
ValueRank = ValueRanks.OneDimension
|
|
}
|
|
];
|
|
meta.EnumDataTypes =
|
|
[
|
|
new EnumDescription
|
|
{
|
|
DataTypeId = enumType,
|
|
Name = new QualifiedName("MachineState", 1),
|
|
EnumDefinition = new EnumDefinition
|
|
{
|
|
Fields =
|
|
[
|
|
new EnumField { Name = "Running", Value = 0 },
|
|
new EnumField { Name = "Suspended", Value = 3 }
|
|
]
|
|
}
|
|
}
|
|
];
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(meta);
|
|
ArrayOf<int> states = [0, 3, 7];
|
|
var dsm = new Opc.Ua.PubSub.Encoding.Json.JsonDataSetMessage
|
|
{
|
|
DataSetWriterId = 1,
|
|
MetaDataVersion = new ConfigurationVersionDataType { MajorVersion = 1 },
|
|
Fields =
|
|
[
|
|
.. JsonTestUtilities.CreateFields().ToArray()!,
|
|
new DataSetField { Name = "State", Value = new Variant(3) },
|
|
new DataSetField { Name = "ServerState", Value = new Variant(3) },
|
|
new DataSetField { Name = "States", Value = new Variant(states) }
|
|
]
|
|
};
|
|
var message = new Opc.Ua.PubSub.Encoding.Json.JsonNetworkMessage
|
|
{
|
|
PublisherId = PublisherId.FromString("P"),
|
|
DataSetMessages = [dsm]
|
|
};
|
|
ReadOnlyMemory<byte> bytes = await new Opc.Ua.PubSub.Encoding.Json.JsonEncoder()
|
|
.EncodeAsync(message, ctx).ConfigureAwait(false);
|
|
string text = JsonTestUtilities.ToText(bytes);
|
|
|
|
PubSubNetworkMessage? result = await new Opc.Ua.PubSub.Encoding.Json.JsonDecoder()
|
|
.TryDecodeAsync(bytes, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(text, Does.Contain("\"State\":\"Suspended_3\""), text);
|
|
// Without a known name the numeric value is encoded as a JSON string.
|
|
Assert.That(text, Does.Contain("\"ServerState\":\"3\""), text);
|
|
Assert.That(text, Does.Contain("\"States\":[\"Running_0\",\"Suspended_3\",\"7\"]"), text);
|
|
Assert.That(result, Is.Not.Null, text);
|
|
ArrayOf<DataSetField> fields = result!.DataSetMessages[0].Fields;
|
|
Assert.That(fields[3].Value, Is.EqualTo(new Variant(3)));
|
|
Assert.That(fields[4].Value, Is.EqualTo(new Variant(3)));
|
|
Assert.That(fields[5].Value, Is.EqualTo(new Variant(states)));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.5.4.3")]
|
|
public async Task VerboseFieldsWithoutUaTypeAreTypedByFieldMetaDataAsync()
|
|
{
|
|
DataSetMetaDataType meta = JsonTestUtilities.CreateMetaData();
|
|
meta.Fields =
|
|
[
|
|
NewField("LocationName", BuiltInType.String, ValueRanks.Scalar),
|
|
NewField("Suspended", BuiltInType.Int32, ValueRanks.Scalar, DataTypeIds.ServerState),
|
|
NewField("Range", BuiltInType.ExtensionObject, ValueRanks.Scalar, DataTypeIds.Range),
|
|
NewField("Matrix", BuiltInType.Int32, ValueRanks.TwoDimensions),
|
|
NewField("AnyScalar", BuiltInType.Int32, ValueRanks.Any),
|
|
NewField("ScalarOrArray", BuiltInType.Int32, ValueRanks.ScalarOrOneDimension),
|
|
NewField("Ranges", BuiltInType.ExtensionObject, ValueRanks.OneDimension, DataTypeIds.Range)
|
|
];
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(meta);
|
|
// Part 14 7.2.5.4.2 second example and Table 186 / Table 187 rows.
|
|
const string json =
|
|
"{\"MessageType\":\"ua-data\",\"PublisherId\":\"P\",\"Messages\":[{\"DataSetWriterId\":1," +
|
|
"\"Payload\":{\"LocationName\":{\"Value\":\"Building A\"," +
|
|
"\"Status\":{\"Code\":1073741824,\"Symbol\":\"Uncertain\"}," +
|
|
"\"SourceTimestamp\":\"2021-09-27T11:32:38.349925Z\"}," +
|
|
"\"Suspended\":\"Suspended_3\"," +
|
|
"\"Range\":{\"Low\":1,\"High\":2}," +
|
|
"\"Matrix\":{\"Value\":[1,2,3,4],\"Dimensions\":[2,2]}," +
|
|
"\"AnyScalar\":{\"Value\":11}," +
|
|
"\"ScalarOrArray\":[1,2,3,4]," +
|
|
"\"Ranges\":[{\"Low\":1,\"High\":2}]}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.EqualTo(1));
|
|
ArrayOf<DataSetField> fields = result.DataSetMessages[0].Fields;
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(fields[0].Value, Is.EqualTo(new Variant("Building A")));
|
|
Assert.That(fields[0].StatusCode, Is.EqualTo((StatusCode)StatusCodes.Uncertain));
|
|
Assert.That(fields[0].SourceTimestamp, Is.Not.EqualTo(DateTimeUtc.MinValue));
|
|
Assert.That(fields[0].Encoding, Is.EqualTo(PubSubFieldEncoding.DataValue));
|
|
Assert.That(fields[1].Value, Is.EqualTo(new Variant(3)));
|
|
Assert.That(fields[2].Value.TryGetValue(out ExtensionObject range), Is.True);
|
|
Assert.That(range.IsNull, Is.False);
|
|
Assert.That(range.TypeId.IsNull, Is.False);
|
|
Assert.That(fields[3].Value.TypeInfo.ValueRank, Is.EqualTo(2));
|
|
Assert.That(fields[4].Value, Is.EqualTo(new Variant(11)));
|
|
Assert.That(fields[5].Value.GetInt32Array(), Has.Count.EqualTo(4));
|
|
Assert.That(fields[6].Value.IsNull, Is.False);
|
|
Assert.That(fields[6].Value.TypeInfo.BuiltInType, Is.EqualTo(BuiltInType.ExtensionObject));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.5.4.3")]
|
|
public async Task VerboseAbstractFieldWithoutUaTypeIsRejectedAsync()
|
|
{
|
|
DataSetMetaDataType meta = JsonTestUtilities.CreateMetaData();
|
|
meta.Fields = [NewField("Any", BuiltInType.Variant, ValueRanks.Scalar)];
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(meta);
|
|
const string json =
|
|
"{\"MessageType\":\"ua-data\",\"PublisherId\":\"P\",\"Messages\":[{\"DataSetWriterId\":1," +
|
|
"\"Payload\":{\"Any\":{\"Value\":\"Apple\"}}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.Zero);
|
|
Assert.That(JsonTestUtilities.Read(ctx,
|
|
PubSubDiagnosticsCounterKind.FailedDataSetMessages),
|
|
Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.5.4.3")]
|
|
public async Task VerboseDataValueFieldOmitsUaTypeForConcreteTypeAsync()
|
|
{
|
|
DataSetMetaDataType meta = JsonTestUtilities.CreateMetaData();
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(meta);
|
|
var sourceTimestamp = (DateTimeUtc)new DateTime(2025, 5, 26, 11, 20, 7, 951, DateTimeKind.Utc);
|
|
var dsm = new Opc.Ua.PubSub.Encoding.Json.JsonDataSetMessage
|
|
{
|
|
DataSetWriterId = 1,
|
|
MetaDataVersion = new ConfigurationVersionDataType { MajorVersion = 1 },
|
|
Fields =
|
|
[
|
|
new DataSetField
|
|
{
|
|
Name = "BoolField",
|
|
Value = new Variant(true),
|
|
Encoding = PubSubFieldEncoding.DataValue
|
|
},
|
|
new DataSetField
|
|
{
|
|
Name = "IntField",
|
|
Value = new Variant(1234),
|
|
SourceTimestamp = sourceTimestamp,
|
|
Encoding = PubSubFieldEncoding.DataValue
|
|
},
|
|
new DataSetField
|
|
{
|
|
Name = "StringField",
|
|
Value = new Variant("Apple"),
|
|
Encoding = PubSubFieldEncoding.DataValue
|
|
}
|
|
]
|
|
};
|
|
var message = new Opc.Ua.PubSub.Encoding.Json.JsonNetworkMessage
|
|
{
|
|
PublisherId = PublisherId.FromString("P"),
|
|
DataSetMessages = [dsm]
|
|
};
|
|
ReadOnlyMemory<byte> bytes = await new Opc.Ua.PubSub.Encoding.Json.JsonEncoder()
|
|
.EncodeAsync(message, ctx).ConfigureAwait(false);
|
|
string text = JsonTestUtilities.ToText(bytes);
|
|
|
|
PubSubNetworkMessage? result = await new Opc.Ua.PubSub.Encoding.Json.JsonDecoder()
|
|
.TryDecodeAsync(bytes, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(text, Does.Contain(
|
|
"\"IntField\":{\"Value\":1234,\"SourceTimestamp\":\"2025-05-26T11:20:07.951Z\"}"), text);
|
|
Assert.That(text, Does.Not.Contain("UaType"), text);
|
|
Assert.That(result, Is.Not.Null, text);
|
|
ArrayOf<DataSetField> fields = result!.DataSetMessages[0].Fields;
|
|
Assert.That(fields[1].Value, Is.EqualTo(new Variant(1234)));
|
|
Assert.That(fields[1].SourceTimestamp, Is.EqualTo(sourceTimestamp));
|
|
Assert.That(fields[2].Value, Is.EqualTo(new Variant("Apple")));
|
|
Assert.That(fields[2].Encoding, Is.EqualTo(PubSubFieldEncoding.DataValue));
|
|
}
|
|
|
|
private static FieldMetaData NewField(
|
|
string name,
|
|
BuiltInType builtInType,
|
|
int valueRank,
|
|
NodeId dataType = default)
|
|
{
|
|
return new FieldMetaData
|
|
{
|
|
Name = name,
|
|
BuiltInType = (byte)builtInType,
|
|
ValueRank = valueRank,
|
|
DataType = dataType
|
|
};
|
|
}
|
|
|
|
[TestCase(JsonEncodingMode.Verbose, "{\"Code\":1073741824,\"Symbol\":\"Uncertain\"}")]
|
|
[TestCase(JsonEncodingMode.Compact, "{\"Code\":1073741824}")]
|
|
[TestSpec("7.2.5.4.1")]
|
|
public async Task DataSetMessageStatusIsEncodedAsStatusCodeObjectAsync(
|
|
JsonEncodingMode mode,
|
|
string expected)
|
|
{
|
|
PubSubNetworkMessageContext ctx = NewContextWithMetaData(
|
|
JsonTestUtilities.CreateMetaData());
|
|
var dsm = new Opc.Ua.PubSub.Encoding.Json.JsonDataSetMessage
|
|
{
|
|
DataSetWriterId = 1,
|
|
Status = StatusCodes.Uncertain,
|
|
MetaDataVersion = new ConfigurationVersionDataType { MajorVersion = 1 },
|
|
ContentMask = JsonDataSetMessageContentMask.DataSetWriterId |
|
|
JsonDataSetMessageContentMask.MetaDataVersion |
|
|
JsonDataSetMessageContentMask.Status,
|
|
Fields = JsonTestUtilities.CreateFields()
|
|
};
|
|
var message = new Opc.Ua.PubSub.Encoding.Json.JsonNetworkMessage
|
|
{
|
|
PublisherId = PublisherId.FromString("P"),
|
|
DataSetMessages = [dsm]
|
|
};
|
|
ReadOnlyMemory<byte> bytes = await new Opc.Ua.PubSub.Encoding.Json.JsonEncoder(mode)
|
|
.EncodeAsync(message, ctx).ConfigureAwait(false);
|
|
string text = JsonTestUtilities.ToText(bytes);
|
|
|
|
PubSubNetworkMessage? result = await new Opc.Ua.PubSub.Encoding.Json.JsonDecoder()
|
|
.TryDecodeAsync(bytes, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(text, Does.Contain("\"Status\":" + expected), text);
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(
|
|
((Opc.Ua.PubSub.Encoding.Json.JsonDataSetMessage)result!.DataSetMessages[0]).Status,
|
|
Is.EqualTo((StatusCode)StatusCodes.Uncertain));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.5.4.1")]
|
|
public async Task DataSetMessageLegacyNumericStatusIsAcceptedAsync()
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
const string json =
|
|
"{\"MessageType\":\"ua-data\",\"Messages\":[{\"DataSetWriterId\":1,\"Status\":1073741824," +
|
|
"\"Payload\":{\"a\":{\"UaType\":6,\"Value\":7}}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.EqualTo(1));
|
|
Assert.That(
|
|
((Opc.Ua.PubSub.Encoding.Json.JsonDataSetMessage)result.DataSetMessages[0]).Status,
|
|
Is.EqualTo((StatusCode)StatusCodes.Uncertain));
|
|
}
|
|
|
|
[TestCase("\"Uncertain\"")]
|
|
[TestCase("[1073741824]")]
|
|
[TestCase("-1")]
|
|
[TestSpec("7.2.5.4.1")]
|
|
public async Task DataSetMessageMalformedStatusIsRejectedAsync(string status)
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
string json =
|
|
"{\"MessageType\":\"ua-data\",\"Messages\":[{\"DataSetWriterId\":1,\"Status\":" + status +
|
|
",\"Payload\":{\"a\":{\"UaType\":6,\"Value\":7}}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.DataSetMessages, Has.Count.Zero);
|
|
Assert.That(JsonTestUtilities.Read(ctx,
|
|
PubSubDiagnosticsCounterKind.FailedDataSetMessages),
|
|
Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.4.4.2")]
|
|
public async Task GuidShapedStringPublisherIdStaysStringAsync()
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
const string publisherId = "3f2504e0-4f89-11d3-9a0c-0305e82c3301";
|
|
const string json =
|
|
"{\"MessageType\":\"ua-data\",\"PublisherId\":\"" + publisherId + "\",\"Messages\":[" +
|
|
"{\"DataSetWriterId\":1,\"Payload\":{\"a\":{\"UaType\":6,\"Value\":7}}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
Assert.That(result!.PublisherId, Is.EqualTo(PublisherId.FromString(publisherId)));
|
|
}
|
|
|
|
[TestCase("2021-09-27T18:45:19.555Z", "2021-09-27T18:45:19.5550000Z")]
|
|
[TestCase("2021-09-27T18:45:19.555", "2021-09-27T18:45:19.5550000Z")]
|
|
[TestCase("2021-09-27T20:45:19.555+02:00", "2021-09-27T18:45:19.5550000Z")]
|
|
[TestCase("9/27/2021", null)]
|
|
[TestSpec("7.2.5.4.1")]
|
|
public async Task DataSetMessageTimestampIsParsedAsIso8601UtcAsync(string wire, string? expected)
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
string json =
|
|
"{\"MessageType\":\"ua-data\",\"Messages\":[{\"DataSetWriterId\":1,\"Timestamp\":\"" + wire +
|
|
"\",\"Payload\":{\"a\":{\"UaType\":6,\"Value\":7}}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Not.Null);
|
|
DateTimeUtc timestamp =
|
|
((Opc.Ua.PubSub.Encoding.Json.JsonDataSetMessage)result!.DataSetMessages[0]).Timestamp;
|
|
if (expected is null)
|
|
{
|
|
Assert.That(timestamp, Is.EqualTo(DateTimeUtc.MinValue));
|
|
return;
|
|
}
|
|
Assert.That(
|
|
((DateTime)timestamp).ToString("o", System.Globalization.CultureInfo.InvariantCulture),
|
|
Is.EqualTo(expected));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.5.6")]
|
|
public async Task ActionRequestEnvelopeWithResponseEntryIsRejectedAsync()
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
const string json =
|
|
"{\"MessageId\":\"a\",\"MessageType\":\"ua-action-request\",\"PublisherId\":\"P\"," +
|
|
"\"Messages\":[{\"DataSetWriterId\":1,\"RequestId\":1}," +
|
|
"{\"DataSetWriterId\":1,\"RequestId\":2,\"Status\":{\"Code\":2147483648}}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
Assert.That(result, Is.Null);
|
|
Assert.That(JsonTestUtilities.Read(ctx,
|
|
PubSubDiagnosticsCounterKind.ReceivedInvalidNetworkMessages),
|
|
Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
[TestSpec("7.2.5.6")]
|
|
public async Task ActionResponseEnvelopeDecodesEveryEntryAsResponseAsync()
|
|
{
|
|
PubSubNetworkMessageContext ctx = JsonTestUtilities.NewContext();
|
|
const string json =
|
|
"{\"MessageId\":\"a\",\"MessageType\":\"ua-action-response\",\"PublisherId\":\"P\"," +
|
|
"\"Messages\":[{\"DataSetWriterId\":1,\"RequestId\":7}]}";
|
|
|
|
PubSubNetworkMessage? result = await DecodeAsync(json, ctx).ConfigureAwait(false);
|
|
|
|
var action = result as Opc.Ua.PubSub.Encoding.Json.JsonActionNetworkMessage;
|
|
Assert.That(action, Is.Not.Null);
|
|
Assert.That(action!.Messages, Has.Count.EqualTo(1));
|
|
Assert.That(action.Messages[0].TryGetValue(out Opc.Ua.JsonActionResponseMessage? response), Is.True);
|
|
Assert.That(response!.RequestId, Is.EqualTo((ushort)7));
|
|
Assert.That(action.MessageId, Is.EqualTo("a"));
|
|
}
|
|
|
|
private static PubSubNetworkMessageContext NewContextWithMetaData(
|
|
DataSetMetaDataType metaData)
|
|
{
|
|
var registry = new DataSetMetaDataRegistry();
|
|
var key = new DataSetMetaDataKey(
|
|
PublisherId.FromString("P"),
|
|
0,
|
|
1,
|
|
Uuid.Empty,
|
|
metaData.ConfigurationVersion.MajorVersion);
|
|
registry.Register(in key, metaData);
|
|
return JsonTestUtilities.NewContext(registry);
|
|
}
|
|
|
|
private static async Task<PubSubNetworkMessage?> DecodeAsync(
|
|
string json,
|
|
PubSubNetworkMessageContext ctx)
|
|
{
|
|
var decoder = new Opc.Ua.PubSub.Encoding.Json.JsonDecoder();
|
|
return await decoder.TryDecodeAsync(Encoding.UTF8.GetBytes(json), ctx)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|