using System.Text.Json; using ScadaLink.CLI.Commands; namespace ScadaLink.CLI.Tests.Commands; /// /// Tests for the table output formatter of the scadalink audit query /// subcommand (Audit Log #23 M8-T6): header rendering, long-field truncation, the /// empty-result-set case, and null-actor handling. /// public class AuditTableFormatterTests { private static IReadOnlyList Events(string json) { using var doc = JsonDocument.Parse(json); return doc.RootElement.EnumerateArray() .Select(e => e.Clone()) .ToList(); } [Fact] public void Table_RendersHeaderRow_WithExpectedColumns() { var formatter = new TableAuditFormatter(); var output = new StringWriter(); formatter.WritePage(Events("[]"), output); var firstLine = output.ToString() .Split('\n', StringSplitOptions.RemoveEmptyEntries)[0]; foreach (var col in new[] { "OccurredAtUtc", "Channel", "Kind", "Status", "Target", "Actor", "DurationMs", "HttpStatus", }) { Assert.Contains(col, firstLine); } } [Fact] public void Table_TruncatesLongTarget_WithEllipsis() { var formatter = new TableAuditFormatter(); var output = new StringWriter(); var longTarget = new string('x', 200); formatter.WritePage( Events($"[{{\"occurredAtUtc\":\"2026-05-20T12:00:00Z\",\"channel\":\"OutboundApi\"," + $"\"kind\":\"SyncCall\",\"status\":\"Delivered\",\"target\":\"{longTarget}\"," + $"\"actor\":\"multi-role\"}}]"), output); var text = output.ToString(); Assert.Contains("…", text); // The full untruncated target must not appear verbatim. Assert.DoesNotContain(longTarget, text); } [Fact] public void Table_EmptyResultSet_RendersHeaderOnly_OrNoRowsMessage() { var formatter = new TableAuditFormatter(); var output = new StringWriter(); formatter.WritePage(Events("[]"), output); var lines = output.ToString() .Split('\n', StringSplitOptions.RemoveEmptyEntries); // Header only — no data rows. (A header line is always emitted so the // column shape is visible even with zero results.) Assert.Single(lines); Assert.Contains("OccurredAtUtc", lines[0]); } [Fact] public void Table_NullActor_RendersBlank() { var formatter = new TableAuditFormatter(); var output = new StringWriter(); formatter.WritePage( Events("[{\"occurredAtUtc\":\"2026-05-20T12:00:00Z\",\"channel\":\"InboundApi\"," + "\"kind\":\"ApiCall\",\"status\":\"Delivered\",\"target\":\"key-1\"," + "\"actor\":null}]"), output); var lines = output.ToString() .Split('\n', StringSplitOptions.RemoveEmptyEntries); Assert.Equal(2, lines.Length); // The data row must not contain the literal "null" for the actor column. Assert.DoesNotContain("null", lines[1]); Assert.Contains("InboundApi", lines[1]); } [Fact] public void Table_HeaderEmittedOncePerPage_DataRowsAligned() { var formatter = new TableAuditFormatter(); var output = new StringWriter(); formatter.WritePage( Events("[{\"occurredAtUtc\":\"2026-05-20T12:00:00Z\",\"channel\":\"OutboundApi\"," + "\"kind\":\"SyncCall\",\"status\":\"Delivered\",\"target\":\"weather-api\"," + "\"actor\":\"multi-role\",\"durationMs\":42,\"httpStatus\":200}," + "{\"occurredAtUtc\":\"2026-05-20T12:01:00Z\",\"channel\":\"Notification\"," + "\"kind\":\"Send\",\"status\":\"Failed\",\"target\":\"ops-list\"," + "\"actor\":\"scheduler\",\"durationMs\":7}]"), output); var lines = output.ToString() .Split('\n', StringSplitOptions.RemoveEmptyEntries); Assert.Equal(3, lines.Length); Assert.Contains("weather-api", lines[1]); Assert.Contains("ops-list", lines[2]); } }