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; /// /// Tests for the compact audit-config query table projection (deferred-work /// register row 57). The command passed no tableProjector, so the generic /// renderer saw an OBJECT, took its Property/Value /// branch, and printed the entire entries array as raw JSON inside one cell. /// The projection unwraps the envelope to a row array, drops the per-entry /// afterStateJson snapshot, and keeps totalCount visible as a trailing note. /// public class AuditConfigTableProjectionTests { /// /// The real server shape, serialised from the REAL /// / types rather than hand-written JSON, so renaming one /// of their properties fails this test instead of silently blanking a column. /// 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 }); /// Verifies that projecting entries unwraps the envelope into rows. [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()); } /// Verifies that projecting entries drops the after-state JSON. [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); } /// Verifies that a truncated page appends a note with the total count. [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 _)); } /// Verifies that a result set returned in full emits no note column. [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); } /// Verifies that an empty result stays an empty array. [Fact] public void ProjectEntries_EmptyResult_StaysAnEmptyArray() { var json = JsonSerializer.Serialize( new AuditLogQueryResult(Array.Empty(), 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()); } /// Verifies that a bare array is still projected. [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 _)); } /// Verifies that an unrecognised JSON object is returned verbatim. [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)); } /// Verifies that non-JSON input is returned verbatim. [Fact] public void ProjectEntries_NonJson_ReturnedVerbatim() { const string notJson = "proxy error"; Assert.Equal(notJson, AuditConfigTableProjection.ProjectEntries(notJson)); } /// Verifies that the projection is substantially smaller than the raw payload. [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})."); } }