toBeInstanceOf(SensitiveDataRedactor::class); }); it('accepts custom RedactionMode', function () { $redactor = new SensitiveDataRedactor(RedactionMode::FULL); expect($redactor)->toBeInstanceOf(SensitiveDataRedactor::class); }); it('accepts email and IP redaction flags', function () { $redactor = new SensitiveDataRedactor( redactEmails: true, redactIps: true ); expect($redactor)->toBeInstanceOf(SensitiveDataRedactor::class); }); }); describe('key-based redaction', function () { it('redacts password fields', function () { $redactor = new SensitiveDataRedactor(RedactionMode::FULL); $data = ['username' => 'john', 'password' => 'secret123']; $redacted = $redactor->redact($data); expect($redacted['username'])->toBe('john'); expect($redacted['password'])->toBe('[REDACTED]'); }); it('redacts API key fields', function () { $redactor = new SensitiveDataRedactor(RedactionMode::FULL); $data = ['api_key' => 'sk_live_1234567890abcdef']; $redacted = $redactor->redact($data); expect($redacted['api_key'])->toBe('[REDACTED]'); }); it('redacts multiple sensitive fields', function () { $redactor = new SensitiveDataRedactor(RedactionMode::FULL); $data = [ 'username' => 'john', 'password' => 'secret', 'api_key' => 'key123', 'token' => 'token456', 'user_id' => 42 ]; $redacted = $redactor->redact($data); expect($redacted['username'])->toBe('john'); expect($redacted['password'])->toBe('[REDACTED]'); expect($redacted['api_key'])->toBe('[REDACTED]'); expect($redacted['token'])->toBe('[REDACTED]'); expect($redacted['user_id'])->toBe(42); }); it('handles nested arrays', function () { $redactor = new SensitiveDataRedactor(RedactionMode::FULL); $data = [ 'user' => [ 'name' => 'John Doe', 'password' => 'secret', 'preferences' => [ 'theme' => 'dark', 'api_key' => 'key123' ] ] ]; $redacted = $redactor->redact($data); expect($redacted['user']['name'])->toBe('John Doe'); expect($redacted['user']['password'])->toBe('[REDACTED]'); expect($redacted['user']['preferences']['theme'])->toBe('dark'); expect($redacted['user']['preferences']['api_key'])->toBe('[REDACTED]'); }); }); describe('redaction modes', function () { it('uses FULL mode to completely mask values', function () { $redactor = new SensitiveDataRedactor(RedactionMode::FULL); $data = ['password' => 'super-secret-password']; $redacted = $redactor->redact($data); expect($redacted['password'])->toBe('[REDACTED]'); }); it('uses PARTIAL mode to partially mask values', function () { $redactor = new SensitiveDataRedactor(RedactionMode::PARTIAL); $data = ['password' => 'super-secret-password']; $redacted = $redactor->redact($data); expect($redacted['password'])->toStartWith('su'); expect($redacted['password'])->toEndWith('rd'); expect($redacted['password'])->toContain('*'); }); it('uses HASH mode for deterministic redaction', function () { $redactor = new SensitiveDataRedactor(RedactionMode::HASH); $data = ['password' => 'test123']; $redacted = $redactor->redact($data); expect($redacted['password'])->toStartWith('[HASH:'); expect($redacted['password'])->toEndWith(']'); // Same input should produce same hash $redacted2 = $redactor->redact($data); expect($redacted['password'])->toBe($redacted2['password']); }); }); describe('content-based redaction', function () { it('redacts credit card numbers', function () { $redactor = new SensitiveDataRedactor(); $message = 'Payment with card 4532-1234-5678-9010'; $redacted = $redactor->redactString($message); expect($redacted)->toContain('[CREDIT_CARD]'); expect(str_contains($redacted, '4532-1234-5678-9010'))->toBeFalsy(); }); it('redacts Bearer tokens', function () { $redactor = new SensitiveDataRedactor(); $message = 'Auth: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature'; $redacted = $redactor->redactString($message); expect($redacted)->toContain('Bearer [REDACTED]'); expect(str_contains($redacted, 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'))->toBeFalsy(); }); it('redacts SSN numbers', function () { $redactor = new SensitiveDataRedactor(); $message = 'SSN: 123-45-6789'; $redacted = $redactor->redactString($message); expect($redacted)->toContain('[SSN]'); expect(str_contains($redacted, '123-45-6789'))->toBeFalsy(); }); it('redacts email addresses when enabled', function () { $redactor = new SensitiveDataRedactor(redactEmails: true); $message = 'Contact: john.doe@example.com'; $redacted = $redactor->redactString($message); expect($redacted)->toContain('@example.com'); expect(str_contains($redacted, 'john.doe'))->toBeFalsy(); }); it('does not redact email addresses when disabled', function () { $redactor = new SensitiveDataRedactor(redactEmails: false); $message = 'Contact: john.doe@example.com'; $redacted = $redactor->redactString($message); expect($redacted)->toContain('john.doe@example.com'); }); it('redacts IP addresses when enabled', function () { $redactor = new SensitiveDataRedactor(redactIps: true); $message = 'Request from 192.168.1.100'; $redacted = $redactor->redactString($message); expect($redacted)->toContain('[IP_ADDRESS]'); expect(str_contains($redacted, '192.168.1.100'))->toBeFalsy(); }); it('does not redact IP addresses when disabled', function () { $redactor = new SensitiveDataRedactor(redactIps: false); $message = 'Request from 192.168.1.100'; $redacted = $redactor->redactString($message); expect($redacted)->toContain('192.168.1.100'); }); }); describe('factory methods', function () { it('creates production redactor with full redaction', function () { $redactor = SensitiveDataRedactor::production(); $data = ['password' => 'secret', 'user' => 'john']; $redacted = $redactor->redact($data); expect($redacted['password'])->toBe('[REDACTED]'); expect($redacted['user'])->toBe('john'); }); it('creates development redactor with partial redaction', function () { $redactor = SensitiveDataRedactor::development(); $data = ['password' => 'super-secret-password']; $redacted = $redactor->redact($data); expect($redacted['password'])->toStartWith('su'); expect($redacted['password'])->toEndWith('rd'); }); it('creates testing redactor with hash-based redaction', function () { $redactor = SensitiveDataRedactor::testing(); $data = ['password' => 'test123']; $redacted = $redactor->redact($data); expect($redacted['password'])->toStartWith('[HASH:'); }); }); describe('edge cases', function () { it('handles empty arrays', function () { $redactor = new SensitiveDataRedactor(); $redacted = $redactor->redact([]); expect($redacted)->toBe([]); }); it('handles empty strings', function () { $redactor = new SensitiveDataRedactor(); $redacted = $redactor->redactString(''); expect($redacted)->toBe(''); }); it('handles arrays with array values in sensitive fields', function () { $redactor = new SensitiveDataRedactor(RedactionMode::FULL); $data = ['password' => ['old' => 'secret1', 'new' => 'secret2']]; $redacted = $redactor->redact($data); expect($redacted['password'])->toBeArray(); expect($redacted['password']['old'])->toBe('[REDACTED]'); expect($redacted['password']['new'])->toBe('[REDACTED]'); }); it('handles non-string values in sensitive fields', function () { $redactor = new SensitiveDataRedactor(RedactionMode::FULL); $data = ['password' => 12345]; $redacted = $redactor->redact($data); expect($redacted['password'])->toBe('[REDACTED]'); }); it('handles short passwords in partial mode', function () { $redactor = new SensitiveDataRedactor(RedactionMode::PARTIAL); $data = ['password' => 'ab']; $redacted = $redactor->redact($data); expect($redacted['password'])->toBe('**'); }); it('handles unicode characters', function () { $redactor = new SensitiveDataRedactor(RedactionMode::PARTIAL); $data = ['password' => 'Passwörd123']; $redacted = $redactor->redact($data); expect($redacted['password'])->toContain('*'); }); }); describe('readonly behavior', function () { it('is a readonly class', function () { $reflection = new ReflectionClass(SensitiveDataRedactor::class); expect($reflection->isReadOnly())->toBeTrue(); }); }); });