using System.Text.Json; using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests; /// /// The VerifyAuditChain management command (ScadaBridge#41): role gate, month /// parsing, site-scope refusal, and the verification result reaching the wire with its /// enums as names. /// public class VerifyAuditChainCommandTests : TestKit { private readonly IAuditLogHashChain _chain = Substitute.For(); private IActorRef CreateActor() { var services = new ServiceCollection().AddTestConfigurationTransaction(); services.AddScoped(_ => _chain); return Sys.ActorOf(Props.Create(() => new ManagementActor( services.BuildServiceProvider(), NullLogger.Instance))); } private static ManagementEnvelope Envelope(object command, string[] sites, params string[] roles) => new(new AuthenticatedUser("u", "U", roles, sites), command, Guid.NewGuid().ToString("N")); /// A Viewer gets the verifier's result for the requested month, enums as names. [Fact] public void Viewer_GetsVerificationResult_ForTheRequestedMonth() { var brk = new AuditChainBreak("2026-05", 7, Guid.Parse("11111111-2222-3333-4444-555555555555"), AuditChainBreakKind.HashMismatch, "edited"); _chain.VerifyMonthAsync(Arg.Any(), Arg.Any()) .Returns(new AuditChainVerificationResult("2026-05", AuditChainStatus.Broken, 6, 0, null, 43, 2, brk)); CreateActor().Tell(Envelope(new VerifyAuditChainCommand("2026-05"), [], "Viewer")); var response = ExpectMsg(TimeSpan.FromSeconds(5)); using var json = JsonDocument.Parse(response.JsonData); Assert.Equal("Broken", json.RootElement.GetProperty("status").GetString()); Assert.Equal("HashMismatch", json.RootElement.GetProperty("firstBreak").GetProperty("kind").GetString()); Assert.Equal(7, json.RootElement.GetProperty("firstBreak").GetProperty("chainSeq").GetInt64()); _chain.Received(1).VerifyMonthAsync( new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc), Arg.Any()); } /// A Designer/Deployer without an OperationalAudit role is refused. [Fact] public void NonAuditRole_IsUnauthorized() { CreateActor().Tell(Envelope(new VerifyAuditChainCommand("2026-05"), [], "Designer", "Deployer")); ExpectMsg(TimeSpan.FromSeconds(5)); _chain.DidNotReceiveWithAnyArgs().VerifyMonthAsync(default, default); } /// A site-scoped Viewer is refused: the chain spans every site. [Fact] public void SiteScopedViewer_IsRefused() { CreateActor().Tell(Envelope(new VerifyAuditChainCommand("2026-05"), ["3"], "Viewer")); var response = ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Contains("spans every site", response.Message); _chain.DidNotReceiveWithAnyArgs().VerifyMonthAsync(default, default); } /// A malformed month is a caller error, not an internal fault. [Fact] public void MalformedMonth_IsInvalidArgument() { CreateActor().Tell(Envelope(new VerifyAuditChainCommand("2026-13"), [], "Administrator")); var response = ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Equal("INVALID_ARGUMENT", response.ErrorCode); } /// A verification that cannot get the chain lock surfaces as CHAIN_BUSY. [Fact] public void BusyChain_IsChainBusy() { _chain.VerifyMonthAsync(Arg.Any(), Arg.Any()) .Returns>(_ => throw new TimeoutException("busy")); CreateActor().Tell(Envelope(new VerifyAuditChainCommand("2026-05"), [], "Administrator")); var response = ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Equal("CHAIN_BUSY", response.ErrorCode); } /// Verification runs under the long-running Ask timeout, not the 30 s default. [Fact] public void VerifyAuditChain_UsesLongRunningAskTimeout() { Assert.Equal(TimeSpan.FromMinutes(5), ManagementEndpoints.ResolveAskTimeout(new ManagementServiceOptions(), typeof(VerifyAuditChainCommand))); } }