using Microsoft.Extensions.Logging.Abstractions; using Opc.Ua; using Opc.Ua.Server; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; /// /// F13b — verifies publishes one /// per and emits both /// Anonymous and UserName entries. The pure-builder tests run /// cross-platform without touching disk; the boot-verify test reuses the F13a PKI pattern. /// 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}"); [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); } [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); } [Fact] public void BuildSecurityPolicies_empty_input_falls_back_to_none() { var policies = OpcUaApplicationHost.BuildSecurityPolicies(Array.Empty()).ToList(); policies.Count.ShouldBe(1); policies[0].SecurityMode.ShouldBe(MessageSecurityMode.None); policies[0].SecurityPolicyUri.ShouldBe(SecurityPolicies.None); } [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); } [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.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 }); } [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.Basic256Sha256SignAndEncrypt }, AutoAcceptUntrustedClientCertificates = false, }, NullLogger.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; } public void Dispose() { if (Directory.Exists(_pkiRoot)) { try { Directory.Delete(_pkiRoot, recursive: true); } catch { /* best-effort cleanup */ } } } }