fix(management): audit-config query returned {} for every filter — project the repo ValueTuple into a named result record
Deferred-work register row 46. HandleQueryAuditLog returned ICentralUiRepository's
(IReadOnlyList<AuditLogEntry> Entries, int TotalCount) ValueTuple straight to
ManagementActor.SerializeResult. System.Text.Json writes properties only, and a
ValueTuple exposes Item1/Item2 as public FIELDS, so the whole payload serialized to
the literal "{}" — the CLI faithfully printed an empty object no matter how many
AuditLogEntries rows matched, with no error anywhere in the stack.
Fixed at the handler (the layer that leaks the tuple), not the CLI: new Commons record
AuditLogQueryResult(Entries, TotalCount), mirroring the SecuredWriteListResult shape
already used by the other paged management query.
Also guards the whole class of bug: SerializeResult now throws on any ITuple result,
so the next handler that returns a tuple fails loudly instead of silently emitting {}.
Tests (fail-before verified — the query test asserted exactly "{}"):
- QueryAuditLogCommand_ReturnsEntriesAndTotalCount
- SerializeResult_TupleResult_ThrowsInsteadOfEmittingEmptyObject
- SerializeResult_AuditLogQueryResult_EmitsEntriesAndTotalCount
Docs: Component-CLI.md + CLI README document the response shape and why it is a record.
This commit is contained in:
@@ -238,6 +238,85 @@ public class ManagementActorTests : TestKit, IDisposable
|
||||
Assert.Equal(envelope.CorrelationId, response.CorrelationId);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 2b. Configuration-change audit query -- result must not serialize to "{}"
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Deferred-work register row 46: <c>audit-config query</c> returned the literal
|
||||
/// <c>{}</c> for every filter combination against the rig even though the
|
||||
/// <c>AuditLogEntries</c> rows were present. The handler returned the repository's
|
||||
/// <c>(Entries, TotalCount)</c> ValueTuple straight to <c>SerializeResult</c>, and
|
||||
/// System.Text.Json writes properties only — a tuple's <c>Item1</c>/<c>Item2</c> are
|
||||
/// fields, so the whole payload vanished with no error anywhere. Pins that the rows
|
||||
/// and the total actually reach the wire.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QueryAuditLogCommand_ReturnsEntriesAndTotalCount()
|
||||
{
|
||||
var uiRepo = Substitute.For<ICentralUiRepository>();
|
||||
var entries = new List<Commons.Entities.Audit.AuditLogEntry>
|
||||
{
|
||||
new("alice", "Created", "Template", "7", "PumpTemplate")
|
||||
{
|
||||
Id = 1,
|
||||
Timestamp = new DateTimeOffset(2026, 8, 15, 12, 0, 0, TimeSpan.Zero)
|
||||
}
|
||||
};
|
||||
uiRepo.GetAuditLogEntriesAsync(
|
||||
Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<string?>(),
|
||||
Arg.Any<DateTimeOffset?>(), Arg.Any<DateTimeOffset?>(),
|
||||
Arg.Any<string?>(), Arg.Any<string?>(), Arg.Any<Guid?>(),
|
||||
Arg.Any<int>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(((IReadOnlyList<Commons.Entities.Audit.AuditLogEntry>)entries, 137));
|
||||
_services.AddScoped(_ => uiRepo);
|
||||
|
||||
var actor = CreateActor();
|
||||
var envelope = Envelope(new QueryAuditLogCommand(PageSize: 25), "Administrator");
|
||||
|
||||
actor.Tell(envelope);
|
||||
|
||||
var response = ExpectMsg<ManagementSuccess>(TimeSpan.FromSeconds(5));
|
||||
Assert.NotEqual("{}", response.JsonData);
|
||||
Assert.Contains("PumpTemplate", response.JsonData);
|
||||
Assert.Contains("\"entries\"", response.JsonData);
|
||||
Assert.Contains("\"totalCount\":137", response.JsonData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Guard for the whole class of bug behind row 46: any handler that returns a tuple
|
||||
/// silently serializes to <c>{}</c>. <see cref="ManagementActor.SerializeResult"/>
|
||||
/// now rejects tuples outright so the next occurrence is a loud failure rather than
|
||||
/// an empty response.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SerializeResult_TupleResult_ThrowsInsteadOfEmittingEmptyObject()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => ManagementActor.SerializeResult((new[] { "a" }, 1)));
|
||||
|
||||
Assert.Contains("tuple", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pins the positive half of the guard: the named result record the handler now
|
||||
/// returns serializes to a populated, camelCased object.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SerializeResult_AuditLogQueryResult_EmitsEntriesAndTotalCount()
|
||||
{
|
||||
var json = ManagementActor.SerializeResult(new AuditLogQueryResult(
|
||||
new List<Commons.Entities.Audit.AuditLogEntry>
|
||||
{
|
||||
new("bob", "Deleted", "Instance", "3", "Pump1")
|
||||
},
|
||||
42));
|
||||
|
||||
Assert.Contains("\"entries\"", json);
|
||||
Assert.Contains("\"totalCount\":42", json);
|
||||
Assert.Contains("Pump1", json);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 3. Template list test -- verify data flows through
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user