using System.Security.Claims; using Bunit; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Forms; using ZB.MOM.WW.ScadaBridge.CentralUI.Services; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections; using ZB.MOM.WW.ScadaBridge.Security; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components; /// /// Covers the M7-B8 (T17) "Verify endpoint" affordance added to /// : a button that calls the injected /// with the current endpoint config and /// renders the typed — green on success, a red /// failure line otherwise, and a read-only certificate panel when the failure is /// with a captured cert. /// /// /// Also covers the M7-B10 (T17) "Trust certificate" affordance on the untrusted-cert /// panel: an Administrator-gated button that calls /// and re-runs Verify on success. /// /// public class OpcUaEndpointVerifyTests : BunitContext { private readonly IEndpointVerificationService _verify = Substitute.For(); private readonly ICertManagementService _certs = Substitute.For(); public OpcUaEndpointVerifyTests() { Services.AddSingleton(_verify); Services.AddSingleton(_certs); } private static ClaimsPrincipal BuildPrincipal(params string[] roles) { var claims = new List { new(JwtTokenService.UsernameClaimType, "tester") }; claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r))); return new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); } private bool _authConfigured; private void UseRoles(params string[] roles) { Services.AddSingleton( new TestAuthStateProvider(BuildPrincipal(roles))); Services.AddAuthorizationCore(); AuthorizationPolicies.AddScadaBridgeAuthorization(Services); Services.AddSingleton(); _authConfigured = true; } // The editor's untrusted-cert panel hosts an AuthorizeView (RequireAdmin) around // the Trust button, so the editor must render inside a CascadingAuthenticationState // with an auth provider available. Tests that don't call UseRoles get a default // Designer principal (can verify + see the cert panel, but NOT the Trust button). private IRenderedComponent RenderEditor() { if (!_authConfigured) { UseRoles(Roles.Designer); } var host = Render(parameters => parameters .Add(p => p.ChildContent, (Microsoft.AspNetCore.Components.RenderFragment)(builder => { builder.OpenComponent(0); builder.AddAttribute(1, nameof(OpcUaEndpointEditor.Config), new OpcUaEndpointConfig { EndpointUrl = "opc.tcp://host:4840" }); builder.AddAttribute(2, nameof(OpcUaEndpointEditor.SiteIdentifier), "plant-a"); builder.AddAttribute(3, nameof(OpcUaEndpointEditor.ConnectionName), "PLC-OPC"); builder.AddAttribute(4, nameof(OpcUaEndpointEditor.Protocol), "OpcUa"); builder.CloseComponent(); }))); return host.FindComponent(); } [Fact] public void Verify_Success_ShowsSuccessMessage() { _verify.VerifyAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new VerifyEndpointResult(true, null, null, null)); var cut = RenderEditor(); cut.Find("[data-test=verify-endpoint-btn]").Click(); Assert.NotEmpty(cut.FindAll("[data-test=verify-success]")); Assert.Empty(cut.FindAll("[data-test=verify-failure]")); Assert.Empty(cut.FindAll("[data-test=verify-cert-panel]")); } [Fact] public void Verify_UntrustedCertificate_ShowsCertPanelWithThumbprint() { var cert = new ServerCertInfo( Thumbprint: "ABCDEF0123456789", Subject: "CN=opc-server", Issuer: "CN=opc-server", NotBeforeUtc: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), NotAfterUtc: new DateTime(2027, 1, 1, 0, 0, 0, DateTimeKind.Utc), DerBase64: "ZGVy"); _verify.VerifyAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new VerifyEndpointResult( false, VerifyFailureKind.UntrustedCertificate, "Untrusted certificate.", cert)); var cut = RenderEditor(); cut.Find("[data-test=verify-endpoint-btn]").Click(); var panel = cut.Find("[data-test=verify-cert-panel]"); Assert.Contains("ABCDEF0123456789", panel.TextContent); Assert.Contains("CN=opc-server", panel.TextContent); // The failure line is still shown alongside the cert panel. Assert.NotEmpty(cut.FindAll("[data-test=verify-failure]")); } [Fact] public void Verify_GenericFailure_ShowsFailureMessage() { _verify.VerifyAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new VerifyEndpointResult( false, VerifyFailureKind.Unreachable, "Connection refused.", null)); var cut = RenderEditor(); cut.Find("[data-test=verify-endpoint-btn]").Click(); var failure = cut.Find("[data-test=verify-failure]"); Assert.Contains("Connection refused.", failure.TextContent); Assert.Empty(cut.FindAll("[data-test=verify-success]")); Assert.Empty(cut.FindAll("[data-test=verify-cert-panel]")); } private static ServerCertInfo SampleCert() => new( Thumbprint: "ABCDEF0123456789", Subject: "CN=opc-server", Issuer: "CN=opc-server", NotBeforeUtc: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), NotAfterUtc: new DateTime(2027, 1, 1, 0, 0, 0, DateTimeKind.Utc), DerBase64: "ZGVy"); private void ArrangeUntrustedVerify(ServerCertInfo cert) => _verify.VerifyAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new VerifyEndpointResult( false, VerifyFailureKind.UntrustedCertificate, "Untrusted certificate.", cert)); [Fact] public void TrustButton_Admin_SeesButton_AndClick_CallsTrustAsync() { UseRoles(Roles.Administrator); var cert = SampleCert(); ArrangeUntrustedVerify(cert); _certs.TrustAsync( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(new CertTrustResult(true, null, null)); var cut = RenderEditor(); cut.Find("[data-test=verify-endpoint-btn]").Click(); // Admin sees the Trust button on the untrusted-cert panel. var trustBtn = cut.Find("[data-test=trust-cert-btn]"); trustBtn.Click(); _certs.Received(1).TrustAsync( "plant-a", "PLC-OPC", cert.DerBase64, cert.Thumbprint, Arg.Any()); } [Fact] public void TrustButton_NonAdmin_DesignerOnly_DoesNotSeeButton() { UseRoles(Roles.Designer); ArrangeUntrustedVerify(SampleCert()); var cut = RenderEditor(); cut.Find("[data-test=verify-endpoint-btn]").Click(); // The cert panel is shown, but a Designer (non-Admin) does not get the Trust button. Assert.NotEmpty(cut.FindAll("[data-test=verify-cert-panel]")); Assert.Empty(cut.FindAll("[data-test=trust-cert-btn]")); } }