using System.Security.Claims; using Bunit; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Services; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Security; using ConnectionCertificates = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design.ConnectionCertificates; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Components; /// /// Covers the M7-B10 (T17) connection-certificates management page: an /// Administrator-gated page that resolves a data connection's owning site, lists /// the site's trusted-peer / rejected certificates via /// , and removes a certificate via /// (the store is node-wide per /// site node). /// public class ConnectionCertificatesTests : BunitContext { private readonly ICertManagementService _certs = Substitute.For(); private readonly ISiteRepository _siteRepo = Substitute.For(); private const int ConnectionId = 7; private const string SiteIdentifier = "plant-a"; public ConnectionCertificatesTests() { Services.AddSingleton(_certs); Services.AddSingleton(_siteRepo); // The page resolves the connection → owning site so the cert relay targets // the right site identifier (the trusted-peer store is node-wide per site node). var connection = new DataConnection("PLC-OPC", "OpcUa", 1) { Id = ConnectionId }; _siteRepo.GetDataConnectionByIdAsync(ConnectionId, Arg.Any()) .Returns(Task.FromResult(connection)); _siteRepo.GetSiteByIdAsync(1, Arg.Any()) .Returns(Task.FromResult(new Site("Plant-A", SiteIdentifier) { Id = 1 })); UseRoles(Roles.Administrator); } 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 void UseRoles(params string[] roles) { Services.AddSingleton( new TestAuthStateProvider(BuildPrincipal(roles))); Services.AddAuthorizationCore(); AuthorizationPolicies.AddScadaBridgeAuthorization(Services); Services.AddSingleton(); } private static TrustedCertInfo Cert(string thumbprint, bool rejected = false) => new( Thumbprint: thumbprint, Subject: $"CN={thumbprint}", Issuer: "CN=ca", NotBeforeUtc: new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), NotAfterUtc: new DateTime(2027, 1, 1, 0, 0, 0, DateTimeKind.Utc), Rejected: rejected); private IRenderedComponent RenderPage() { var host = Render(parameters => parameters .Add(p => p.ChildContent, (RenderFragment)(builder => { builder.OpenComponent(0); builder.AddAttribute(1, nameof(ConnectionCertificates.Id), ConnectionId); builder.CloseComponent(); }))); return host.FindComponent(); } [Fact] public void Lists_Two_Certs_From_Owning_Site() { _certs.ListAsync(SiteIdentifier, Arg.Any()) .Returns(new CertTrustResult(true, null, new[] { Cert("AAAA1111"), Cert("BBBB2222", rejected: true), })); var cut = RenderPage(); Assert.NotEmpty(cut.FindAll("[data-test=connection-certificates]")); var rows = cut.FindAll("[data-test=cert-row]"); Assert.Equal(2, rows.Count); Assert.Contains("AAAA1111", cut.Markup); Assert.Contains("BBBB2222", cut.Markup); // List was resolved against the connection's owning site identifier. _certs.Received(1).ListAsync(SiteIdentifier, Arg.Any()); } [Fact] public void Remove_Calls_RemoveAsync_With_SiteIdentifier_And_Thumbprint() { _certs.ListAsync(SiteIdentifier, Arg.Any()) .Returns(new CertTrustResult(true, null, new[] { Cert("AAAA1111") })); _certs.RemoveAsync(SiteIdentifier, "AAAA1111", Arg.Any()) .Returns(new CertTrustResult(true, null, null)); var cut = RenderPage(); cut.Find("[data-test=cert-remove-btn]").Click(); _certs.Received(1).RemoveAsync(SiteIdentifier, "AAAA1111", Arg.Any()); } [Fact] public void Empty_Store_Shows_Empty_Note() { _certs.ListAsync(SiteIdentifier, Arg.Any()) .Returns(new CertTrustResult(true, null, Array.Empty())); var cut = RenderPage(); Assert.NotEmpty(cut.FindAll("[data-test=cert-empty]")); Assert.Empty(cut.FindAll("[data-test=cert-row]")); } [Fact] public void List_Failure_Shows_Load_Error() { _certs.ListAsync(SiteIdentifier, Arg.Any()) .Returns(new CertTrustResult(false, "Site unreachable.", null)); var cut = RenderPage(); var error = cut.Find("[data-test=cert-load-error]"); Assert.Contains("Site unreachable.", error.TextContent); } }