/* ========================================================================
* 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 System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.Client.TestFramework;
using ISession = Opc.Ua.Client.ISession;
namespace Opc.Ua.InformationModel.Tests
{
///
/// compliance tests for Attribute Service Set – Reading complex attributes,
/// structured types, data encodings, and optional/extended attributes.
///
[TestFixture]
[Category("Conformance")]
[Category("AttributeReadComplex")]
public class AttributeReadComplexTests : TestFixture
{
[Description("Read Server_ServerStatus (structured type) and verify the value is returned as an ExtensionObject with Good status.")]
[Test]
public async Task ReadExtensionObjectValueAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"Read of Server_ServerStatus should return Good.");
Variant variant = response.Results[0].WrappedValue;
// Server_ServerStatus is per spec a ServerStatusDataType structure
// wire-encoded as ExtensionObject. Both shapes are accepted: either
// the wire ExtensionObject or a server that already decoded it.
bool isExtensionObject = variant.TryGetValue(out ExtensionObject _);
bool isDecoded = variant.TryGetStructure(out ServerStatusDataType? _);
Assert.That(isExtensionObject || isDecoded, Is.True,
"Server_ServerStatus value should not be null.");
Assert.That(isExtensionObject || isDecoded, Is.True,
"Value should be an ExtensionObject or ServerStatusDataType.");
}
[Description("Read Server_ServerStatus and verify the decoded structure contains accessible nested fields such as StartTime and State.")]
[Test]
public async Task ReadNestedStructureValueAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True);
Variant variant = response.Results[0].WrappedValue;
ServerStatusDataType? serverStatus = null;
// Per spec ServerStatus is a ServerStatusDataType wire-encoded as
// ExtensionObject; accept both the wire form and an already-decoded
// structure.
if (variant.TryGetStructure(out ServerStatusDataType? decoded))
{
serverStatus = decoded;
}
else if (variant.TryGetValue(out ExtensionObject extensionObject) &&
extensionObject.TryGetValue(out ServerStatusDataType? fromWire))
{
serverStatus = fromWire;
}
Assert.That(serverStatus, Is.Not.Null,
"Should be able to decode ServerStatusDataType.");
Assert.That(
serverStatus.StartTime, Is.Not.EqualTo(DateTime.MinValue),
"StartTime should be set.");
Assert.That(
#if NET5_0_OR_GREATER
Enum.IsDefined(serverStatus.State),
#else
Enum.IsDefined(typeof(ServerState), serverStatus.State),
#endif
Is.True,
"State should be a valid ServerState enum value.");
}
[Description("Read the SessionDiagnosticsArray variable which contains an array of ExtensionObjects. Verify the result is Good and the value is an array or empty collection.")]
[Test]
public async Task ReadArrayOfExtensionObjectsAsync()
{
ISession admin = await ConnectAsSysAdminAsync().ConfigureAwait(false);
try
{
ISession session = admin ?? Session;
ReadResponse response = await session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds
.Server_ServerDiagnostics_SessionsDiagnosticsSummary_SessionDiagnosticsArray,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
StatusCode statusCode = response.Results[0].StatusCode;
if (!StatusCode.IsGood(statusCode))
{
Assert.Ignore(
$"SessionDiagnosticsArray not readable: {statusCode}");
}
// SessionDiagnosticsArray is genuinely polymorphic: server can
// return an array of ExtensionObject, an empty array, or a
// null Variant. A null/empty value is consistent with
// "no live sessions"; reject only the silent default-Variant
// shape.
Assert.That(
response.Results[0].WrappedValue.TypeInfo,
Is.Not.Null,
"Value should not be null when status is Good.");
}
finally
{
if (admin != null)
{
await admin.CloseAsync(5000, true).ConfigureAwait(false);
admin.Dispose();
}
}
}
[Description("Read Server_ServerStatus_State which is an enumeration value. Verify the result is a numeric value representing a valid ServerState.")]
[Test]
public async Task ReadEnumerationValueAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus_State,
AttributeId = Attributes.Value
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"Read of Server_ServerStatus_State should return Good.");
int stateValue = response.Results[0].GetValue(0);
Assert.That(
Enum.IsDefined(typeof(ServerState), stateValue), Is.True,
$"State value {stateValue} should be a valid ServerState.");
}
[Description("Read a structured type node with DataEncoding set to \"Default Binary\". Expect Good status since binary encoding is the native OPC UA encoding.")]
[Test]
public async Task ReadWithDataEncodingDefaultBinaryAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value,
DataEncoding = new QualifiedName("Default Binary")
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode) ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingUnsupported,
Is.True,
"Default Binary encoding should return Good or unsupported.");
}
[Description("Read a structured type node with DataEncoding set to \"Default XML\". Expect Good or BadDataEncodingUnsupported since XML encoding is optional.")]
[Test]
public async Task ReadWithDataEncodingDefaultXmlAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value,
DataEncoding = new QualifiedName("Default XML")
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode) ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingUnsupported ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingInvalid,
Is.True,
"Default XML encoding should return Good or unsupported.");
}
[Description("Read with an invalid DataEncoding name. Expect BadDataEncodingInvalid or BadDataEncodingUnsupported.")]
[Test]
public async Task ReadWithInvalidDataEncodingAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = VariableIds.Server_ServerStatus,
AttributeId = Attributes.Value,
DataEncoding = new QualifiedName(
"NonExistentEncoding_Invalid")
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingInvalid ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadDataEncodingUnsupported,
Is.True,
"Invalid DataEncoding should return BadDataEncodingInvalid " +
"or BadDataEncodingUnsupported.");
}
[Description("Read all standard attributes of a Variable node in a single call. Verify each attribute returns Good status.")]
[Test]
public async Task ReadAllAttributesOfVariableNodeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
uint[] attributeIds =
[
Attributes.NodeId,
Attributes.NodeClass,
Attributes.BrowseName,
Attributes.DisplayName,
Attributes.Description,
Attributes.DataType,
Attributes.ValueRank,
Attributes.Value,
Attributes.AccessLevel,
Attributes.UserAccessLevel
];
var readValueIds = attributeIds
.Select(attrId => new ReadValueId
{
NodeId = nodeId,
AttributeId = attrId
}).ToArrayOf();
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
readValueIds,
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(attributeIds.Length));
for (int i = 0; i < response.Results.Count; i++)
{
Assert.That(
StatusCode.IsGood(response.Results[i].StatusCode), Is.True,
$"Variable attribute {attributeIds[i]} should return Good.");
}
}
[Description("Read all standard attributes of the Server object node. Verify NodeClass, BrowseName, DisplayName, Description, and EventNotifier return valid results.")]
[Test]
public async Task ReadAllAttributesOfObjectNodeAsync()
{
uint[] attributeIds =
[
Attributes.NodeClass,
Attributes.BrowseName,
Attributes.DisplayName,
Attributes.Description,
Attributes.EventNotifier
];
var readValueIds = attributeIds
.Select(attrId => new ReadValueId
{
NodeId = ObjectIds.Server,
AttributeId = attrId
}).ToArrayOf();
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
readValueIds,
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(attributeIds.Length));
for (int i = 0; i < response.Results.Count; i++)
{
Assert.That(
StatusCode.IsGood(response.Results[i].StatusCode), Is.True,
$"Object attribute {attributeIds[i]} should return Good.");
}
// Verify NodeClass is Object
int nodeClass = response.Results[0].GetValue(0);
Assert.That(nodeClass, Is.EqualTo((int)NodeClass.Object),
"Server node should have NodeClass = Object.");
}
[Description("Read the AccessLevelEx attribute (id=27) from a Variable node. This attribute may not be supported by all servers; if BadAttributeIdInvalid is returned the test is skipped.")]
[Test]
public async Task ReadAccessLevelExAttributeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.AccessLevelEx
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
if (response.Results[0].StatusCode.Code ==
StatusCodes.BadAttributeIdInvalid)
{
Assert.Ignore(
"AccessLevelEx attribute not supported by this server.");
}
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"AccessLevelEx should return Good when supported.");
}
[Description("Read the RolePermissions attribute (id=26) from a Variable node. This attribute may not be supported; if BadAttributeIdInvalid is returned the test is skipped.")]
[Test]
public async Task ReadRolePermissionsAttributeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.RolePermissions
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
if (response.Results[0].StatusCode.Code ==
StatusCodes.BadAttributeIdInvalid)
{
Assert.Ignore(
"RolePermissions attribute not supported by this server.");
}
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode) ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadNotReadable,
Is.True,
"RolePermissions should return Good or BadNotReadable.");
}
[Description("Read the UserRolePermissions attribute (id=25) from a Variable node. This attribute may not be supported; if BadAttributeIdInvalid is returned the test is skipped.")]
[Test]
public async Task ReadUserRolePermissionsAttributeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticInt32);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.UserRolePermissions
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
if (response.Results[0].StatusCode.Code ==
StatusCodes.BadAttributeIdInvalid)
{
Assert.Ignore(
"UserRolePermissions attribute not supported by " +
"this server.");
}
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode) ||
response.Results[0].StatusCode.Code ==
StatusCodes.BadNotReadable,
Is.True,
"UserRolePermissions should return Good or BadNotReadable.");
}
[Description("Read the DataTypeDefinition attribute (id=23) on a DataType node. This provides the structure or enum definition of the type. If BadAttributeIdInvalid is returned the test is skipped.")]
[Test]
public async Task ReadDataTypeDefinitionAttributeAsync()
{
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = DataTypeIds.ServerStatusDataType,
AttributeId = Attributes.DataTypeDefinition
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
if (response.Results[0].StatusCode.Code ==
StatusCodes.BadAttributeIdInvalid)
{
Assert.Ignore(
"DataTypeDefinition attribute not supported by " +
"this server.");
}
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"DataTypeDefinition should return Good when supported.");
// DataTypeDefinition is an ExtensionObject wrapping a
// StructureDefinition or EnumDefinition; verify the wire form
// is non-null and that an inner IEncodeable can be unwrapped.
Assert.That(
response.Results[0].WrappedValue.TryGetValue(out ExtensionObject extObj),
Is.True,
"DataTypeDefinition value should be an ExtensionObject.");
Assert.That(
extObj.TryGetValue(out IEncodeable? encodeable),
Is.True,
"DataTypeDefinition body should decode to an IEncodeable.");
Assert.That(encodeable, Is.Not.Null,
"DataTypeDefinition value should not be null.");
}
[Description("Read the ArrayDimensions attribute on an array variable node. Verify the result is Good and the value is a valid array.")]
[Test]
public async Task ReadArrayDimensionsOnArrayNodeAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticArrayInt32);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.ArrayDimensions
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"ArrayDimensions on an array node should return Good.");
}
[Description("Read the DataType attribute of a Variable node and verify it returns a valid, non-null NodeId.")]
[Test]
public async Task ReadDataTypeOfVariableAsync()
{
NodeId nodeId = ToNodeId(Constants.ScalarStaticDouble);
ReadResponse response = await Session.ReadAsync(
null, 0, TimestampsToReturn.Both,
new ReadValueId[]
{
new() {
NodeId = nodeId,
AttributeId = Attributes.DataType
}
}.ToArrayOf(),
CancellationToken.None).ConfigureAwait(false);
Assert.That(response.Results.Count, Is.EqualTo(1));
Assert.That(
StatusCode.IsGood(response.Results[0].StatusCode), Is.True,
"DataType attribute should return Good.");
NodeId dataType = response.Results[0].GetValue(default);
Assert.That(dataType, Is.Not.Null,
"DataType should not be null.");
Assert.That(dataType, Is.Not.EqualTo(NodeId.Null),
"DataType should not be the Null NodeId.");
Assert.That(dataType, Is.EqualTo(DataTypeIds.Double),
"DataType for ScalarStaticDouble should be Double.");
}
}
}