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); } }