using FluentAssertions; using PiiRedaction.Core.Models; using PiiRedaction.Core.Redaction; namespace PiiRedaction.Core.Tests.Redaction; [TestFixture] public sealed class PlaceholderPiiRedactorTests { private readonly PlaceholderPiiRedactor _redactor = new(); [Test] public void Redact_NoEntities_ReturnsOriginalTextAndEmptyMap() { const string text = "No PII here."; var result = _redactor.Redact(text, []); result.SanitizedText.Should().Be(text); result.PlaceholderMap.Should().BeEmpty(); } [Test] public void Redact_SingleEntity_ReplacesWithTypedPlaceholder() { const string text = "Email ravi@test.com end."; var entities = new[] { Entity(PiiEntityType.Email, "ravi@test.com", 6) }; var result = _redactor.Redact(text, entities); result.SanitizedText.Should().Be("Email end."); result.PlaceholderMap[""].Should().Be("ravi@test.com"); } [Test] public void Redact_DuplicateSameTypeAndValue_ReusesPlaceholder() { const string text = "a@b.co and a@b.co"; var entities = new[] { Entity(PiiEntityType.Email, "a@b.co", 0), Entity(PiiEntityType.Email, "a@b.co", 11) }; var result = _redactor.Redact(text, entities); result.SanitizedText.Should().Be(" and "); result.PlaceholderMap.Should().HaveCount(1); } [Test] public void Redact_MultipleTypes_AssignsIndependentCounters() { const string text = "9876543210 ravi@test.com"; var entities = new[] { Entity(PiiEntityType.Phone, "9876543210", 0), Entity(PiiEntityType.Email, "ravi@test.com", 11) }; var result = _redactor.Redact(text, entities); result.SanitizedText.Should().Be(" "); } [Test] public void Redact_MultipleSameTypeDifferentValues_IncrementsCounter() { const string text = "Customer Ravi Kumar and Customer Priya Nair"; var entities = new[] { Entity(PiiEntityType.Person, "Ravi Kumar", 9), Entity(PiiEntityType.Person, "Priya Nair", 33) }; var result = _redactor.Redact(text, entities); result.SanitizedText.Should().Be("Customer and Customer "); result.PlaceholderMap.Should().HaveCount(2); } [Test] public void Redact_EntityTypes_UseTypedPlaceholderPrefixes() { (PiiEntityType Type, string Prefix)[] cases = [ (PiiEntityType.Person, "PERSON"), (PiiEntityType.Email, "EMAIL"), (PiiEntityType.LoanNumber, "LOAN_NUMBER") ]; foreach (var (type, prefix) in cases) { const string value = "VALUE"; var text = $"start {value} end"; var result = _redactor.Redact(text, [Entity(type, value, 6)]); result.SanitizedText.Should().Contain($"<{prefix}_1>", because: type.ToString()); } } [Test] public void Redact_RightToLeftReplacement_PreservesCorrectOutput() { const string text = "AA BB CC"; var entities = new[] { Entity(PiiEntityType.Phone, "AA", 0), Entity(PiiEntityType.Email, "BB", 3), Entity(PiiEntityType.Pan, "CC", 6) }; var result = _redactor.Redact(text, entities); result.SanitizedText.Should().Be(" "); } [TestCase(null)] [TestCase("")] [TestCase(" ")] public void Redact_InvalidText_ThrowsArgumentException(string? text) { var action = () => _redactor.Redact(text!, []); action.Should().Throw(); } [Test] public void Redact_NullEntities_ThrowsArgumentNullException() { var action = () => _redactor.Redact("text", null!); action.Should().Throw(); } private static PiiEntity Entity(PiiEntityType type, string value, int start) => new(type, value, start, value.Length, PiiDetectionSource.Regex); }