145 lines
5.8 KiB
C#
145 lines
5.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// <see cref="ICertManagementService.ListAsync"/>, and removes a certificate via
|
|
/// <see cref="ICertManagementService.RemoveAsync"/> (the store is node-wide per
|
|
/// site node).
|
|
/// </summary>
|
|
public class ConnectionCertificatesTests : BunitContext
|
|
{
|
|
private readonly ICertManagementService _certs = Substitute.For<ICertManagementService>();
|
|
private readonly ISiteRepository _siteRepo = Substitute.For<ISiteRepository>();
|
|
|
|
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<CancellationToken>())
|
|
.Returns(Task.FromResult<DataConnection?>(connection));
|
|
_siteRepo.GetSiteByIdAsync(1, Arg.Any<CancellationToken>())
|
|
.Returns(Task.FromResult<Site?>(new Site("Plant-A", SiteIdentifier) { Id = 1 }));
|
|
|
|
UseRoles(Roles.Administrator);
|
|
}
|
|
|
|
private static ClaimsPrincipal BuildPrincipal(params string[] roles)
|
|
{
|
|
var claims = new List<Claim> { 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<AuthenticationStateProvider>(
|
|
new TestAuthStateProvider(BuildPrincipal(roles)));
|
|
Services.AddAuthorizationCore();
|
|
AuthorizationPolicies.AddScadaBridgeAuthorization(Services);
|
|
Services.AddSingleton<IAuthorizationService, DefaultAuthorizationService>();
|
|
}
|
|
|
|
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<ConnectionCertificates> RenderPage()
|
|
{
|
|
var host = Render<CascadingAuthenticationState>(parameters => parameters
|
|
.Add(p => p.ChildContent, (RenderFragment)(builder =>
|
|
{
|
|
builder.OpenComponent<ConnectionCertificates>(0);
|
|
builder.AddAttribute(1, nameof(ConnectionCertificates.Id), ConnectionId);
|
|
builder.CloseComponent();
|
|
})));
|
|
return host.FindComponent<ConnectionCertificates>();
|
|
}
|
|
|
|
[Fact]
|
|
public void Lists_Two_Certs_From_Owning_Site()
|
|
{
|
|
_certs.ListAsync(SiteIdentifier, Arg.Any<CancellationToken>())
|
|
.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<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public void Remove_Calls_RemoveAsync_With_SiteIdentifier_And_Thumbprint()
|
|
{
|
|
_certs.ListAsync(SiteIdentifier, Arg.Any<CancellationToken>())
|
|
.Returns(new CertTrustResult(true, null, new[] { Cert("AAAA1111") }));
|
|
_certs.RemoveAsync(SiteIdentifier, "AAAA1111", Arg.Any<CancellationToken>())
|
|
.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<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public void Empty_Store_Shows_Empty_Note()
|
|
{
|
|
_certs.ListAsync(SiteIdentifier, Arg.Any<CancellationToken>())
|
|
.Returns(new CertTrustResult(true, null, Array.Empty<TrustedCertInfo>()));
|
|
|
|
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<CancellationToken>())
|
|
.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);
|
|
}
|
|
}
|