import { checkPii } from './piiDetection'; const baseConfig = { enabled: true, rules: [ { id: 'email', label: { it: 'Email', en: 'Email' }, pattern: '\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b', message: { it: 'Contiene email.', en: 'Contains email.' }, }, { id: 'iban', label: { it: 'IBAN', en: 'IBAN' }, pattern: '\\b[A-Z]{2}\\d{2}(?:[ ]?[A-Z0-9]{4}){3,7}(?:[ ]?[A-Z0-9]{1,4})?\\b', message: { it: 'Contiene IBAN.', en: 'Contains IBAN.' }, }, ], errorMessage: { it: 'Dati sensibili.', en: 'Sensitive data.', }, }; describe('checkPii', () => { describe('edge cases: no match', () => { it('returns no match when config is disabled', () => { expect( checkPii('test@example.com', { ...baseConfig, enabled: false }, 'en') ).toEqual({ matched: false }); }); it('returns no match when config is null', () => { expect(checkPii('hello', null as any, 'en')).toEqual({ matched: false }); }); it('returns no match when config is undefined', () => { expect(checkPii('hello', undefined as any, 'en')).toEqual({ matched: false, }); }); it('returns no match when rules is empty array', () => { expect( checkPii('test@example.com', { ...baseConfig, rules: [] }, 'en') ).toEqual({ matched: false }); }); it('returns no match when rules is not an array', () => { expect( checkPii('test@example.com', { ...baseConfig, rules: null as any }, 'en') ).toEqual({ matched: false }); }); it('returns no match when no rule matches the text', () => { expect(checkPii('just plain hello world', baseConfig, 'en')).toEqual({ matched: false, }); }); it('skips rules with empty pattern (avoids matching everything)', () => { const configWithEmptyPattern = { ...baseConfig, rules: [ { id: 'empty', label: { it: 'Empty', en: 'Empty' }, pattern: '', message: { en: 'Empty' } }, { id: 'space', label: { it: 'Space', en: 'Space' }, pattern: ' ', message: { en: 'Space' }, }, ], }; expect(checkPii('anything at all', configWithEmptyPattern, 'en')).toEqual( { matched: false } ); }); it('treats invalid regex as no match and does not throw', () => { const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); const configBadRegex = { ...baseConfig, rules: [ { id: 'bad', label: { it: 'Bad', en: 'Bad' }, pattern: '[unclosed', message: { en: 'Bad' }, }, ], }; expect(checkPii('test', configBadRegex, 'en')).toEqual({ matched: false }); expect(warnSpy).toHaveBeenCalledWith( '[PII] Invalid regex for rule:', 'bad', '[unclosed', expect.any(Error) ); warnSpy.mockRestore(); }); }); describe('single rule match', () => { it('returns match and errorText when email pattern matches', () => { const result = checkPii('contact me at test@example.com please', baseConfig, 'en'); expect(result.matched).toBe(true); expect(result.errorText).toContain('Sensitive data.'); expect(result.errorText).toContain('Contains email.'); expect(result.errorText).not.toContain('Contains IBAN.'); }); it('returns match with Italian messages when lang is it', () => { const result = checkPii('test@example.com', baseConfig, 'it'); expect(result.matched).toBe(true); expect(result.errorText).toContain('Dati sensibili.'); expect(result.errorText).toContain('Contiene email.'); }); it('falls back to en when lang has no translation', () => { const result = checkPii('test@example.com', baseConfig, 'fr'); expect(result.matched).toBe(true); expect(result.errorText).toContain('Sensitive data.'); expect(result.errorText).toContain('Contains email.'); }); it('falls back to first available message when errorMessage has no en or requested lang', () => { const configNoEn = { ...baseConfig, errorMessage: { it: 'Solo italiano' }, }; const result = checkPii('test@example.com', configNoEn, 'de'); expect(result.matched).toBe(true); expect(result.errorText).toContain('Solo italiano'); }); }); describe('multiple rules and deduplication', () => { it('returns match with both rule messages when text matches email and IBAN', () => { const text = 'send to test@example.com and IT60X0542811101000000123456'; const result = checkPii(text, baseConfig, 'en'); expect(result.matched).toBe(true); expect(result.errorText).toContain('Sensitive data.'); expect(result.errorText).toContain('Contains email.'); expect(result.errorText).toContain('Contains IBAN.'); }); it('deduplicates by rule id when multiple rules share same id', () => { const configDupId = { ...baseConfig, rules: [ { id: 'email', label: { it: 'Email 1', en: 'Email 1' }, pattern: '\\b[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}\\b', message: { en: 'Email 1' }, }, { id: 'email', label: { it: 'Email 2', en: 'Email 2' }, pattern: '@', message: { en: 'Email 2' }, }, ], }; const result = checkPii('test@example.com', configDupId, 'en'); expect(result.matched).toBe(true); const lines = result.errorText!.split('\n'); const emailLines = lines.filter(l => l.includes('Email')); expect(emailLines.length).toBe(1); }); }); describe('lang normalization', () => { it('handles empty string lang with fallback to en', () => { const result = checkPii('test@example.com', baseConfig, ''); expect(result.matched).toBe(true); expect(result.errorText).toContain('Sensitive data.'); }); it('normalizes lang to lowercase for lookup', () => { const result = checkPii('test@example.com', baseConfig, 'EN'); expect(result.matched).toBe(true); expect(result.errorText).toContain('Sensitive data.'); }); }); });