feat(auditlog): payload filter uses InboundMaxBytes for ApiInbound rows
This commit is contained in:
133
tests/ScadaLink.AuditLog.Tests/Payload/InboundChannelCapTests.cs
Normal file
133
tests/ScadaLink.AuditLog.Tests/Payload/InboundChannelCapTests.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ScadaLink.AuditLog.Configuration;
|
||||
using ScadaLink.AuditLog.Payload;
|
||||
using ScadaLink.Commons.Entities.Audit;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
|
||||
namespace ScadaLink.AuditLog.Tests.Payload;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the docs/plans/2026-05-23-inbound-api-full-response-audit-design.md
|
||||
/// inbound carve-out: ApiInbound rows use InboundMaxBytes (default 1 MiB) for
|
||||
/// RequestSummary / ResponseSummary truncation, NOT DefaultCapBytes /
|
||||
/// ErrorCapBytes. Other channels keep the existing caps.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses a file-local <see cref="StaticMonitor"/> helper mirroring the
|
||||
/// convention in the sibling Payload tests (TruncationTests,
|
||||
/// FilterIntegrationTests, BodyRegexRedactionTests, etc.) — the
|
||||
/// <c>TestOptionsMonitor<T></c> helper referenced by the plan is a
|
||||
/// private nested class inside <c>AuditLogOptionsBindingTests</c> and thus
|
||||
/// not reachable from this file.
|
||||
/// </remarks>
|
||||
public class InboundChannelCapTests
|
||||
{
|
||||
private static AuditEvent MakeInbound(
|
||||
AuditStatus status,
|
||||
string? request = null,
|
||||
string? response = null) =>
|
||||
new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAtUtc = DateTime.UtcNow,
|
||||
Channel = AuditChannel.ApiInbound,
|
||||
Kind = AuditKind.InboundRequest,
|
||||
Status = status,
|
||||
RequestSummary = request,
|
||||
ResponseSummary = response,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void ApiInbound_Delivered_RequestBody_BelowInboundMaxBytes_NotTruncated()
|
||||
{
|
||||
// Body well above the legacy 8 KiB default cap but under the 1 MiB
|
||||
// inbound ceiling — must NOT truncate.
|
||||
var body = new string('a', 100_000);
|
||||
var opts = new AuditLogOptions(); // defaults
|
||||
var filter = new DefaultAuditPayloadFilter(
|
||||
new StaticMonitor(opts),
|
||||
NullLogger<DefaultAuditPayloadFilter>.Instance);
|
||||
|
||||
var result = filter.Apply(MakeInbound(AuditStatus.Delivered, request: body));
|
||||
|
||||
Assert.False(result.PayloadTruncated);
|
||||
Assert.Equal(100_000, Encoding.UTF8.GetByteCount(result.RequestSummary!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApiInbound_Delivered_ResponseBody_BelowInboundMaxBytes_NotTruncated()
|
||||
{
|
||||
var body = new string('a', 100_000);
|
||||
var opts = new AuditLogOptions();
|
||||
var filter = new DefaultAuditPayloadFilter(
|
||||
new StaticMonitor(opts),
|
||||
NullLogger<DefaultAuditPayloadFilter>.Instance);
|
||||
|
||||
var result = filter.Apply(MakeInbound(AuditStatus.Delivered, response: body));
|
||||
|
||||
Assert.False(result.PayloadTruncated);
|
||||
Assert.Equal(100_000, Encoding.UTF8.GetByteCount(result.ResponseSummary!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApiInbound_Failed_BodyAboveInboundMaxBytes_TruncatedToInboundMaxBytes()
|
||||
{
|
||||
// Even on error rows, the inbound cap is InboundMaxBytes (NOT ErrorCapBytes).
|
||||
var opts = new AuditLogOptions { InboundMaxBytes = 16_384 };
|
||||
var oversized = new string('z', 50_000);
|
||||
var filter = new DefaultAuditPayloadFilter(
|
||||
new StaticMonitor(opts),
|
||||
NullLogger<DefaultAuditPayloadFilter>.Instance);
|
||||
|
||||
var result = filter.Apply(MakeInbound(AuditStatus.Failed, response: oversized));
|
||||
|
||||
Assert.True(result.PayloadTruncated);
|
||||
Assert.True(Encoding.UTF8.GetByteCount(result.ResponseSummary!) <= 16_384);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApiOutbound_StillUsesDefaultCap_NotInboundMaxBytes()
|
||||
{
|
||||
// Regression guard: lifting the inbound cap MUST NOT change other
|
||||
// channels. An ApiOutbound 100 KB body still hits the 8 KiB cap.
|
||||
var opts = new AuditLogOptions();
|
||||
var body = new string('a', 100_000);
|
||||
var filter = new DefaultAuditPayloadFilter(
|
||||
new StaticMonitor(opts),
|
||||
NullLogger<DefaultAuditPayloadFilter>.Instance);
|
||||
|
||||
var evt = new AuditEvent
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAtUtc = DateTime.UtcNow,
|
||||
Channel = AuditChannel.ApiOutbound,
|
||||
Kind = AuditKind.ApiCall,
|
||||
Status = AuditStatus.Delivered,
|
||||
RequestSummary = body,
|
||||
};
|
||||
var result = filter.Apply(evt);
|
||||
|
||||
Assert.True(result.PayloadTruncated);
|
||||
Assert.True(Encoding.UTF8.GetByteCount(result.RequestSummary!) <= opts.DefaultCapBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IOptionsMonitor test double — returns the same snapshot on every read,
|
||||
/// no change-token plumbing required for these tests. Mirrors the helper
|
||||
/// used in <c>TruncationTests</c>, <c>FilterIntegrationTests</c>, etc.
|
||||
/// </summary>
|
||||
private sealed class StaticMonitor : IOptionsMonitor<AuditLogOptions>
|
||||
{
|
||||
private readonly AuditLogOptions _value;
|
||||
|
||||
public StaticMonitor(AuditLogOptions value) => _value = value;
|
||||
|
||||
public AuditLogOptions CurrentValue => _value;
|
||||
|
||||
public AuditLogOptions Get(string? name) => _value;
|
||||
|
||||
public IDisposable? OnChange(Action<AuditLogOptions, string?> listener) => null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user