using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Services;
/// Gitea #43: the Central UI cert-trust facade records every trust decision in the central audit log.
public class CertManagementServiceAuditTests
{
private readonly ICentralAuditWriter _audit = Substitute.For();
private sealed class FakeComm(CertTrustResult? result, Exception? throws = null)
: CommunicationService(Options.Create(new CommunicationOptions()), NullLogger.Instance)
{
public override Task TrustServerCertAsync(string siteId, TrustServerCertCommand command, CancellationToken cancellationToken = default)
=> throws is null ? Task.FromResult(result!) : Task.FromException(throws);
public override Task RemoveServerCertAsync(string siteId, RemoveServerCertCommand command, CancellationToken cancellationToken = default)
=> throws is null ? Task.FromResult(result!) : Task.FromException(throws);
}
private sealed class AuthStub(ClaimsPrincipal user) : AuthenticationStateProvider
{
public override Task GetAuthenticationStateAsync() => Task.FromResult(new AuthenticationState(user));
}
private static ClaimsPrincipal Admin() => new(new ClaimsIdentity(
[
new Claim(JwtTokenService.UsernameClaimType, "alice"),
new Claim(JwtTokenService.RoleClaimType, Roles.Administrator),
], "test"));
private CertManagementService Sut(CommunicationService comm, ClaimsPrincipal? user = null)
=> new(comm, new AuthStub(user ?? Admin()), _audit, NullLogger.Instance);
private AuditEvent Captured() => Assert.Single(_audit.ReceivedCalls()
.Where(c => c.GetMethodInfo().Name == nameof(ICentralAuditWriter.WriteAsync))
.Select(c => (AuditEvent)c.GetArguments()[0]!));
/// A successful trust writes a Delivered CertTrust row naming the actor, site and thumbprint.
[Fact]
public async Task Trust_Success_WritesDeliveredRow()
{
var r = await Sut(new FakeComm(new CertTrustResult(true, null, null))).TrustAsync("SITE1", "conn", "ZGVy", "AB12");
Assert.True(r.Success);
var evt = Captured();
Assert.Equal("alice", evt.Actor);
Assert.Equal("SITE1/AB12", evt.Target);
Assert.Contains("\"kind\":\"CertTrust\"", evt.DetailsJson);
Assert.Contains("\"status\":\"Delivered\"", evt.DetailsJson);
}
/// A failed relay is audited as Failed with the error, and the page still gets its inline result.
[Fact]
public async Task Trust_RelayThrows_WritesFailedRow_AndReturnsInlineError()
{
var r = await Sut(new FakeComm(null, new TimeoutException("site unreachable"))).TrustAsync("SITE1", "conn", "ZGVy", "AB12");
Assert.False(r.Success);
var evt = Captured();
Assert.Contains("\"status\":\"Failed\"", evt.DetailsJson);
Assert.Contains("site unreachable", evt.DetailsJson);
}
/// A removal writes a CertTrustRemove row.
[Fact]
public async Task Remove_Success_WritesRemoveRow()
{
await Sut(new FakeComm(new CertTrustResult(true, null, null))).RemoveAsync("SITE1", "AB12");
Assert.Contains("\"kind\":\"CertTrustRemove\"", Captured().DetailsJson);
}
/// An unauthorized caller never reaches the site and writes no audit row.
[Fact]
public async Task Trust_Unauthorized_WritesNothing()
{
var viewer = new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtTokenService.UsernameClaimType, "bob")], "test"));
var r = await Sut(new FakeComm(new CertTrustResult(true, null, null)), viewer).TrustAsync("SITE1", "conn", "ZGVy", "AB12");
Assert.False(r.Success);
Assert.Empty(_audit.ReceivedCalls());
}
/// An audit-writer failure never changes the trust result.
[Fact]
public async Task Trust_AuditWriterThrows_ResultUnaffected()
{
_audit.WriteAsync(Arg.Any(), Arg.Any()).Returns(Task.FromException(new InvalidOperationException("db down")));
var r = await Sut(new FakeComm(new CertTrustResult(true, null, null))).TrustAsync("SITE1", "conn", "ZGVy", "AB12");
Assert.True(r.Success);
}
}