/* ========================================================================
* 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.Collections.Generic;
using System.Linq;
using System.Xml;
using Moq;
using NUnit.Framework;
// The encodeable type registry API remains experimental.
#pragma warning disable UA_NETStandard_1
namespace Opc.Ua.Schema.Tests
{
///
/// Tests for resolving data type definitions from the encodeable factory.
///
[TestFixture]
[Category("Schema")]
public class EncodeableFactoryDefinitionSourceTests
{
[Test]
public void DefinitionSourceIsStableApi()
{
Assert.That(
typeof(EncodeableFactoryDefinitionSource).GetCustomAttributesData()
.Select(attribute => attribute.AttributeType.FullName),
Does.Not.Contain("System.Diagnostics.CodeAnalysis.ExperimentalAttribute"));
}
[Test]
public void TryResolveReturnsDefinitionFromFactoryType()
{
StructureDefinition definition = CreateDefinition();
var typeId = new ExpandedNodeId(new NodeId(4001, 1));
IEncodeableFactory factory = CreateFactory(typeId, "FactoryType", definition);
var source = new EncodeableFactoryDefinitionSource(factory, new NamespaceTable());
bool resolved = source.TryResolve(typeId, out UaTypeDescription? description);
Assert.Multiple(() =>
{
Assert.That(resolved, Is.True);
Assert.That(description!.Definition, Is.SameAs(definition));
Assert.That(description.Name, Is.EqualTo("FactoryType"));
});
}
[Test]
public void TryResolveReturnsFalseForUnknownType()
{
StructureDefinition definition = CreateDefinition();
var knownId = new ExpandedNodeId(new NodeId(4001, 1));
IEncodeableFactory factory = CreateFactory(knownId, "FactoryType", definition);
var source = new EncodeableFactoryDefinitionSource(factory, new NamespaceTable());
bool resolved = source.TryResolve(new ExpandedNodeId(new NodeId(9999, 1)), out UaTypeDescription? description);
Assert.Multiple(() =>
{
Assert.That(resolved, Is.False);
Assert.That(description, Is.Null);
});
}
[Test]
public void TryResolveNodeIdFindsUriFormFactoryType()
{
const string namespaceUri = "http://test.org/standin";
var namespaceUris = new NamespaceTable();
namespaceUris.Append(namespaceUri);
StructureDefinition definition = CreateDefinition();
var typeId = new ExpandedNodeId(4003, namespaceUri);
var standIn = new Encoders.Structure(
new XmlQualifiedName("UriFactoryType", namespaceUri),
typeId,
ExpandedNodeId.Null,
ExpandedNodeId.Null,
definition,
new Dictionary { ["Value"] = BuiltInType.Int32 });
IEncodeableFactory factory = EncodeableFactory.Create();
factory.Builder.AddEncodeableType(typeId, standIn).Commit();
var source = new EncodeableFactoryDefinitionSource(factory, namespaceUris);
var sourceWithoutMapping = new EncodeableFactoryDefinitionSource(
factory,
new NamespaceTable());
bool nodeIdResolved = source.TryResolve(
new NodeId(4003, 1),
out UaTypeDescription? nodeIdDescription);
bool expandedNodeIdResolved = source.TryResolve(
new ExpandedNodeId(new NodeId(4003, 1)),
out UaTypeDescription? expandedNodeIdDescription);
var provider = new DefaultSchemaProvider(source, [new Json.JsonSchemaGenerator()]);
bool schemaResolved = provider.TryGetSchema(
new ExpandedNodeId(new NodeId(4003, 1)),
UaSchemaFormat.JsonCompact,
UaSchemaScope.Type,
out IUaSchema? schema);
bool unresolved = sourceWithoutMapping.TryResolve(
new NodeId(4003, 1),
out UaTypeDescription? missing);
Assert.Multiple(() =>
{
Assert.That(nodeIdResolved, Is.True);
Assert.That(nodeIdDescription!.Definition, Is.SameAs(definition));
Assert.That(nodeIdDescription.NamespaceUri, Is.EqualTo(namespaceUri));
Assert.That(expandedNodeIdResolved, Is.True);
Assert.That(expandedNodeIdDescription!.Definition, Is.SameAs(definition));
Assert.That(schemaResolved, Is.True);
Assert.That(schema, Is.Not.Null);
Assert.That(unresolved, Is.False);
Assert.That(missing, Is.Null);
});
}
[TestCaseSource(typeof(SchemaTestData), nameof(SchemaTestData.TypeIdentifierCases))]
public void TryResolveUriFindsIndexedFactoryType(NodeId localId)
{
var namespaceUris = new NamespaceTable();
namespaceUris.Append(SchemaTestData.TestNamespace);
var definition = new EnumDefinition
{
Fields = [new EnumField { Name = "Enabled", Value = 1 }]
};
var standIn = new Encoders.Enumeration(
new XmlQualifiedName("IndexedType", SchemaTestData.TestNamespace),
definition);
IEncodeableFactory factory = EncodeableFactory.Create();
factory.Builder.AddEnumeratedType(new ExpandedNodeId(localId), standIn).Commit();
var source = new EncodeableFactoryDefinitionSource(factory, namespaceUris);
ExpandedNodeId uriId = NodeId.ToExpandedNodeId(localId, namespaceUris);
Assert.That(source.TryResolve(localId, out UaTypeDescription? indexed), Is.True);
Assert.That(indexed!.Definition, Is.SameAs(definition));
Assert.That(source.TryResolve(uriId, out UaTypeDescription? expanded), Is.True);
Assert.That(expanded!.Definition, Is.SameAs(definition));
var provider = new DefaultSchemaProvider(source, [new Json.JsonSchemaGenerator()]);
Assert.That(
provider.TryGetSchema(uriId, UaSchemaFormat.JsonCompact, UaSchemaScope.Type, out IUaSchema? schema),
Is.True);
Assert.That(schema!.ToSchemaString(), Does.Contain("IndexedType").And.Contain("Enabled"));
}
[Test]
public void TryResolveDoesNotNormalizeUnknownNamespacesOrRemoteIds()
{
var namespaceUris = new NamespaceTable();
namespaceUris.Append(SchemaTestData.TestNamespace);
var localId = new NodeId(7701, SchemaTestData.TestNamespaceIndex);
var standIn = new Encoders.Enumeration(
new XmlQualifiedName("LocalType", SchemaTestData.TestNamespace),
new EnumDefinition());
IEncodeableFactory factory = EncodeableFactory.Create();
factory.Builder.AddEnumeratedType(new ExpandedNodeId(localId), standIn).Commit();
var source = new EncodeableFactoryDefinitionSource(factory, namespaceUris);
ArrayOf unresolvedIds =
[
new ExpandedNodeId(localId, SchemaTestData.OtherNamespace),
new ExpandedNodeId(new NodeId(7701, SchemaTestData.OtherNamespaceIndex)),
new ExpandedNodeId(localId, SchemaTestData.TestNamespace, serverIndex: 1),
new ExpandedNodeId(localId, serverIndex: 1)
];
Assert.Multiple(() =>
{
foreach (ExpandedNodeId typeId in unresolvedIds)
{
Assert.That(source.TryResolve(typeId, out UaTypeDescription? description), Is.False);
Assert.That(description, Is.Null);
}
});
}
[Test]
public void TryResolvePrefersExactRegistration()
{
var namespaceUris = new NamespaceTable();
namespaceUris.Append(SchemaTestData.TestNamespace);
var indexedId = new ExpandedNodeId(new NodeId(7701, SchemaTestData.TestNamespaceIndex));
var uriId = new ExpandedNodeId(7701, SchemaTestData.TestNamespace);
var indexedDefinition = new EnumDefinition();
var uriDefinition = new EnumDefinition();
IEncodeableFactory factory = EncodeableFactory.Create();
factory.Builder
.AddEnumeratedType(indexedId, new Encoders.Enumeration(
new XmlQualifiedName("IndexedType", SchemaTestData.TestNamespace), indexedDefinition))
.AddEnumeratedType(uriId, new Encoders.Enumeration(
new XmlQualifiedName("UriType", SchemaTestData.TestNamespace), uriDefinition))
.Commit();
var source = new EncodeableFactoryDefinitionSource(factory, namespaceUris);
Assert.That(source.TryResolve(indexedId, out UaTypeDescription? indexed), Is.True);
Assert.That(indexed!.Definition, Is.SameAs(indexedDefinition));
Assert.That(source.TryResolve(uriId, out UaTypeDescription? expanded), Is.True);
Assert.That(expanded!.Definition, Is.SameAs(uriDefinition));
}
[Test]
public void ConstructorRejectsNullArguments()
{
IEncodeableFactory factory = EncodeableFactory.Create();
Assert.Multiple(() =>
{
Assert.That(
() => new EncodeableFactoryDefinitionSource(null!, new NamespaceTable()),
Throws.ArgumentNullException.With.Property("ParamName").EqualTo("factory"));
Assert.That(
() => new EncodeableFactoryDefinitionSource(factory, null!),
Throws.ArgumentNullException.With.Property("ParamName").EqualTo("namespaceUris"));
});
}
[Test]
public void GetNamespaceTypesReturnsUniqueTypesFromKnownFactoryIds()
{
StructureDefinition definition = CreateDefinition();
var typeId = new ExpandedNodeId(new NodeId(5101, 1));
var encodingId = new ExpandedNodeId(new NodeId(5102, 1));
var standIn = new Opc.Ua.Encoders.Structure(
new XmlQualifiedName("NamespaceStructure", "http://test.org/standin"),
typeId,
encodingId,
ExpandedNodeId.Null,
definition,
new Dictionary { ["Value"] = BuiltInType.Int32 });
IEncodeableFactory factory = EncodeableFactory.Create();
factory.Builder
.AddEncodeableType(typeId, standIn)
.AddEncodeableType(encodingId, standIn)
.Commit();
var source = new EncodeableFactoryDefinitionSource(factory, new NamespaceTable());
IReadOnlyCollection emptyNamespace = source.GetNamespaceTypes(string.Empty);
IReadOnlyCollection matchingNamespace = source.GetNamespaceTypes("http://test.org/standin");
IReadOnlyCollection missingNamespace = source.GetNamespaceTypes("http://missing");
Assert.Multiple(() =>
{
Assert.That(emptyNamespace, Is.Empty);
Assert.That(matchingNamespace, Has.Count.EqualTo(1));
Assert.That(matchingNamespace.Single().Name, Is.EqualTo("NamespaceStructure"));
Assert.That(missingNamespace, Is.Empty);
});
}
[Test]
public void CompositeResolverFallsThroughToFactorySource()
{
StructureDefinition definition = CreateDefinition();
var typeId = new ExpandedNodeId(new NodeId(4002, 1));
IEncodeableFactory factory = CreateFactory(typeId, "CompositeType", definition);
var registry = new DataTypeDefinitionRegistry();
var composite = new CompositeDataTypeDefinitionResolver(
[registry, new EncodeableFactoryDefinitionSource(factory, new NamespaceTable())]);
bool resolved = composite.TryResolve(typeId, out UaTypeDescription? description);
Assert.Multiple(() =>
{
Assert.That(resolved, Is.True);
Assert.That(description!.Name, Is.EqualTo("CompositeType"));
});
}
[Test]
public void CompositeResolverCoversNullFalseNodeIdAndNamespaceMergeCases()
{
Assert.That(
() => new CompositeDataTypeDefinitionResolver(null!),
Throws.ArgumentNullException.With.Property("ParamName").EqualTo("resolvers"));
UaTypeDescription first = SchemaTestData.Enumeration(5201, "First", ("A", 0));
UaTypeDescription duplicate = SchemaTestData.Enumeration(5201, "Duplicate", ("B", 0));
UaTypeDescription second = SchemaTestData.Enumeration(5202, "Second", ("C", 0));
var firstRegistry = new DataTypeDefinitionRegistry();
firstRegistry.Add(first);
firstRegistry.Add(duplicate);
var secondRegistry = new DataTypeDefinitionRegistry();
secondRegistry.Add(second);
var composite = new CompositeDataTypeDefinitionResolver([firstRegistry, secondRegistry]);
bool resolved = composite.TryResolve(first.TypeId.InnerNodeId, out UaTypeDescription? description);
bool missing = composite.TryResolve(new NodeId(9999, SchemaTestData.TestNamespaceIndex), out _);
IReadOnlyCollection namespaceTypes = composite.GetNamespaceTypes(
SchemaTestData.TestNamespace);
Assert.Multiple(() =>
{
Assert.That(resolved, Is.True);
Assert.That(description!.Name, Is.EqualTo("Duplicate"));
Assert.That(missing, Is.False);
Assert.That(namespaceTypes.Select(t => t.Name), Is.EqualTo(CompositeNamespaceTypeNames));
});
}
[Test]
public void TryResolveReturnsDefinitionFromStandInStructure()
{
const string namespaceUri = "http://test.org/standin";
StructureDefinition definition = CreateDefinition();
var typeId = new ExpandedNodeId(new NodeId(5001, 1));
var standIn = new Encoders.Structure(
new XmlQualifiedName("StandInStructure", namespaceUri),
typeId,
ExpandedNodeId.Null,
ExpandedNodeId.Null,
definition,
new Dictionary { ["Value"] = BuiltInType.Int32 });
IEncodeableFactory factory = EncodeableFactory.Create();
factory.Builder.AddEncodeableType(typeId, standIn).Commit();
var namespaceUris = new NamespaceTable();
namespaceUris.Append(namespaceUri);
var source = new EncodeableFactoryDefinitionSource(factory, namespaceUris);
bool indexResolved = source.TryResolve(typeId, out UaTypeDescription? indexDescription);
bool uriResolved = source.TryResolve(
new ExpandedNodeId(typeId.InnerNodeId, namespaceUri),
out UaTypeDescription? uriDescription);
Assert.Multiple(() =>
{
Assert.That(indexResolved, Is.True);
Assert.That(indexDescription!.Definition, Is.SameAs(definition));
Assert.That(indexDescription.Name, Is.EqualTo("StandInStructure"));
Assert.That(uriResolved, Is.True);
Assert.That(uriDescription!.Definition, Is.SameAs(definition));
});
}
[Test]
public void TryResolveReturnsDefinitionFromStandInEnumeration()
{
var definition = new EnumDefinition
{
Fields =
[
new EnumField { Name = "A", Value = 0 },
new EnumField { Name = "B", Value = 1 }
]
};
var typeId = new ExpandedNodeId(new NodeId(5002, 1));
var standIn = new Encoders.Enumeration(
new XmlQualifiedName("StandInEnum", "http://test.org/standin"),
definition);
IEncodeableFactory factory = EncodeableFactory.Create();
factory.Builder.AddEnumeratedType(typeId, standIn).Commit();
var source = new EncodeableFactoryDefinitionSource(factory, new NamespaceTable());
bool resolved = source.TryResolve(typeId, out UaTypeDescription? description);
Assert.Multiple(() =>
{
Assert.That(resolved, Is.True);
Assert.That(description!.Definition, Is.SameAs(definition));
Assert.That(description.Name, Is.EqualTo("StandInEnum"));
});
}
[Test]
public void TryResolveReturnsDefinitionFromStandInOptionSet()
{
var definition = new EnumDefinition
{
Fields =
[
new EnumField { Name = "Bit0", Value = 0 },
new EnumField { Name = "Bit1", Value = 1 }
]
};
var typeId = new ExpandedNodeId(new NodeId(5003, 1));
var standIn = new Encoders.OptionSet(
new XmlQualifiedName("StandInOptionSet", "http://test.org/standin"),
typeId,
ExpandedNodeId.Null,
ExpandedNodeId.Null,
definition);
IEncodeableFactory factory = EncodeableFactory.Create();
factory.Builder.AddEncodeableType(typeId, standIn).Commit();
var source = new EncodeableFactoryDefinitionSource(factory, new NamespaceTable());
bool resolved = source.TryResolve(typeId, out UaTypeDescription? description);
Assert.Multiple(() =>
{
Assert.That(resolved, Is.True);
Assert.That(description!.Definition, Is.SameAs(definition));
Assert.That(description.Name, Is.EqualTo("StandInOptionSet"));
});
}
private static StructureDefinition CreateDefinition()
{
return new StructureDefinition
{
BaseDataType = DataTypeIds.Structure,
StructureType = StructureType.Structure,
Fields = new[]
{
SchemaTestData.Field("Value", SchemaTestData.BuiltIn(BuiltInType.Int32))
}
};
}
private static IEncodeableFactory CreateFactory(
ExpandedNodeId typeId,
string name,
DataTypeDefinition definition)
{
var typeMock = new Mock();
typeMock.SetupGet(t => t.XmlName)
.Returns(new XmlQualifiedName(name, "http://test.org/factory"));
typeMock.As()
.Setup(s => s.GetDataTypeDefinition(It.IsAny()))
.Returns(definition);
IEncodeableType? encodeableType = typeMock.Object;
IEnumeratedType? enumeratedType = null;
var factoryMock = new Mock();
factoryMock.Setup(f => f.TryGetEncodeableType(typeId, out encodeableType)).Returns(true);
factoryMock.Setup(f => f.TryGetEnumeratedType(
It.IsAny(), out enumeratedType)).Returns(false);
return factoryMock.Object;
}
private static readonly string[] CompositeNamespaceTypeNames = ["Duplicate", "Second"];
}
}