/* ======================================================================== * 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"]; } }