Add PII redaction POC for secure LLM prompting.
Implements detect-redact-sanitize pipeline with regex, domain rules, and ONNX NER before the LLM boundary, plus NUnit tests and Xenovex push documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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 <EMAIL_1> end.");
|
||||
result.PlaceholderMap["<EMAIL_1>"].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("<EMAIL_1> and <EMAIL_1>");
|
||||
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("<PHONE_1> <EMAIL_1>");
|
||||
}
|
||||
|
||||
[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 <PERSON_2> and Customer <PERSON_1>");
|
||||
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("<PHONE_1> <EMAIL_1> <PAN_1>");
|
||||
}
|
||||
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void Redact_InvalidText_ThrowsArgumentException(string? text)
|
||||
{
|
||||
var action = () => _redactor.Redact(text!, []);
|
||||
action.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Redact_NullEntities_ThrowsArgumentNullException()
|
||||
{
|
||||
var action = () => _redactor.Redact("text", null!);
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
private static PiiEntity Entity(PiiEntityType type, string value, int start) =>
|
||||
new(type, value, start, value.Length, PiiDetectionSource.Regex);
|
||||
}
|
||||
Reference in New Issue
Block a user