feat(infra): JsonSanitizer + AuditOptions binding (UDT-010 B3)
Adds the metadata sanitization layer per #REQ-AUD-5:
SIGCM2.Infrastructure/Audit/JsonSanitizer.cs (static class):
- Sanitize(object?, IReadOnlyCollection<string>) -> string?
- Serializes via System.Text.Json + JsonNode recursive traversal.
- Strips blacklisted keys at every nesting level (objects + arrays).
- Case-insensitive match (ToLowerInvariant on both sides).
- Null input -> null output (never throws).
- Output is always valid JSON (ISJSON=1 compatible — satisfies AuditEvent CHECK).
SIGCM2.Application/Audit/AuditOptions.cs:
- Documented the IConfiguration array-binding quirk: config is ADDITIVE
(append at higher indices), not REPLACE. Intentional for security — defaults
like 'password'/'token'/'cvv' must not be silently dropped.
SIGCM2.Infrastructure/DependencyInjection.cs:
- services.Configure<AuditOptions>(configuration.GetSection(AuditOptions.SectionName))
wired in AddInfrastructure().
Tests (Strict TDD, RED -> GREEN):
- JsonSanitizerTests (10): null/empty-blacklist/flat/nested/arrays/case-insensitive/
primitives/round-trip-valid-json/string-as-value/default-keys-effective.
- AuditOptionsBindingTests (2): defaults when section absent + additive override.
One test needed adjustment during GREEN: 'AlreadySerializedJsonString' originally
asserted against an encoding-specific literal; rewrote to use JsonDocument
round-trip (validates behavior without coupling to encoder quirks).
Suite: 348/348 Application.Tests + 130/130 Api.Tests = 478/478 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-5, design#D-5, tasks#B3}
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace SIGCM2.Application.Tests.Infrastructure.Audit;
|
||||
|
||||
/// UDT-010 Batch 3 — AuditOptions binding smoke tests.
|
||||
/// Validates that AddInfrastructure binds AuditOptions from the "Audit" config section
|
||||
/// and falls back to the POCO defaults when the section is absent.
|
||||
public sealed class AuditOptionsBindingTests
|
||||
{
|
||||
private static IServiceProvider BuildProvider(IEnumerable<KeyValuePair<string, string?>>? overrides = null)
|
||||
{
|
||||
// Minimum required config for AddInfrastructure to succeed.
|
||||
var inMemory = new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:SqlServer"] = "Server=nowhere;Database=x;Integrated Security=true;",
|
||||
["Jwt:Issuer"] = "test",
|
||||
["Jwt:Audience"] = "test",
|
||||
["Jwt:AccessTokenMinutes"] = "60",
|
||||
["Jwt:RefreshTokenDays"] = "7",
|
||||
["Jwt:PrivateKeyPath"] = "unused-in-this-test.pem",
|
||||
["Jwt:PublicKeyPath"] = "unused-in-this-test.pem",
|
||||
};
|
||||
|
||||
if (overrides is not null)
|
||||
foreach (var kv in overrides)
|
||||
inMemory[kv.Key] = kv.Value;
|
||||
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(inMemory).Build();
|
||||
var services = new ServiceCollection();
|
||||
services.AddInfrastructure(config);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuditOptions_WithoutConfigSection_UsesPocoDefaults()
|
||||
{
|
||||
using var sp = (ServiceProvider)BuildProvider();
|
||||
|
||||
var opts = sp.GetRequiredService<IOptions<AuditOptions>>().Value;
|
||||
|
||||
opts.SanitizedKeys.Should().Contain("password");
|
||||
opts.SanitizedKeys.Should().Contain("refreshToken");
|
||||
opts.SanitizedKeys.Should().Contain("apiKey");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuditOptions_WithConfigSection_AddsToDefaults_PerIConfigurationArrayBinding()
|
||||
{
|
||||
// IConfiguration array binding is ADDITIVE, not REPLACE: config values at indices 0..N
|
||||
// overwrite those indices but defaults beyond N are preserved. This is intended for
|
||||
// AuditOptions — extensibility is additive (append, not replace).
|
||||
using var sp = (ServiceProvider)BuildProvider(new[]
|
||||
{
|
||||
new KeyValuePair<string, string?>("Audit:SanitizedKeys:11", "customSecret"),
|
||||
new KeyValuePair<string, string?>("Audit:SanitizedKeys:12", "internalToken"),
|
||||
});
|
||||
|
||||
var opts = sp.GetRequiredService<IOptions<AuditOptions>>().Value;
|
||||
|
||||
// The 11 defaults remain + the 2 extras appear at the configured indices.
|
||||
opts.SanitizedKeys.Should().Contain("password"); // default survived
|
||||
opts.SanitizedKeys.Should().Contain("customSecret"); // appended via config
|
||||
opts.SanitizedKeys.Should().Contain("internalToken"); // appended via config
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user