/* ========================================================================
* 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.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Opc.Ua.Configuration;
namespace Opc.Ua.Client.Tests.ClientBuilder
{
///
/// Exercises the provider-agnostic part of the eager client
/// configuration load: the public
/// surface and
/// the hosted
/// service that awaits
/// during
/// host start. The supplied-document (AddClient(configurationFile))
/// cases live in .
///
[TestFixture]
[Category("Client")]
[Category("ClientBuilder")]
[SetCulture("en-us")]
[SetUICulture("en-us")]
[Parallelizable]
public sealed class ClientEagerConfigurationLoadTests
{
[Test]
public async Task LoadConfigurationOnStartAwaitsSharedProviderAsync()
{
ApplicationConfiguration configuration = CreateConfiguration();
Mock configurationProvider =
CreateProviderMock(configuration);
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(configurationProvider.Object);
services.AddOpcUa().AddClient(options => options.LoadConfigurationOnStart = true);
await using ServiceProvider provider = services.BuildServiceProvider();
OpcUaClientOptions options = provider.GetRequiredService();
// The shared provider is exposed publicly, so an application can
// load and read the configuration back without a session.
Assert.That(
options.ConfigurationProvider,
Is.SameAs(configurationProvider.Object));
IHostedService loader = GetLoader(provider);
await loader.StartAsync(CancellationToken.None).ConfigureAwait(false);
configurationProvider.Verify(
p => p.GetAsync(It.IsAny()),
Times.Once);
Assert.That(options.Configuration, Is.SameAs(configuration));
await loader.StopAsync(CancellationToken.None).ConfigureAwait(false);
}
[Test]
public async Task LoadConfigurationOnStartIsNoOpForExplicitConfigurationAsync()
{
ApplicationConfiguration configuration = CreateConfiguration();
var services = new ServiceCollection();
services.AddLogging();
services.AddOpcUa().AddClient(options =>
{
options.Configuration = configuration;
options.LoadConfigurationOnStart = true;
});
await using ServiceProvider provider = services.BuildServiceProvider();
OpcUaClientOptions options = provider.GetRequiredService();
// Nothing to load: an explicit configuration is already complete.
Assert.That(options.ConfigurationProvider, Is.Null);
IHostedService loader = GetLoader(provider);
Assert.That(
() => loader.StartAsync(CancellationToken.None),
Throws.Nothing);
Assert.That(options.Configuration, Is.SameAs(configuration));
}
[Test]
public async Task LoadConfigurationOnStartCancellationAbortsHostStartAsync()
{
ApplicationConfiguration configuration = CreateConfiguration();
var started = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
var configurationProvider =
new Mock();
configurationProvider.Setup(p => p.Configuration).Returns(configuration);
configurationProvider
.Setup(p => p.GetAsync(It.IsAny()))
.Returns(async (CancellationToken ct) =>
{
started.TrySetResult(true);
await Task.Delay(Timeout.Infinite, ct).ConfigureAwait(false);
return configuration;
});
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(configurationProvider.Object);
services.AddOpcUa().AddClient(options => options.LoadConfigurationOnStart = true);
await using ServiceProvider provider = services.BuildServiceProvider();
IHostedService loader = GetLoader(provider);
using var cts = new CancellationTokenSource();
Task start = loader.StartAsync(cts.Token);
await started.Task.ConfigureAwait(false);
cts.Cancel();
// The host's startup token is passed through to the load.
Assert.That(
async () => await start.ConfigureAwait(false),
Throws.InstanceOf());
}
[Test]
public async Task LoadConfigurationOnStartBindsFromConfigurationSectionAsync()
{
IConfiguration configurationRoot = new ConfigurationBuilder()
.Add(new MemoryConfigurationSource
{
InitialData = new Dictionary
{
["OpcUa:Client:LoadConfigurationOnStart"] = "true"
}
})
.Build();
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(CreateProviderMock(CreateConfiguration()).Object);
services.AddOpcUa().AddClient(configurationRoot);
await using ServiceProvider provider = services.BuildServiceProvider();
Assert.That(
provider.GetRequiredService().LoadConfigurationOnStart,
Is.True);
Assert.That(GetLoader(provider), Is.Not.Null);
}
#if NET8_0_OR_GREATER
[Test]
public async Task HostStartAsyncCompletesTheConfigurationLoadAsync()
{
ApplicationConfiguration configuration = CreateConfiguration();
Mock configurationProvider =
CreateProviderMock(configuration);
using IHost host = new HostBuilder()
.ConfigureServices((_, services) =>
{
services.AddLogging();
services.AddSingleton(configurationProvider.Object);
services.AddOpcUa().AddClient(
options => options.LoadConfigurationOnStart = true);
})
.Build();
await host.StartAsync().ConfigureAwait(false);
try
{
// When StartAsync returns the configuration is loaded and
// readable through the public options surface, which is what
// user-interface hosts need before any session exists.
OpcUaClientOptions options =
host.Services.GetRequiredService();
Assert.That(
options.ConfigurationProvider!.Configuration,
Is.SameAs(configuration));
Assert.That(options.Configuration, Is.SameAs(configuration));
configurationProvider.Verify(
p => p.GetAsync(It.IsAny()),
Times.Once);
}
finally
{
await host.StopAsync().ConfigureAwait(false);
}
}
#endif
private static IHostedService GetLoader(IServiceProvider provider)
{
return provider.GetServices()
.Single(service => service is ClientConfigurationLoaderHostedService);
}
private static Mock CreateProviderMock(
ApplicationConfiguration configuration)
{
var configurationProvider =
new Mock();
configurationProvider.Setup(p => p.Configuration).Returns(configuration);
configurationProvider
.Setup(p => p.GetAsync(It.IsAny()))
.ReturnsAsync(configuration);
return configurationProvider;
}
private static ApplicationConfiguration CreateConfiguration()
{
return new ApplicationConfiguration(
Opc.Ua.Tests.NUnitTelemetryContext.Create());
}
}
}