Files
UA-.NETStandard/tests/Opc.Ua.PubSub.Server.Tests/PubSubMethodHandlersMutationTests.cs
Wolfgang Colsman 12a76ade55 PubSub server: organize PublishedDataSets under PublishSubscribe.PublishedDataSets (#4793)
# Description

PubSubNodeManager looked up the parent of the PublishedDataSet and
DataSetFolder objects it materializes by NodeId i=14478. That is
DataSetFolderType's <DataSetFolderName> placeholder, an
InstanceDeclaration
of the type, not the PublishSubscribe.PublishedDataSets object
(i=17371).
Every PublishedDataSet therefore became a child of the type definition
and
the standard PublishedDataSets folder stayed empty.

Use ObjectIds.PublishSubscribe_PublishedDataSets as the parent. The test
harnesses now expose the real folder and the placeholder, and a new test
asserts that a PublishedDataSet ends up under the folder and that the
placeholder gains no children.

## Related Issues

None.

## 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.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [x] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
2026-10-15 14:27:52 +02:00

471 lines
20 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.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.PubSub.Application;
using Opc.Ua.PubSub.Tests;
using Opc.Ua.PubSub.Transports;
using Opc.Ua.Tests;
namespace Opc.Ua.PubSub.Server.Tests
{
[TestFixture]
[TestSpec("9.1.3.4", Summary = "AddConnection handler")]
[TestSpec("9.1.3.5", Summary = "RemoveConnection handler")]
[TestSpec("9.1.6", Summary = "Configuration methods")]
[TestSpec("9.1.4.3", Summary = "PublishedDataItemsType variable methods")]
[TestSpec("9.1.4.5", Summary = "DataSetFolderType dataset methods")]
public class PubSubMethodHandlersMutationTests
{
[Test]
[TestSpec("9.1.3.4")]
public void OnAddConnection_ValidInput_ReturnsGoodAndNodeId()
{
PubSubMethodHandlers handlers = CreateHandlers();
var connCfg = new PubSubConnectionDataType
{
Name = "handler-conn",
TransportProfileUri =
"http://opcfoundation.org/UA-Profile/Transport/pubsub-udp-uadp",
Address = new ExtensionObject(
new NetworkAddressUrlDataType { Url = "opc.udp://224.0.0.22:4840" })
};
ArrayOf<Variant> inputs = BuildArray(Variant.From(new ExtensionObject(connCfg)));
var outputs = new List<Variant>();
ServiceResult result = handlers.OnAddConnection(
BuildContext(), method: null!, inputArguments: inputs, outputArguments: outputs);
Assert.Multiple(() =>
{
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
Assert.That(outputs, Has.Count.EqualTo(1));
Assert.That(outputs[0].TryGetValue(out NodeId id), Is.True);
Assert.That(id.IsNull, Is.False);
});
}
[Test]
[TestSpec("9.1.3.5")]
public void OnRemoveConnection_ValidInput_ReturnsGood()
{
PubSubMethodHandlers handlers = CreateHandlers();
var connCfg = new PubSubConnectionDataType
{
Name = "to-remove",
TransportProfileUri =
"http://opcfoundation.org/UA-Profile/Transport/pubsub-udp-uadp",
Address = new ExtensionObject(
new NetworkAddressUrlDataType { Url = "opc.udp://224.0.0.22:4840" })
};
ArrayOf<Variant> addInputs = BuildArray(Variant.From(new ExtensionObject(connCfg)));
var addOutputs = new List<Variant>();
handlers.OnAddConnection(
BuildContext(), method: null!, inputArguments: addInputs, outputArguments: addOutputs);
Assert.That(addOutputs[0].TryGetValue(out NodeId connId), Is.True);
ArrayOf<Variant> removeInputs = BuildArray(Variant.From(connId));
var removeOutputs = new List<Variant>();
ServiceResult result = handlers.OnRemoveConnection(
BuildContext(), method: null!, inputArguments: removeInputs,
outputArguments: removeOutputs);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
}
[Test]
[TestSpec("9.1.6")]
public void OnGetConfiguration_ReturnsGood()
{
PubSubMethodHandlers handlers = CreateHandlers();
var outputs = new List<Variant>();
ServiceResult result = handlers.OnGetConfiguration(
BuildContext(), method: null!, inputArguments: default, outputArguments: outputs);
Assert.Multiple(() =>
{
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
Assert.That(outputs, Has.Count.EqualTo(1));
});
}
[Test]
[TestSpec("9.1.6")]
public void OnSetConfiguration_ValidInput_ReturnsGood()
{
PubSubMethodHandlers handlers = CreateHandlers();
var cfg = new PubSubConfigurationDataType
{
Connections = [],
PublishedDataSets = []
};
ArrayOf<Variant> inputs = BuildArray(Variant.From(new ExtensionObject(cfg)));
var outputs = new List<Variant>();
ServiceResult result = handlers.OnSetConfiguration(
BuildContext(), method: null!, inputArguments: inputs, outputArguments: outputs);
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
}
[Test]
[TestSpec("9.1.4.5")]
public void OnAddPublishedDataItems_RegistersPublishedDataItemsDataSet()
{
IPubSubApplication application = CreateApplication();
PubSubMethodHandlers handlers = CreateHandlers(application);
var outputs = new List<Variant>();
var variable = new PublishedVariableDataType
{
PublishedVariable = new NodeId(Variables.Server_ServerStatus_CurrentTime),
AttributeId = Attributes.Value
};
ServiceResult result = handlers.OnAddPublishedDataItems(
BuildContext(),
method: null!,
inputArguments: BuildArray(
Variant.From("items"),
Variant.From(new ArrayOf<string>(s_currentTimeAlias)),
Variant.From(Array.Empty<DataSetFieldFlags>()),
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
new[] { variable }))),
outputArguments: outputs);
PubSubConfigurationDataType configuration = application.GetConfiguration();
Assert.Multiple(() =>
{
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
Assert.That(outputs, Has.Count.EqualTo(3));
Assert.That(configuration.PublishedDataSets, Has.Count.EqualTo(1));
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
out PublishedDataItemsDataType? _), Is.True);
});
}
[Test]
[TestSpec("9.1.4.5")]
public void OnAddPublishedDataItemsTemplate_UsesProvidedMetaData()
{
IPubSubApplication application = CreateApplication();
PubSubMethodHandlers handlers = CreateHandlers(application);
var outputs = new List<Variant>();
var variable = new PublishedVariableDataType
{
PublishedVariable = new NodeId(Variables.Server_ServerStatus_CurrentTime),
AttributeId = Attributes.Value
};
var metaData = new DataSetMetaDataType
{
Name = "template",
Fields = new ArrayOf<FieldMetaData>(new[]
{
new FieldMetaData
{
Name = "FromTemplate",
DataType = DataTypeIds.DateTime,
ValueRank = ValueRanks.Scalar
}
})
};
ServiceResult result = handlers.OnAddPublishedDataItemsTemplate(
BuildContext(),
method: null!,
inputArguments: BuildArray(
Variant.From("template"),
Variant.From(new ExtensionObject(metaData)),
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
new[] { variable }))),
outputArguments: outputs);
PubSubConfigurationDataType configuration = application.GetConfiguration();
Assert.Multiple(() =>
{
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
Assert.That(outputs, Has.Count.EqualTo(2));
Assert.That(configuration.PublishedDataSets[0].Name, Is.EqualTo("template"));
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
out PublishedDataItemsDataType? _), Is.True);
});
}
[Test]
[TestSpec("9.1.4.5")]
public void OnAddPublishedEvents_RegistersPublishedEventsDataSet()
{
IPubSubApplication application = CreateApplication();
PubSubMethodHandlers handlers = CreateHandlers(application);
var outputs = new List<Variant>();
var selectedField = new SimpleAttributeOperand
{
TypeDefinitionId = ObjectTypeIds.BaseEventType,
BrowsePath = new ArrayOf<QualifiedName>(new[] { new QualifiedName(BrowseNames.EventId) }),
AttributeId = Attributes.Value
};
ServiceResult result = handlers.OnAddPublishedEvents(
BuildContext(),
method: NewPublishedDataItemsMethod("items", "AddVariables"),
inputArguments: BuildArray(
Variant.From("events"),
Variant.From(ObjectIds.Server),
Variant.From(new ArrayOf<string>(s_eventIdAlias)),
Variant.From(Array.Empty<DataSetFieldFlags>()),
Variant.FromStructure(new ArrayOf<SimpleAttributeOperand>(
new[] { selectedField })),
Variant.From(new ExtensionObject(new ContentFilter()))),
outputArguments: outputs);
PubSubConfigurationDataType configuration = application.GetConfiguration();
Assert.Multiple(() =>
{
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
Assert.That(outputs, Has.Count.EqualTo(2));
Assert.That(outputs[0].TryGetValue(out ExtensionObject configurationVersion), Is.True);
Assert.That(
configurationVersion.TryGetValue(out ConfigurationVersionDataType? _),
Is.True);
Assert.That(outputs[1].TryGetValue(out NodeId dataSetId), Is.True);
Assert.That(dataSetId.IsNull, Is.False);
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
out PublishedEventsDataType? _), Is.True);
});
}
[Test]
[TestSpec("9.1.4.3")]
public void OnAddVariablesAndRemoveVariables_MutateFieldsAndBumpConfigurationVersion()
{
IPubSubApplication application = CreateApplication();
PubSubMethodHandlers handlers = CreateHandlers(application);
var addDataSetOutputs = new List<Variant>();
var variable = new PublishedVariableDataType
{
PublishedVariable = new NodeId(Variables.Server_ServerStatus_CurrentTime),
AttributeId = Attributes.Value
};
handlers.OnAddPublishedDataItems(
BuildContext(),
method: NewPublishedDataItemsMethod("items", "RemoveVariables"),
inputArguments: BuildArray(
Variant.From("items"),
Variant.From(new ArrayOf<string>(s_currentTimeAlias)),
Variant.From(Array.Empty<DataSetFieldFlags>()),
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
new[] { variable }))),
outputArguments: addDataSetOutputs);
var addedVariable = new PublishedVariableDataType
{
PublishedVariable = new NodeId(Variables.Server_ServerStatus_StartTime),
AttributeId = Attributes.Value
};
var addVariableOutputs = new List<Variant>();
ServiceResult addResult = handlers.OnAddVariables(
BuildContext(),
method: NewPublishedDataItemsMethod("items", "AddVariables"),
inputArguments: BuildArray(
addDataSetOutputs[1],
Variant.From(new ArrayOf<string>(s_startTimeAlias)),
Variant.From(s_notPromotedField),
Variant.FromStructure(new ArrayOf<PublishedVariableDataType>(
new[] { addedVariable }))),
outputArguments: addVariableOutputs);
Assert.That(StatusCode.IsGood(addResult.StatusCode), Is.True);
Assert.That(addVariableOutputs, Has.Count.EqualTo(2));
var removeVariableOutputs = new List<Variant>();
ServiceResult removeResult = handlers.OnRemoveVariables(
BuildContext(),
method: NewPublishedDataItemsMethod("items", "RemoveVariables"),
inputArguments: BuildArray(addVariableOutputs[0], Variant.From(new ArrayOf<uint>(s_firstVariableIndex))),
outputArguments: removeVariableOutputs);
PubSubConfigurationDataType configuration = application.GetConfiguration();
Assert.That(configuration.PublishedDataSets[0].DataSetSource.TryGetValue(
out PublishedDataItemsDataType? items), Is.True);
Assert.Multiple(() =>
{
Assert.That(StatusCode.IsGood(addResult.StatusCode), Is.True);
Assert.That(StatusCode.IsGood(removeResult.StatusCode), Is.True);
Assert.That(items!.PublishedData, Has.Count.EqualTo(1));
Assert.That(configuration.PublishedDataSets[0].DataSetMetaData.ConfigurationVersion.MinorVersion,
Is.GreaterThan(1u));
});
}
[Test]
[TestSpec("9.1.5")]
public void OnAddDataSetFolder_ReturnsGoodWithNodeId()
{
PubSubMethodHandlers handlers = CreateHandlers();
ArrayOf<Variant> inputs = BuildArray(Variant.From("my-folder"));
var outputs = new List<Variant>();
ServiceResult result = handlers.OnAddDataSetFolder(
BuildContext(), method: null!, inputArguments: inputs, outputArguments: outputs);
Assert.Multiple(() =>
{
Assert.That(StatusCode.IsGood(result.StatusCode), Is.True);
Assert.That(outputs, Has.Count.EqualTo(1));
});
}
private static PubSubMethodHandlers CreateHandlers()
{
return CreateHandlers(CreateApplication());
}
private static PubSubMethodHandlers CreateHandlers(IPubSubApplication app)
{
var options = new PubSubServerOptions
{
ExposeConfigurationMethods = true
};
return new PubSubMethodHandlers(
app, null, options, NUnitTelemetryContext.Create());
}
private static IPubSubApplication CreateApplication()
{
return new PubSubApplicationBuilder(
NUnitTelemetryContext.Create())
.WithApplicationId("handler-mutation-tests")
.UseConfiguration(new PubSubConfigurationDataType
{
Connections = [],
PublishedDataSets = []
})
.UseAllStandardEncoders()
.AddTransportFactory(new StubTransportFactory())
.Build();
}
private static SystemContext BuildContext()
{
return new SystemContext(NUnitTelemetryContext.Create());
}
private static ArrayOf<Variant> BuildArray(params Variant[] values)
{
return new ArrayOf<Variant>(values);
}
private static MethodState NewPublishedDataItemsMethod(string dataSetName, string methodName)
{
var parent = new BaseObjectState(null)
{
NodeId = new NodeId($"pubsub:published-data-set:{dataSetName}", 0),
BrowseName = new QualifiedName(dataSetName)
};
var method = new MethodState(parent)
{
NodeId = new NodeId($"pubsub:published-data-set:{dataSetName}:{methodName}", 0),
BrowseName = new QualifiedName(methodName)
};
parent.AddChild(method);
return method;
}
private static readonly string[] s_currentTimeAlias = ["CurrentTime"];
private static readonly string[] s_eventIdAlias = ["EventId"];
private static readonly string[] s_startTimeAlias = ["StartTime"];
private static readonly bool[] s_notPromotedField = [false];
private static readonly uint[] s_firstVariableIndex = [0u];
private sealed class StubTransportFactory : IPubSubTransportFactory
{
public string TransportProfileUri => Profiles.PubSubUdpUadpTransport;
public IPubSubTransport Create(
PubSubConnectionDataType connection,
ITelemetryContext telemetry,
TimeProvider timeProvider)
{
_ = connection;
_ = telemetry;
_ = timeProvider;
return new StubTransport();
}
}
private sealed class StubTransport : IPubSubTransport
{
private bool m_isConnected;
public string TransportProfileUri => Profiles.PubSubUdpUadpTransport;
public PubSubTransportDirection Direction => PubSubTransportDirection.SendReceive;
public bool IsConnected => m_isConnected;
public event EventHandler<PubSubTransportStateChangedEventArgs>? StateChanged
{
add { }
remove { }
}
public ValueTask OpenAsync(CancellationToken cancellationToken = default)
{
_ = cancellationToken;
m_isConnected = true;
return default;
}
public ValueTask CloseAsync(CancellationToken cancellationToken = default)
{
_ = cancellationToken;
m_isConnected = false;
return default;
}
public ValueTask SendAsync(
ReadOnlyMemory<byte> payload,
string? topic = null,
CancellationToken cancellationToken = default)
{
_ = payload;
_ = topic;
_ = cancellationToken;
return default;
}
public IAsyncEnumerable<PubSubTransportFrame> ReceiveAsync(
CancellationToken cancellationToken = default)
{
_ = cancellationToken;
return TestAsyncEnumerable.Empty<PubSubTransportFrame>();
}
public ValueTask DisposeAsync()
{
m_isConnected = false;
return default;
}
}
}
}