From 941955d6b8b74c6d4963fabaf91e4a09cf2bba8e Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Tue, 9 Dec 2025 13:58:29 +0200 Subject: [PATCH 1/2] Only treat an actual StatusCode value equal to Good as null to avoid misencoding --- .../Encoding/JsonDataSetMessage.cs | 8 +- .../Encoding/JsonDataSetMessageTests.cs | 344 ++++++++++++++++++ 2 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs diff --git a/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs b/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs index b3918d5624..909056afcd 100644 --- a/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs +++ b/Libraries/Opc.Ua.PubSub/Encoding/JsonDataSetMessage.cs @@ -499,8 +499,12 @@ private void EncodeField(IJsonEncoder encoder, Field field) Variant valueToEncode = field.Value.WrappedValue; - // The StatusCode.Good value is not encoded correctly then it shall be committed - if (valueToEncode == StatusCodes.Good && + // Only treat an actual StatusCode value equal to Good as null to avoid misencoding + bool isStatusCodeValue = + valueToEncode.TypeInfo?.BuiltInType == BuiltInType.StatusCode || + valueToEncode.Value is StatusCode; + if (isStatusCodeValue && + valueToEncode == StatusCodes.Good && m_fieldTypeEncoding != FieldTypeEncodingMask.Variant) { valueToEncode = Variant.Null; diff --git a/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs b/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs new file mode 100644 index 0000000000..1160cb30bf --- /dev/null +++ b/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs @@ -0,0 +1,344 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 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 Newtonsoft.Json.Linq; +using NUnit.Framework; +using Opc.Ua; +using Opc.Ua.PubSub.PublishedData; +using Opc.Ua.Tests; +using PubSubEncoding = Opc.Ua.PubSub.Encoding; + +namespace Opc.Ua.PubSub.Tests.Encoding +{ + /// + /// Tests for JsonDataSetMessage encoding behavior. + /// Validates correct handling of zero values vs StatusCode.Good per OPC UA Part 6 specification. + /// + /// Note: JsonDataSetMessage currently only supports Reversible and NonReversible encoding modes. + /// Compact and Verbose encoding modes are not yet supported for PubSub messages because + /// the encoder throws when trying to modify ForceNamespaceUri property with these modes. + /// + [TestFixture] + [Parallelizable] + public class JsonDataSetMessageTests + { + #region Regression Tests - UInt32 Zero Value Preservation (DataValue Mode) + + /// + /// Regression test: UInt32 value of 0 must not be confused with StatusCode.Good + /// and must be preserved in DataValue mode with Reversible encoding. + /// + [Test] + public void EncodeUInt32ZeroPreservesValueInDataValueModeReversible() + { + var field = CreateField("TestField", BuiltInType.UInt32, (uint)0); + var message = CreateDataValueMessage(field); + + var json = EncodeMessage(message, JsonEncodingType.Reversible); + var fieldObj = GetPayloadField(json, "TestField"); + + Assert.That(fieldObj, Is.Not.Null, "Field should be encoded."); + Assert.That(fieldObj["Value"]?.Value(), Is.EqualTo(0u), + "UInt32 zero value must be preserved in Reversible encoding."); + } + + /// + /// Regression test: UInt32 value of 0 must not be confused with StatusCode.Good + /// and must be preserved in DataValue mode with NonReversible encoding. + /// + [Test] + public void EncodeUInt32ZeroPreservesValueInDataValueModeNonReversible() + { + var field = CreateField("TestField", BuiltInType.UInt32, (uint)0); + var message = CreateDataValueMessage(field); + + var json = EncodeMessage(message, JsonEncodingType.NonReversible); + var fieldObj = GetPayloadField(json, "TestField"); + + Assert.That(fieldObj, Is.Not.Null, "Field should be encoded."); + Assert.That(fieldObj["Value"]?.Value(), Is.EqualTo(0u), + "UInt32 zero value must be preserved in NonReversible encoding."); + } + + #endregion + + #region Regression Tests - UInt32 Zero Value Preservation (RawData Mode) + + /// + /// Regression test: UInt32 value of 0 must be preserved in RawData mode. + /// Per OPC 10000-6: RawData uses non-reversible encoding for the value itself. + /// + [Test] + public void EncodeUInt32ZeroPreservesValueInRawDataModeReversible() + { + var field = CreateField("TestField", BuiltInType.UInt32, (uint)0); + + var message = new PubSubEncoding.JsonDataSetMessage(new DataSet { Fields = [field] }); + message.SetFieldContentMask(DataSetFieldContentMask.RawData); + + var json = EncodeMessage(message, JsonEncodingType.Reversible); + var root = JObject.Parse(json); + var payload = (root["Payload"] as JObject) ?? root; + + Assert.That(payload["TestField"]?.Value(), Is.EqualTo(0u), + "UInt32 zero value must be preserved in RawData mode with Reversible encoding."); + } + + /// + /// Regression test: UInt32 value of 0 must be preserved in RawData mode with NonReversible encoding. + /// + [Test] + public void EncodeUInt32ZeroPreservesValueInRawDataModeNonReversible() + { + var field = CreateField("TestField", BuiltInType.UInt32, (uint)0); + + var message = new PubSubEncoding.JsonDataSetMessage(new DataSet { Fields = [field] }); + message.SetFieldContentMask(DataSetFieldContentMask.RawData); + + var json = EncodeMessage(message, JsonEncodingType.NonReversible); + var root = JObject.Parse(json); + var payload = (root["Payload"] as JObject) ?? root; + + Assert.That(payload["TestField"]?.Value(), Is.EqualTo(0u), + "UInt32 zero value must be preserved in RawData mode with NonReversible encoding."); + } + + #endregion + + #region Regression Tests - UInt32 Zero Value Preservation (Variant Mode) + + /// + /// In Variant mode (FieldContentMask.None), values are encoded with type information. + /// UInt32 zero should still be preserved as it's a valid value. + /// Per OPC 10000-6: Variant mode uses reversible encoding with Type/Body structure. + /// + [Test] + public void EncodeUInt32ZeroPreservesValueInVariantModeReversible() + { + var field = CreateField("TestField", BuiltInType.UInt32, (uint)0); + + var message = new PubSubEncoding.JsonDataSetMessage(new DataSet { Fields = [field] }); + message.SetFieldContentMask(DataSetFieldContentMask.None); // Variant mode + + var json = EncodeMessage(message, JsonEncodingType.Reversible); + var root = JObject.Parse(json); + var payload = (root["Payload"] as JObject) ?? root; + + // In Variant mode with Reversible encoding, format is { "Type": 7, "Body": 0 } + var variantObj = payload["TestField"] as JObject; + Assert.That(variantObj, Is.Not.Null, "Field should be encoded as Variant object."); + Assert.That(variantObj["Body"]?.Value(), Is.EqualTo(0u), + "UInt32 zero value must be preserved in Variant Body."); + } + + #endregion + + #region StatusCode.Good Encoding Tests + + /// + /// Verify that a real StatusCode.Good value results in null/omitted Value + /// in DataValue mode per spec: "The Code is omitted if the numeric code is 0 (Good)." + /// + [Test] + public void EncodeStatusCodeGoodResultsInNullValueInDataValueModeReversible() + { + var field = CreateStatusCodeField("StatusField", StatusCodes.Good); + var message = CreateDataValueMessage(field); + + var json = EncodeMessage(message, JsonEncodingType.Reversible); + var root = JObject.Parse(json); + var payload = (root["Payload"] as JObject) ?? root; + + var fieldObj = payload["StatusField"] as JObject; + Assert.That(fieldObj, Is.Not.Null, "Field should be present."); + + // The Value field should be omitted entirely (StatusCode.Good is intentionally nulled) + Assert.That(fieldObj["Value"], Is.Null, + "StatusCode.Good should result in omitted Value in Reversible DataValue mode."); + } + + /// + /// Verify that a real StatusCode.Good value results in null/omitted Value + /// in DataValue mode with NonReversible encoding. + /// + [Test] + public void EncodeStatusCodeGoodResultsInNullValueInDataValueModeNonReversible() + { + var field = CreateStatusCodeField("StatusField", StatusCodes.Good); + var message = CreateDataValueMessage(field); + + var json = EncodeMessage(message, JsonEncodingType.NonReversible); + var root = JObject.Parse(json); + var payload = (root["Payload"] as JObject) ?? root; + + var fieldObj = payload["StatusField"] as JObject; + Assert.That(fieldObj, Is.Not.Null, "Field should be present."); + + // The Value field should be omitted entirely (StatusCode.Good is intentionally nulled) + Assert.That(fieldObj["Value"], Is.Null, + "StatusCode.Good should result in omitted Value in NonReversible DataValue mode."); + } + + /// + /// Verify that a non-Good StatusCode value is preserved in Reversible encoding. + /// + [Test] + public void EncodeStatusCodeBadPreservesValueReversible() + { + var field = CreateStatusCodeField("StatusField", StatusCodes.BadInvalidArgument); + var message = CreateDataValueMessage(field); + + var json = EncodeMessage(message, JsonEncodingType.Reversible); + var root = JObject.Parse(json); + var payload = (root["Payload"] as JObject) ?? root; + + var fieldObj = payload["StatusField"] as JObject; + Assert.That(fieldObj, Is.Not.Null, "Field should be present."); + + // A bad StatusCode should be encoded + var valueToken = fieldObj["Value"]; + Assert.That(valueToken, Is.Not.Null, "Bad StatusCode value should be present in Reversible encoding."); + } + + /// + /// Verify that a non-Good StatusCode value is preserved in NonReversible encoding. + /// + [Test] + public void EncodeStatusCodeBadPreservesValueNonReversible() + { + var field = CreateStatusCodeField("StatusField", StatusCodes.BadInvalidArgument); + var message = CreateDataValueMessage(field); + + var json = EncodeMessage(message, JsonEncodingType.NonReversible); + var root = JObject.Parse(json); + var payload = (root["Payload"] as JObject) ?? root; + + var fieldObj = payload["StatusField"] as JObject; + Assert.That(fieldObj, Is.Not.Null, "Field should be present."); + + // A bad StatusCode should be encoded + var valueToken = fieldObj["Value"]; + Assert.That(valueToken, Is.Not.Null, "Bad StatusCode value should be present in NonReversible encoding."); + } + + #endregion + + /// + /// Verify that Compact encoding mode is not supported for PubSub messages. + /// JsonDataSetMessage.EncodeField modifies ForceNamespaceUri which throws for Compact mode. + /// + [Test] + public void EncodeWithCompactModeThrowsNotSupportedException() + { + var field = CreateField("TestField", BuiltInType.UInt32, (uint)0); + var message = CreateDataValueMessage(field); + + Assert.Throws(() => + EncodeMessage(message, JsonEncodingType.Compact)); + } + + /// + /// Verify that Verbose encoding mode is not supported for PubSub messages. + /// JsonDataSetMessage.EncodeField modifies ForceNamespaceUri which throws for Verbose mode. + /// + [Test] + public void EncodeWithVerboseModeThrowsNotSupportedException() + { + var field = CreateField("TestField", BuiltInType.UInt32, (uint)0); + var message = CreateDataValueMessage(field); + + Assert.Throws(() => + EncodeMessage(message, JsonEncodingType.Verbose)); + } + + #endregion + #region Helper Methods + + private static Field CreateField(string name, BuiltInType builtInType, object value) + { + return new Field + { + FieldMetaData = new FieldMetaData + { + Name = name, + BuiltInType = (byte)builtInType, + ValueRank = ValueRanks.Scalar + }, + Value = new DataValue(new Variant(value)) + { + SourceTimestamp = DateTime.UtcNow + } + }; + } + + private static Field CreateStatusCodeField(string name, uint statusCode) + { + return new Field + { + FieldMetaData = new FieldMetaData + { + Name = name, + BuiltInType = (byte)BuiltInType.StatusCode, + ValueRank = ValueRanks.Scalar + }, + Value = new DataValue(new Variant(new StatusCode(statusCode))) + }; + } + + private static PubSubEncoding.JsonDataSetMessage CreateDataValueMessage(Field field) + { + var message = new PubSubEncoding.JsonDataSetMessage(new DataSet { Fields = [field] }); + // DataValue mode requires at least one of these flags + message.SetFieldContentMask( + DataSetFieldContentMask.StatusCode | + DataSetFieldContentMask.SourceTimestamp); + return message; + } + + private static string EncodeMessage(PubSubEncoding.JsonDataSetMessage message, JsonEncodingType encodingType) + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var encoder = new JsonEncoder( + new ServiceMessageContext(telemetry), + encodingType); + message.Encode(encoder); + return encoder.CloseAndReturnText(); + } + + private static JObject GetPayloadField(string json, string fieldName) + { + var root = JObject.Parse(json); + var payload = (root["Payload"] as JObject) ?? root; + return payload?[fieldName] as JObject; + } + + #endregion + } +} From 9fc44a436993d79f1df8a07673b91bf4ffac9eca Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Tue, 9 Dec 2025 18:43:04 +0200 Subject: [PATCH 2/2] Removed orhpaned #endregion --- .../Encoding/JsonDataSetMessageTests.cs | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs b/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs index 1160cb30bf..6627f5efb4 100644 --- a/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs +++ b/Tests/Opc.Ua.PubSub.Tests/Encoding/JsonDataSetMessageTests.cs @@ -250,35 +250,7 @@ public void EncodeStatusCodeBadPreservesValueNonReversible() #endregion - /// - /// Verify that Compact encoding mode is not supported for PubSub messages. - /// JsonDataSetMessage.EncodeField modifies ForceNamespaceUri which throws for Compact mode. - /// - [Test] - public void EncodeWithCompactModeThrowsNotSupportedException() - { - var field = CreateField("TestField", BuiltInType.UInt32, (uint)0); - var message = CreateDataValueMessage(field); - - Assert.Throws(() => - EncodeMessage(message, JsonEncodingType.Compact)); - } - - /// - /// Verify that Verbose encoding mode is not supported for PubSub messages. - /// JsonDataSetMessage.EncodeField modifies ForceNamespaceUri which throws for Verbose mode. - /// - [Test] - public void EncodeWithVerboseModeThrowsNotSupportedException() - { - var field = CreateField("TestField", BuiltInType.UInt32, (uint)0); - var message = CreateDataValueMessage(field); - - Assert.Throws(() => - EncodeMessage(message, JsonEncodingType.Verbose)); - } - #endregion #region Helper Methods private static Field CreateField(string name, BuiltInType builtInType, object value)