Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/OpcUaApplicationHostSecurityTests.cs
T
Joseph Doherty 64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
docs: backfill XML documentation across 756 files
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public
members surfaced by commentchecker — resolves 5,847 of 5,869 issues
(99.6%) across three /fixdocs passes.
2026-05-28 08:10:17 -04:00

179 lines
7.1 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Server;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>
/// F13b — verifies <see cref="OpcUaApplicationHost"/> publishes one
/// <see cref="ServerSecurityPolicy"/> per <see cref="OpcUaSecurityProfile"/> and emits both
/// Anonymous and UserName <see cref="UserTokenPolicy"/> entries. The pure-builder tests run
/// cross-platform without touching disk; the boot-verify test reuses the F13a PKI pattern.
/// </summary>
public sealed class OpcUaApplicationHostSecurityTests : IDisposable
{
private static CancellationToken Ct => TestContext.Current.CancellationToken;
private readonly string _pkiRoot = Path.Combine(
Path.GetTempPath(),
$"otopcua-pki-{Guid.NewGuid():N}");
/// <summary>
/// Verifies that BuildSecurityPolicies emits all three baseline security profiles.
/// </summary>
[Fact]
public void BuildSecurityPolicies_default_set_emits_all_three_baseline_profiles()
{
var policies = OpcUaApplicationHost.BuildSecurityPolicies(new[]
{
OpcUaSecurityProfile.None,
OpcUaSecurityProfile.Basic256Sha256Sign,
OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
}).ToList();
policies.Count.ShouldBe(3);
policies[0].SecurityMode.ShouldBe(MessageSecurityMode.None);
policies[0].SecurityPolicyUri.ShouldBe(SecurityPolicies.None);
policies[1].SecurityMode.ShouldBe(MessageSecurityMode.Sign);
policies[1].SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
policies[2].SecurityMode.ShouldBe(MessageSecurityMode.SignAndEncrypt);
policies[2].SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
}
/// <summary>
/// Verifies that BuildSecurityPolicies deduplicates repeated profiles.
/// </summary>
[Fact]
public void BuildSecurityPolicies_dedupes_repeated_profiles()
{
var policies = OpcUaApplicationHost.BuildSecurityPolicies(new[]
{
OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
OpcUaSecurityProfile.None,
}).ToList();
policies.Count.ShouldBe(2);
policies[0].SecurityMode.ShouldBe(MessageSecurityMode.SignAndEncrypt);
policies[1].SecurityMode.ShouldBe(MessageSecurityMode.None);
}
/// <summary>
/// Verifies that BuildSecurityPolicies falls back to None when given empty input.
/// </summary>
[Fact]
public void BuildSecurityPolicies_empty_input_falls_back_to_none()
{
var policies = OpcUaApplicationHost.BuildSecurityPolicies(Array.Empty<OpcUaSecurityProfile>()).ToList();
policies.Count.ShouldBe(1);
policies[0].SecurityMode.ShouldBe(MessageSecurityMode.None);
policies[0].SecurityPolicyUri.ShouldBe(SecurityPolicies.None);
}
/// <summary>
/// Verifies that BuildUserTokenPolicies emits anonymous and username policies.
/// </summary>
[Fact]
public void BuildUserTokenPolicies_emits_anonymous_and_username()
{
var tokens = OpcUaApplicationHost.BuildUserTokenPolicies().ToList();
tokens.Count.ShouldBe(2);
tokens.ShouldContain(t => t.TokenType == UserTokenType.Anonymous && t.PolicyId == "anonymous");
var userName = tokens.Single(t => t.TokenType == UserTokenType.UserName);
userName.PolicyId.ShouldBe("username_basic256sha256");
userName.SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
}
/// <summary>
/// Verifies that StartAsync populates ServerConfiguration with all enabled security profiles.
/// </summary>
[Fact]
public async Task StartAsync_populates_ServerConfiguration_with_all_enabled_profiles()
{
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SecAll",
ApplicationUri = $"urn:OtOpcUa.SecAll:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
EnabledSecurityProfiles =
{
OpcUaSecurityProfile.None,
OpcUaSecurityProfile.Basic256Sha256Sign,
OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt,
},
AutoAcceptUntrustedClientCertificates = true,
},
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(new StandardServer(), Ct);
var config = host.ApplicationInstance!.ApplicationConfiguration;
config.ServerConfiguration.SecurityPolicies.Count.ShouldBe(3);
config.ServerConfiguration.UserTokenPolicies.Count.ShouldBe(2);
config.SecurityConfiguration.AutoAcceptUntrustedCertificates.ShouldBeTrue();
var modes = config.ServerConfiguration.SecurityPolicies
.Select(p => p.SecurityMode)
.OrderBy(m => (int)m)
.ToArray();
modes.ShouldBe(new[] { MessageSecurityMode.None, MessageSecurityMode.Sign, MessageSecurityMode.SignAndEncrypt });
}
/// <summary>
/// Verifies that StartAsync with only SignAndEncrypt omits the None endpoint.
/// </summary>
[Fact]
public async Task StartAsync_with_only_signandencrypt_omits_None_endpoint()
{
await using var host = new OpcUaApplicationHost(
new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SecHardened",
ApplicationUri = $"urn:OtOpcUa.SecHardened:{Guid.NewGuid():N}",
OpcUaPort = AllocateFreePort(),
PublicHostname = "localhost",
PkiStoreRoot = _pkiRoot,
EnabledSecurityProfiles = new List<OpcUaSecurityProfile> { OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt },
AutoAcceptUntrustedClientCertificates = false,
},
NullLogger<OpcUaApplicationHost>.Instance);
await host.StartAsync(new StandardServer(), Ct);
var policies = host.ApplicationInstance!.ApplicationConfiguration.ServerConfiguration.SecurityPolicies;
policies.Count.ShouldBe(1);
policies[0].SecurityMode.ShouldBe(MessageSecurityMode.SignAndEncrypt);
policies[0].SecurityPolicyUri.ShouldBe(SecurityPolicies.Basic256Sha256);
host.ApplicationInstance.ApplicationConfiguration.SecurityConfiguration
.AutoAcceptUntrustedCertificates.ShouldBeFalse();
}
private static int AllocateFreePort()
{
using var listener = new System.Net.Sockets.TcpListener(System.Net.IPAddress.Loopback, 0);
listener.Start();
var port = ((System.Net.IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
/// <summary>
/// Cleans up temporary PKI files.
/// </summary>
public void Dispose()
{
if (Directory.Exists(_pkiRoot))
{
try { Directory.Delete(_pkiRoot, recursive: true); }
catch { /* best-effort cleanup */ }
}
}
}