/* ======================================================================== * 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.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NUnit.Framework; using Opc.Ua.PubSub.Application; using Opc.Ua.PubSub.Security; using Opc.Ua.PubSub.Security.Policies; using Opc.Ua.PubSub.Security.Sks; using Opc.Ua.PubSub.Tests.Security.Sks; using Opc.Ua.PubSub.Transports; using Opc.Ua.Tests; namespace Opc.Ua.PubSub.Tests.DependencyInjection { /// /// Unit tests for /// . /// [TestFixture] [SetCulture("en-us")] [SetUICulture("en-us")] public class PubSubSecurityServiceCollectionExtensionsTests { private const string GroupId = "group-1"; private static string PolicyUri => PubSubSecurityPolicyUri.PubSubAes128Ctr; private static FakeSecurityKeyService CreateFake() { return new FakeSecurityKeyService( PubSubSecurityPolicyRegistry.Default.GetByUri(PolicyUri)!, TimeSpan.FromHours(1)); } [Test] public async Task AddPubSubSecurityKeyServiceClientRegistersProviderAndStarterAsync() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); IOpcUaBuilder builder = services.AddOpcUa(); builder.AddPubSubSecurityKeyServiceClient(GroupId, PolicyUri, _ => CreateFake()); await using ServiceProvider sp = services.BuildServiceProvider(); IPubSubSecurityKeyProvider provider = sp.GetServices().Single(); Assert.That(provider, Is.TypeOf()); Assert.That(provider.SecurityGroupId, Is.EqualTo(GroupId)); Assert.That( sp.GetServices().OfType().Count(), Is.EqualTo(1)); } [Test] public async Task AddSecurityKeyServiceClientOnPubSubBuilderRegistersProviderAndStarterAsync() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); services.AddLogging(); services.AddOpcUa().AddPubSub(pubsub => pubsub.AddSecurityKeyServiceClient( GroupId, PolicyUri, _ => CreateFake())); await using ServiceProvider sp = services.BuildServiceProvider(); Assert.Multiple(() => { Assert.That(sp.GetServices().Single(), Is.TypeOf()); Assert.That( sp.GetServices().OfType().Count(), Is.EqualTo(1)); }); } [Test] public async Task AddSecurityKeyServiceClientOnPubSubBuilderBuildsSecuredApplicationAsync() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); services.AddLogging(); services.AddPubSubTransportFactory(_ => new StubTransportFactory()); services.AddOpcUa().AddPubSub(pubsub => pubsub .UseConfiguration(CreateSecuredConfiguration()) .AddSecurityKeyServiceClient(GroupId, PolicyUri, _ => CreateFake())); await using ServiceProvider sp = services.BuildServiceProvider(); IPubSubApplication app = sp.GetRequiredService(); Assert.That(app.Connections, Has.Count.EqualTo(1)); } [Test] public async Task AddSecurityKeyServiceServerOnPubSubBuilderRegistersSingletonAsync() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); services.AddOpcUa().AddPubSub(pubsub => pubsub.AddSecurityKeyServiceServer()); await using ServiceProvider sp = services.BuildServiceProvider(); InMemoryPubSubKeyServiceServer? server = sp.GetService(); Assert.Multiple(() => { Assert.That(server, Is.Not.Null); Assert.That(sp.GetService(), Is.SameAs(server)); }); } [Test] public async Task AddSecurityKeyPushTargetOnPubSubBuilderRegistersProviderAsync() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); services.AddOpcUa().AddPubSub(pubsub => pubsub.AddSecurityKeyPushTarget(GroupId)); await using ServiceProvider sp = services.BuildServiceProvider(); Assert.That( sp.GetServices().Single(), Is.TypeOf()); } [Test] public async Task AddSecurityKeyPushTargetOnPubSubBuilderBuildsSecuredApplicationAsync() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); services.AddLogging(); services.AddPubSubTransportFactory(_ => new StubTransportFactory()); services.AddOpcUa().AddPubSub(pubsub => pubsub .UseConfiguration(CreateSecuredConfiguration()) .AddSecurityKeyPushTarget(GroupId)); await using ServiceProvider sp = services.BuildServiceProvider(); IPubSubApplication app = sp.GetRequiredService(); Assert.That(app.Connections, Has.Count.EqualTo(1)); } [Test] public async Task AddPubSubSecurityKeyServiceClientStartsProviderThatServesKeysAsync() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); IOpcUaBuilder builder = services.AddOpcUa(); builder.AddPubSubSecurityKeyServiceClient( GroupId, PolicyUri, _ => CreateFake(), options => options.RequestedFutureKeyCount = 1); await using ServiceProvider sp = services.BuildServiceProvider(); PubSubSecurityKeyProviderStarter starter = sp .GetServices() .OfType() .Single(); await starter.StartAsync(CancellationToken.None).ConfigureAwait(false); var provider = (PullSecurityKeyProvider)sp.GetServices().Single(); PubSubSecurityKey key = await provider.GetCurrentKeyAsync().ConfigureAwait(false); Assert.That(key.TokenId, Is.EqualTo(1U)); } [Test] public async Task AddPubSubSecurityKeyServiceClientRegistersStarterOnlyOnceAsync() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); IOpcUaBuilder builder = services.AddOpcUa(); builder.AddPubSubSecurityKeyServiceClient("group-1", PolicyUri, _ => CreateFake()); builder.AddPubSubSecurityKeyServiceClient("group-2", PolicyUri, _ => CreateFake()); await using ServiceProvider sp = services.BuildServiceProvider(); Assert.That(sp.GetServices().Count(), Is.EqualTo(2)); Assert.That( sp.GetServices().OfType().Count(), Is.EqualTo(1)); } [Test] public void AddPubSubSecurityKeyServiceClientUnknownPolicyThrows() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); IOpcUaBuilder builder = services.AddOpcUa(); Assert.That( () => builder.AddPubSubSecurityKeyServiceClient(GroupId, "urn:not-a-policy", _ => CreateFake()), Throws.ArgumentException); } [Test] public void AddPubSubSecurityKeyServiceClientNullBuilderThrows() { IOpcUaBuilder? builder = null; Assert.That( () => builder!.AddPubSubSecurityKeyServiceClient(GroupId, PolicyUri, _ => CreateFake()), Throws.ArgumentNullException); } [Test] public void AddPubSubSecurityKeyServiceServerRegistersSingleton() { var services = new ServiceCollection(); services.AddSingleton(NUnitTelemetryContext.Create()); IOpcUaBuilder builder = services.AddOpcUa(); builder.AddPubSubSecurityKeyServiceServer(); using ServiceProvider sp = services.BuildServiceProvider(); InMemoryPubSubKeyServiceServer? server = sp.GetService(); Assert.Multiple(() => { Assert.That(server, Is.Not.Null); Assert.That(sp.GetService(), Is.SameAs(server)); }); } [Test] public void AddPubSubSecurityKeyServiceServerNullBuilderThrows() { IOpcUaBuilder? builder = null; Assert.That( () => builder!.AddPubSubSecurityKeyServiceServer(), Throws.ArgumentNullException); } private static PubSubConfigurationDataType CreateSecuredConfiguration() { return new PubSubConfigurationDataType { Connections = [ new PubSubConnectionDataType { Name = "secured-conn", TransportProfileUri = Profiles.PubSubUdpUadpTransport, PublisherId = new Variant((ushort)7), Address = new ExtensionObject( new NetworkAddressUrlDataType { Url = "opc.udp://224.0.0.22:4840" }), WriterGroups = [ new WriterGroupDataType { Name = "wg", WriterGroupId = 1, PublishingInterval = 1000, SecurityMode = MessageSecurityMode.SignAndEncrypt, SecurityGroupId = GroupId, SecurityKeyServices = [ new EndpointDescription { EndpointUrl = "opc.tcp://localhost:4840" } ] } ] } ], PublishedDataSets = [] }; } private sealed class StubTransportFactory : IPubSubTransportFactory { public string TransportProfileUri => Profiles.PubSubUdpUadpTransport; public IPubSubTransport Create( PubSubConnectionDataType connection, ITelemetryContext telemetry, TimeProvider timeProvider) { return new StubTransport(); } } private sealed class StubTransport : IPubSubTransport { public string TransportProfileUri => Profiles.PubSubUdpUadpTransport; public PubSubTransportDirection Direction => PubSubTransportDirection.SendReceive; public bool IsConnected => false; public event EventHandler? StateChanged { add { } remove { } } public ValueTask OpenAsync(CancellationToken cancellationToken = default) { return default; } public ValueTask CloseAsync(CancellationToken cancellationToken = default) { return default; } public ValueTask SendAsync( ReadOnlyMemory payload, string? topic = null, CancellationToken cancellationToken = default) { return default; } public IAsyncEnumerable ReceiveAsync( CancellationToken cancellationToken = default) { return TestAsyncEnumerable.Empty(); } public ValueTask DisposeAsync() { return default; } } } }