From 25cdf857c95b3520b8446ea06dadb4c044b243b7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 20 May 2026 16:59:10 -0400 Subject: [PATCH] feat(auditlog): IAuditPayloadFilter contract (#23 M5) --- .../Payload/IAuditPayloadFilter.cs | 30 ++++++++++ .../Payload/PayloadFilterContractTests.cs | 59 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 src/ScadaLink.AuditLog/Payload/IAuditPayloadFilter.cs create mode 100644 tests/ScadaLink.AuditLog.Tests/Payload/PayloadFilterContractTests.cs diff --git a/src/ScadaLink.AuditLog/Payload/IAuditPayloadFilter.cs b/src/ScadaLink.AuditLog/Payload/IAuditPayloadFilter.cs new file mode 100644 index 0000000..45b7ee2 --- /dev/null +++ b/src/ScadaLink.AuditLog/Payload/IAuditPayloadFilter.cs @@ -0,0 +1,30 @@ +using ScadaLink.Commons.Entities.Audit; + +namespace ScadaLink.AuditLog.Payload; + +/// +/// Filters an between construction and persistence — +/// truncates oversized payload fields, applies header/body/SQL-parameter +/// redaction, sets . +/// +/// +/// +/// Pure function: returns a filtered COPY of the input via with +/// expressions; never throws (over-redacts on internal failure and increments +/// the AuditRedactionFailure health metric). +/// +/// +/// Wired in M5 between event construction and the writer chain +/// (FallbackAuditWriter.WriteAsync, CentralAuditWriter.WriteAsync, +/// and the AuditLogIngestActor handlers). +/// +/// +public interface IAuditPayloadFilter +{ + /// + /// Apply the configured truncation + redaction policy to + /// and return a filtered copy. MUST NOT throw — on internal failure, over-redact + /// and surface the failure via the audit-redaction-failure health metric. + /// + AuditEvent Apply(AuditEvent rawEvent); +} diff --git a/tests/ScadaLink.AuditLog.Tests/Payload/PayloadFilterContractTests.cs b/tests/ScadaLink.AuditLog.Tests/Payload/PayloadFilterContractTests.cs new file mode 100644 index 0000000..6848b29 --- /dev/null +++ b/tests/ScadaLink.AuditLog.Tests/Payload/PayloadFilterContractTests.cs @@ -0,0 +1,59 @@ +using System.Linq; +using System.Reflection; +using ScadaLink.AuditLog.Payload; +using ScadaLink.Commons.Entities.Audit; + +namespace ScadaLink.AuditLog.Tests.Payload; + +/// +/// Bundle A (M5-T1) contract test for . The +/// interface is the seam between event construction and writer persistence; +/// later bundles register the production implementation as a singleton and +/// invoke it from the site/central writer paths. We pin the surface area here +/// via reflection so accidental signature drift breaks the build before the +/// downstream wiring goes red. +/// +public class PayloadFilterContractTests +{ + [Fact] + public void Interface_Exists_InPayloadNamespace() + { + var type = typeof(IAuditPayloadFilter); + + Assert.True(type.IsInterface, "IAuditPayloadFilter must be an interface"); + Assert.Equal("ScadaLink.AuditLog.Payload", type.Namespace); + } + + [Fact] + public void Apply_Method_HasDocumentedSignature() + { + var type = typeof(IAuditPayloadFilter); + + var method = type.GetMethod( + "Apply", + BindingFlags.Instance | BindingFlags.Public, + binder: null, + types: new[] { typeof(AuditEvent) }, + modifiers: null); + + Assert.NotNull(method); + Assert.Equal(typeof(AuditEvent), method!.ReturnType); + + var parameters = method.GetParameters(); + Assert.Single(parameters); + Assert.Equal("rawEvent", parameters[0].Name); + Assert.Equal(typeof(AuditEvent), parameters[0].ParameterType); + } + + [Fact] + public void Interface_DeclaresExactlyOneMethod() + { + var type = typeof(IAuditPayloadFilter); + var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where(m => !m.IsSpecialName) + .ToArray(); + + Assert.Single(methods); + Assert.Equal("Apply", methods[0].Name); + } +}