using System.Net; using System.Net.Sockets; using System.Text; using System.Text.Json; using ZB.MOM.WW.ScadaBridge.CLI.Commands; namespace ZB.MOM.WW.ScadaBridge.CLI.Tests.Commands; /// /// Tests for the scadabridge audit verify-chain subcommand (ScadaBridge#41). The /// command sends the VerifyAuditChain management command for the month and maps /// the verification status onto the exit code: 0 intact / not chained, 3 broken. /// [Collection("Console")] public class AuditVerifyChainCommandTests { private const string IntactBody = "{\"partition\":\"2026-05\",\"status\":\"Intact\",\"rowsVerified\":42,\"tombstonesApplied\":1,\"headSeq\":43,\"unchainedRows\":0}"; private const string BrokenBody = "{\"partition\":\"2026-05\",\"status\":\"Broken\",\"rowsVerified\":6,\"tombstonesApplied\":0,\"headSeq\":43,\"unchainedRows\":2," + "\"firstBreak\":{\"partition\":\"2026-05\",\"chainSeq\":7,\"eventId\":\"11111111-2222-3333-4444-555555555555\",\"kind\":\"HashMismatch\",\"detail\":\"edited\"}}"; /// /// CLI round trip: the request carries VerifyAuditChain + the month, and the /// server's verdict drives the exit code and the report. /// /// The verification result the fake management endpoint serves. /// Output format. /// Expected exit code. /// Text the report must contain. /// A task that represents the asynchronous operation. [Theory] [InlineData(IntactBody, "table", 0, "Status : Intact")] [InlineData(IntactBody, "json", 0, "\"status\":\"Intact\"")] [InlineData(BrokenBody, "table", 3, "eventId : 11111111-2222-3333-4444-555555555555")] [InlineData(BrokenBody, "json", 3, "\"chainSeq\":7")] public async Task VerifyChain_SendsVerifyAuditChain_AndMapsVerdictToExitCode( string body, string format, int expectedExit, string expectedText) { var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); try { var port = ((IPEndPoint)listener.LocalEndpoint).Port; string? requestBody = null; var server = Task.Run(async () => { using var client = await listener.AcceptTcpClientAsync(); await using var stream = client.GetStream(); using var reader = new StreamReader(stream, Encoding.UTF8, leaveOpen: true); var length = 0; while (await reader.ReadLineAsync() is { Length: > 0 } line) if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) length = int.Parse(line[15..].Trim()); var buffer = new char[length]; var read = 0; while (read < length) { var count = await reader.ReadAsync(buffer.AsMemory(read)); if (count == 0) throw new EndOfStreamException(); read += count; } requestBody = new string(buffer); var bytes = Encoding.UTF8.GetBytes(body); var headers = Encoding.ASCII.GetBytes( $"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {bytes.Length}\r\nConnection: close\r\n\r\n"); await stream.WriteAsync(headers); await stream.WriteAsync(bytes); }); var root = AuditCommandTestHarness.BuildRoot(); var (exit, output, _) = AuditCommandTestHarness.Invoke( root, "audit", "verify-chain", "--month", "2026-05", "--url", $"http://127.0.0.1:{port}", "--username", "u", "--password", "p", "--format", format); await server.WaitAsync(TimeSpan.FromSeconds(5)); Assert.Equal(expectedExit, exit); Assert.Contains(expectedText, output); using var payload = JsonDocument.Parse(requestBody!); Assert.Equal("VerifyAuditChain", payload.RootElement.GetProperty("command").GetString()); var month = payload.RootElement.GetProperty("payload").EnumerateObject() .First(p => string.Equals(p.Name, "month", StringComparison.OrdinalIgnoreCase)).Value.GetString(); Assert.Equal("2026-05", month); } finally { listener.Stop(); } } /// Verifies that a malformed month value exits non-zero without contacting the server. [Fact] public void VerifyChain_MalformedMonth_ExitsNonZero() { var root = AuditCommandTestHarness.BuildRoot(); var (exit, _, _) = AuditCommandTestHarness.Invoke( root, "audit", "verify-chain", "--month", "2026-13"); Assert.NotEqual(0, exit); } /// Verifies that omitting the month option produces the required-option error. [Fact] public void VerifyChain_MissingMonth_ProducesRequiredFlagError() { var root = AuditCommandTestHarness.BuildRoot(); var (exit, _, err) = AuditCommandTestHarness.Invoke(root, "audit", "verify-chain"); Assert.NotEqual(0, exit); Assert.Contains("--month", err); } /// Verifies that month validation accepts only values in yyyy-MM format. /// The month string to validate. /// Whether the month is expected to be valid. [Theory] [InlineData("2026-05", true)] [InlineData("2026-01", true)] [InlineData("2026-12", true)] [InlineData("2026-13", false)] [InlineData("2026-00", false)] [InlineData("2026-5", false)] [InlineData("not-a-month", false)] [InlineData("", false)] public void IsValidMonth_ValidatesYyyyMm(string month, bool expected) { Assert.Equal(expected, AuditVerifyChainHelpers.IsValidMonth(month)); } }