118 lines
4.2 KiB
C#
118 lines
4.2 KiB
C#
using System.Text.Json;
|
|
using ScadaLink.CLI.Commands;
|
|
|
|
namespace ScadaLink.CLI.Tests.Commands;
|
|
|
|
/// <summary>
|
|
/// Tests for the <c>table</c> output formatter of the <c>scadalink audit query</c>
|
|
/// subcommand (Audit Log #23 M8-T6): header rendering, long-field truncation, the
|
|
/// empty-result-set case, and null-actor handling.
|
|
/// </summary>
|
|
public class AuditTableFormatterTests
|
|
{
|
|
private static IReadOnlyList<JsonElement> 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]);
|
|
}
|
|
}
|