fix(auditlog): capture request/response payloads on outbound API audit rows
The outbound ApiCall emitter hard-coded RequestSummary/ResponseSummary to null, so audited API calls carried no inputs/outputs — contrary to the Audit Log payload-capture spec. Thread the call arguments into the sync ApiCall emitter and the cached immediate-completion path (CachedSubmit / ApiCallCached / CachedResolve), and stamp the response body from ExternalCallResult.ResponseJson. The writer's payload filter still applies the size cap + redaction downstream. The S&F retry-loop cached rows are unchanged — request data is not threaded through the store-and-forward buffer (same boundary as SourceScript).
This commit is contained in:
@@ -94,6 +94,42 @@ public class ExternalSystemCachedCallEmissionTests
|
||||
Assert.Null(packet.Operational.TerminalAtUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CachedCall_ImmediateCompletion_CapturesRequestArgs_AndResponseBody()
|
||||
{
|
||||
var client = new Mock<IExternalSystemClient>();
|
||||
client
|
||||
.Setup(c => c.CachedCallAsync(
|
||||
"ERP", "GetOrder",
|
||||
It.IsAny<IReadOnlyDictionary<string, object?>?>(),
|
||||
InstanceName,
|
||||
It.IsAny<CancellationToken>(),
|
||||
It.IsAny<TrackedOperationId?>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, "{\"ok\":true}", null, WasBuffered: false));
|
||||
var forwarder = new CapturingForwarder();
|
||||
|
||||
var helper = CreateHelper(client.Object, forwarder);
|
||||
var args = new Dictionary<string, object?> { ["orderId"] = 42 };
|
||||
await helper.CachedCall("ERP", "GetOrder", args);
|
||||
|
||||
// Immediate completion (WasBuffered=false) emits Submit, Attempted, Resolve.
|
||||
Assert.Equal(3, forwarder.Telemetry.Count);
|
||||
var submit = forwarder.Telemetry.Single(t => t.Audit.Kind == AuditKind.CachedSubmit);
|
||||
var attempted = forwarder.Telemetry.Single(t => t.Audit.Kind == AuditKind.ApiCallCached);
|
||||
var resolve = forwarder.Telemetry.Single(t => t.Audit.Kind == AuditKind.CachedResolve);
|
||||
|
||||
// Every row carries the request args; the two post-call rows also carry
|
||||
// the response body (Submit precedes the call, so it has no response).
|
||||
Assert.Equal("{\"orderId\":42}", submit.Audit.RequestSummary);
|
||||
Assert.Null(submit.Audit.ResponseSummary);
|
||||
|
||||
Assert.Equal("{\"orderId\":42}", attempted.Audit.RequestSummary);
|
||||
Assert.Equal("{\"ok\":true}", attempted.Audit.ResponseSummary);
|
||||
|
||||
Assert.Equal("{\"orderId\":42}", resolve.Audit.RequestSummary);
|
||||
Assert.Equal("{\"ok\":true}", resolve.Audit.ResponseSummary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CachedCall_ReturnsTrackedOperationId()
|
||||
{
|
||||
|
||||
@@ -81,6 +81,29 @@ public class ExternalSystemCallAuditEmissionTests
|
||||
Assert.Equal(DateTimeKind.Utc, evt.OccurredAtUtc.Kind);
|
||||
Assert.NotEqual(Guid.Empty, evt.EventId);
|
||||
Assert.False(evt.PayloadTruncated);
|
||||
// No call arguments → null request summary; the response body is captured.
|
||||
Assert.Null(evt.RequestSummary);
|
||||
Assert.Equal("{}", evt.ResponseSummary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Call_CapturesRequestArgs_AndResponseBody_OnTheAuditRow()
|
||||
{
|
||||
var client = new Mock<IExternalSystemClient>();
|
||||
client
|
||||
.Setup(c => c.CallAsync("Weather", "GetCurrent", It.IsAny<IReadOnlyDictionary<string, object?>?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ExternalCallResult(true, "{\"tempC\":11.4}", null));
|
||||
var writer = new CapturingAuditWriter();
|
||||
|
||||
var helper = CreateHelper(client.Object, writer);
|
||||
var args = new Dictionary<string, object?> { ["city"] = "Dublin" };
|
||||
await helper.Call("Weather", "GetCurrent", args);
|
||||
|
||||
var evt = Assert.Single(writer.Events);
|
||||
// RequestSummary is the serialized argument dictionary; ResponseSummary
|
||||
// is the verbatim response body. (Cap + redaction are the writer's job.)
|
||||
Assert.Equal("{\"city\":\"Dublin\"}", evt.RequestSummary);
|
||||
Assert.Equal("{\"tempC\":11.4}", evt.ResponseSummary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user