513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
192 lines
8.8 KiB
C#
192 lines
8.8 KiB
C#
using System.Text.Json;
|
|
using ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for the compact <c>audit-config query</c> table projection (deferred-work
|
|
/// register row 57). The command passed no <c>tableProjector</c>, so the generic
|
|
/// renderer saw an <see cref="AuditLogQueryResult"/> OBJECT, took its Property/Value
|
|
/// branch, and printed the entire <c>entries</c> array as raw JSON inside one cell.
|
|
/// The projection unwraps the envelope to a row array, drops the per-entry
|
|
/// <c>afterStateJson</c> snapshot, and keeps <c>totalCount</c> visible as a trailing note.
|
|
/// </summary>
|
|
public class AuditConfigTableProjectionTests
|
|
{
|
|
/// <summary>
|
|
/// The real server shape, serialised from the REAL <see cref="AuditLogQueryResult"/>
|
|
/// / <see cref="AuditLogEntry"/> types rather than hand-written JSON, so renaming one
|
|
/// of their properties fails this test instead of silently blanking a column.
|
|
/// </summary>
|
|
private static string RealPayload(int totalCount) =>
|
|
JsonSerializer.Serialize(
|
|
new AuditLogQueryResult(
|
|
new[]
|
|
{
|
|
new AuditLogEntry("multi-role", "Created", "Template", "7", "PumpTemplate")
|
|
{
|
|
Id = 412,
|
|
AfterStateJson = "{\"id\":7,\"name\":\"PumpTemplate\",\"attributes\":[]}",
|
|
Timestamp = new DateTimeOffset(2026, 8, 15, 12, 0, 0, TimeSpan.Zero),
|
|
},
|
|
new AuditLogEntry("designer", "Deleted", "ExternalSystem", "3", "MES")
|
|
{
|
|
Id = 413,
|
|
AfterStateJson = null,
|
|
Timestamp = new DateTimeOffset(2026, 8, 15, 12, 5, 0, TimeSpan.Zero),
|
|
},
|
|
},
|
|
totalCount),
|
|
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
|
|
|
/// <summary>Verifies that projecting entries unwraps the envelope into rows.</summary>
|
|
[Fact]
|
|
public void ProjectEntries_UnwrapsTheEnvelopeIntoRows()
|
|
{
|
|
var compact = AuditConfigTableProjection.ProjectEntries(RealPayload(totalCount: 2));
|
|
|
|
using var doc = JsonDocument.Parse(compact);
|
|
var root = doc.RootElement;
|
|
|
|
// An ARRAY is the whole point — the object envelope is what made the renderer
|
|
// print one Property/Value row with the entries blob in a single cell.
|
|
Assert.Equal(JsonValueKind.Array, root.ValueKind);
|
|
Assert.Equal(2, root.GetArrayLength());
|
|
|
|
var first = root[0];
|
|
Assert.Equal(412, first.GetProperty("id").GetInt32());
|
|
Assert.Equal("multi-role", first.GetProperty("user").GetString());
|
|
Assert.Equal("Created", first.GetProperty("action").GetString());
|
|
Assert.Equal("Template", first.GetProperty("entityType").GetString());
|
|
Assert.Equal("PumpTemplate", first.GetProperty("entityName").GetString());
|
|
Assert.Equal("7", first.GetProperty("entityId").GetString());
|
|
Assert.Contains("2026-08-15", first.GetProperty("timestamp").GetString());
|
|
|
|
var second = root[1];
|
|
Assert.Equal("Deleted", second.GetProperty("action").GetString());
|
|
Assert.Equal("MES", second.GetProperty("entityName").GetString());
|
|
}
|
|
|
|
/// <summary>Verifies that projecting entries drops the after-state JSON.</summary>
|
|
[Fact]
|
|
public void ProjectEntries_DropsAfterStateJson()
|
|
{
|
|
var compact = AuditConfigTableProjection.ProjectEntries(RealPayload(totalCount: 2));
|
|
|
|
using var doc = JsonDocument.Parse(compact);
|
|
// The fat field must NOT survive — one entity snapshot per row fills the terminal,
|
|
// which is the audit analogue of template's child arrays.
|
|
Assert.False(doc.RootElement[0].TryGetProperty("afterStateJson", out _));
|
|
Assert.DoesNotContain("afterStateJson", compact, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
/// <summary>Verifies that a truncated page appends a note with the total count.</summary>
|
|
[Fact]
|
|
public void ProjectEntries_TruncatedPage_AppendsTotalCountNote()
|
|
{
|
|
var compact = AuditConfigTableProjection.ProjectEntries(RealPayload(totalCount: 137));
|
|
|
|
using var doc = JsonDocument.Parse(compact);
|
|
var root = doc.RootElement;
|
|
|
|
// Two entries plus the note row.
|
|
Assert.Equal(3, root.GetArrayLength());
|
|
var note = root[2].GetProperty("note").GetString();
|
|
Assert.Contains("2 of 137", note);
|
|
// The note must not masquerade as an entry.
|
|
Assert.False(root[2].TryGetProperty("id", out _));
|
|
}
|
|
|
|
/// <summary>Verifies that a result set returned in full emits no note column.</summary>
|
|
[Fact]
|
|
public void ProjectEntries_WholeResultSet_EmitsNoNoteColumn()
|
|
{
|
|
// When the page IS the result set the count adds nothing the operator cannot
|
|
// already see, so the column must not appear and widen every small query.
|
|
var compact = AuditConfigTableProjection.ProjectEntries(RealPayload(totalCount: 2));
|
|
|
|
using var doc = JsonDocument.Parse(compact);
|
|
Assert.Equal(2, doc.RootElement.GetArrayLength());
|
|
Assert.DoesNotContain("note", compact, StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>Verifies that an empty result stays an empty array.</summary>
|
|
[Fact]
|
|
public void ProjectEntries_EmptyResult_StaysAnEmptyArray()
|
|
{
|
|
var json = JsonSerializer.Serialize(
|
|
new AuditLogQueryResult(Array.Empty<AuditLogEntry>(), 0),
|
|
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
|
|
|
var compact = AuditConfigTableProjection.ProjectEntries(json);
|
|
|
|
using var doc = JsonDocument.Parse(compact);
|
|
// The renderer prints "(no results)" for an empty array; an empty OBJECT would
|
|
// have printed an empty Property/Value table instead.
|
|
Assert.Equal(JsonValueKind.Array, doc.RootElement.ValueKind);
|
|
Assert.Equal(0, doc.RootElement.GetArrayLength());
|
|
}
|
|
|
|
/// <summary>Verifies that a bare array is still projected.</summary>
|
|
[Fact]
|
|
public void ProjectEntries_BareArray_IsStillProjected()
|
|
{
|
|
// Tolerated so the projection does not regress if the payload is ever unwrapped
|
|
// upstream, or when the deprecated `audit-log` alias hits an older server.
|
|
const string bare = """
|
|
[ { "id": 1, "user": "u", "action": "Updated", "entityType": "Site",
|
|
"entityName": "SiteA", "entityId": "2", "timestamp": "2026-08-15T12:00:00+00:00",
|
|
"afterStateJson": "{\"big\":true}" } ]
|
|
""";
|
|
|
|
var compact = AuditConfigTableProjection.ProjectEntries(bare);
|
|
|
|
using var doc = JsonDocument.Parse(compact);
|
|
Assert.Equal(1, doc.RootElement.GetArrayLength());
|
|
Assert.Equal("SiteA", doc.RootElement[0].GetProperty("entityName").GetString());
|
|
Assert.False(doc.RootElement[0].TryGetProperty("afterStateJson", out _));
|
|
}
|
|
|
|
/// <summary>Verifies that an unrecognised JSON object is returned verbatim.</summary>
|
|
[Fact]
|
|
public void ProjectEntries_UnrecognisedObject_ReturnedVerbatim()
|
|
{
|
|
// e.g. an error envelope — leave it to the renderer's own fallbacks rather than
|
|
// projecting it into an empty table that hides the message.
|
|
const string other = """{ "error": "boom" }""";
|
|
Assert.Equal(other, AuditConfigTableProjection.ProjectEntries(other));
|
|
}
|
|
|
|
/// <summary>Verifies that non-JSON input is returned verbatim.</summary>
|
|
[Fact]
|
|
public void ProjectEntries_NonJson_ReturnedVerbatim()
|
|
{
|
|
const string notJson = "<html>proxy error</html>";
|
|
Assert.Equal(notJson, AuditConfigTableProjection.ProjectEntries(notJson));
|
|
}
|
|
|
|
/// <summary>Verifies that the projection is substantially smaller than the raw payload.</summary>
|
|
[Fact]
|
|
public void ProjectEntries_IsSubstantiallySmallerThanTheRawPayload()
|
|
{
|
|
// The reported symptom was the whole entries array in one cell. A page of rows
|
|
// carrying real afterStateJson snapshots must collapse hard.
|
|
var entries = Enumerable.Range(0, 50).Select(i =>
|
|
new AuditLogEntry("designer", "Updated", "Template", i.ToString(), $"Template{i}")
|
|
{
|
|
Id = i,
|
|
AfterStateJson = new string('x', 2000),
|
|
Timestamp = DateTimeOffset.UnixEpoch.AddMinutes(i),
|
|
}).ToArray();
|
|
var json = JsonSerializer.Serialize(new AuditLogQueryResult(entries, entries.Length),
|
|
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
|
|
|
var compact = AuditConfigTableProjection.ProjectEntries(json);
|
|
|
|
Assert.True(compact.Length * 4 < json.Length,
|
|
$"Expected compact ({compact.Length}) to be far smaller than raw ({json.Length}).");
|
|
}
|
|
}
|