174 lines
6.2 KiB
C#
174 lines
6.2 KiB
C#
|
|
using FluentAssertions;
|
||
|
|
using SIGCM2.Infrastructure.Audit;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace SIGCM2.Application.Tests.Infrastructure.Audit;
|
||
|
|
|
||
|
|
/// UDT-010 Batch 3 — JsonSanitizer unit tests (Strict TDD).
|
||
|
|
/// Validates #REQ-AUD-5: metadata MUST be stripped of blacklisted keys before persisting.
|
||
|
|
public sealed class JsonSanitizerTests
|
||
|
|
{
|
||
|
|
private static readonly string[] DefaultBlacklist =
|
||
|
|
[
|
||
|
|
"password", "passwordHash", "token", "refreshToken",
|
||
|
|
"accessToken", "cvv", "card", "cardNumber",
|
||
|
|
"secret", "apiKey", "privateKey",
|
||
|
|
];
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_NullInput_ReturnsNull()
|
||
|
|
{
|
||
|
|
var result = JsonSanitizer.Sanitize(null, DefaultBlacklist);
|
||
|
|
result.Should().BeNull();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_EmptyBlacklist_PreservesAllKeys()
|
||
|
|
{
|
||
|
|
var result = JsonSanitizer.Sanitize(new { password = "x", username = "y" }, []);
|
||
|
|
|
||
|
|
result.Should().NotBeNull();
|
||
|
|
result.Should().Contain("\"password\"").And.Contain("\"username\"");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_FlatObject_RemovesBlacklistedKeysAndKeepsOthers()
|
||
|
|
{
|
||
|
|
var result = JsonSanitizer.Sanitize(
|
||
|
|
new { password = "secret1", username = "juan", email = "j@x.com" },
|
||
|
|
DefaultBlacklist);
|
||
|
|
|
||
|
|
result.Should().NotBeNull();
|
||
|
|
result.Should().NotContain("\"password\"");
|
||
|
|
result.Should().NotContain("secret1");
|
||
|
|
result.Should().Contain("\"username\":\"juan\"");
|
||
|
|
result.Should().Contain("\"email\":\"j@x.com\"");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_NestedObject_RemovesBlacklistedKeysAtEveryLevel()
|
||
|
|
{
|
||
|
|
var input = new
|
||
|
|
{
|
||
|
|
user = new { password = "x", email = "y@z.com" },
|
||
|
|
payload = new { nested = new { token = "abc", safe = "keep" } },
|
||
|
|
};
|
||
|
|
|
||
|
|
var result = JsonSanitizer.Sanitize(input, DefaultBlacklist);
|
||
|
|
|
||
|
|
result.Should().NotBeNull();
|
||
|
|
result.Should().NotContain("\"password\"").And.NotContain("\"token\"")
|
||
|
|
.And.NotContain("\"x\"").And.NotContain("abc");
|
||
|
|
result.Should().Contain("\"email\":\"y@z.com\"");
|
||
|
|
result.Should().Contain("\"safe\":\"keep\"");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_ArrayOfObjects_StripsBlacklistedFromEachElement()
|
||
|
|
{
|
||
|
|
var input = new
|
||
|
|
{
|
||
|
|
items = new object[]
|
||
|
|
{
|
||
|
|
new { password = "a", name = "first" },
|
||
|
|
new { token = "b", name = "second" },
|
||
|
|
new { name = "third" },
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
var result = JsonSanitizer.Sanitize(input, DefaultBlacklist);
|
||
|
|
|
||
|
|
result.Should().NotBeNull();
|
||
|
|
result.Should().NotContain("\"password\"").And.NotContain("\"token\"");
|
||
|
|
result.Should().Contain("\"name\":\"first\"");
|
||
|
|
result.Should().Contain("\"name\":\"second\"");
|
||
|
|
result.Should().Contain("\"name\":\"third\"");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_CaseInsensitiveMatching_StripsAllVariants()
|
||
|
|
{
|
||
|
|
var input = new Dictionary<string, object?>
|
||
|
|
{
|
||
|
|
["Password"] = "a",
|
||
|
|
["PASSWORD"] = "b",
|
||
|
|
["pAsSwOrD"] = "c",
|
||
|
|
["username"] = "keep",
|
||
|
|
};
|
||
|
|
|
||
|
|
var result = JsonSanitizer.Sanitize(input, new[] { "password" });
|
||
|
|
|
||
|
|
result.Should().NotBeNull();
|
||
|
|
result.Should().NotContain("\"a\"").And.NotContain("\"b\"").And.NotContain("\"c\"");
|
||
|
|
result.Should().Contain("\"username\":\"keep\"");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_PreservesPrimitives_Numbers_Bools_Null()
|
||
|
|
{
|
||
|
|
var input = new { count = 42, flag = true, missing = (string?)null, password = "secret" };
|
||
|
|
|
||
|
|
var result = JsonSanitizer.Sanitize(input, DefaultBlacklist);
|
||
|
|
|
||
|
|
result.Should().NotBeNull();
|
||
|
|
result.Should().Contain("\"count\":42");
|
||
|
|
result.Should().Contain("\"flag\":true");
|
||
|
|
result.Should().Contain("\"missing\":null");
|
||
|
|
result.Should().NotContain("\"password\"");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_ProducesValidJson()
|
||
|
|
{
|
||
|
|
var input = new { password = "x", user = new { token = "y", name = "z" } };
|
||
|
|
var result = JsonSanitizer.Sanitize(input, DefaultBlacklist);
|
||
|
|
|
||
|
|
result.Should().NotBeNull();
|
||
|
|
// Round-trip parse validates shape
|
||
|
|
var parsed = System.Text.Json.JsonDocument.Parse(result!);
|
||
|
|
parsed.RootElement.TryGetProperty("password", out _).Should().BeFalse();
|
||
|
|
parsed.RootElement.GetProperty("user").TryGetProperty("token", out _).Should().BeFalse();
|
||
|
|
parsed.RootElement.GetProperty("user").GetProperty("name").GetString().Should().Be("z");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_AlreadySerializedJsonString_IsTreatedAsStringNotObject()
|
||
|
|
{
|
||
|
|
// Callers that want to sanitize an already-serialized JSON string must parse+re-sanitize
|
||
|
|
// themselves — the sanitizer treats a string argument as a JSON string primitive.
|
||
|
|
var result = JsonSanitizer.Sanitize("""{"password":"x"}""", DefaultBlacklist);
|
||
|
|
|
||
|
|
result.Should().NotBeNull();
|
||
|
|
|
||
|
|
// Round-trip parse proves the result is a JSON string primitive (not an object).
|
||
|
|
var parsed = System.Text.Json.JsonDocument.Parse(result!);
|
||
|
|
parsed.RootElement.ValueKind.Should().Be(System.Text.Json.JsonValueKind.String);
|
||
|
|
parsed.RootElement.GetString().Should().Be("""{"password":"x"}""");
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Sanitize_AuditOptionsDefaultKeys_AreAllEffective()
|
||
|
|
{
|
||
|
|
// Defensive: AuditOptions.SanitizedKeys must match what tests expect.
|
||
|
|
var opts = new SIGCM2.Application.Audit.AuditOptions();
|
||
|
|
|
||
|
|
var input = new
|
||
|
|
{
|
||
|
|
password = "1", passwordHash = "2", token = "3", refreshToken = "4",
|
||
|
|
accessToken = "5", cvv = "6", card = "7", cardNumber = "8",
|
||
|
|
secret = "9", apiKey = "10", privateKey = "11",
|
||
|
|
keep = "yes",
|
||
|
|
};
|
||
|
|
|
||
|
|
var result = JsonSanitizer.Sanitize(input, opts.SanitizedKeys);
|
||
|
|
|
||
|
|
result.Should().NotBeNull();
|
||
|
|
foreach (var bad in new[] { "password", "passwordHash", "token", "refreshToken",
|
||
|
|
"accessToken", "cvv", "card", "cardNumber", "secret", "apiKey", "privateKey" })
|
||
|
|
{
|
||
|
|
result.Should().NotContain($"\"{bad}\"", because: $"'{bad}' is in default blacklist");
|
||
|
|
}
|
||
|
|
result.Should().Contain("\"keep\":\"yes\"");
|
||
|
|
}
|
||
|
|
}
|