import { describe, it, expect } from 'vitest'; import path from 'node:path'; // Mirrors the helpers in src/routes/documents.ts. They aren't exported (the route module pulls in // env + DB on import, which this suite deliberately avoids), so the logic is restated verbatim and // asserted against the shapes that matter. If the route changes, change this with it. const MAX_NAME_LENGTH = 200; function safeDisplayName(filename: string): string { const base = path.basename(filename.replace(/\\/g, '/')); // eslint-disable-next-line no-control-regex const cleaned = base.replace(/[\u0000-\u001f\u007f]/g, '').trim(); return (cleaned || 'document').slice(0, MAX_NAME_LENGTH); } function safeExtension(filename: string): string { const ext = path.extname(safeDisplayName(filename)).toLowerCase(); return /^\.[a-z0-9]{1,8}$/.test(ext) ? ext : ''; } describe('upload filename sanitisation', () => { it('keeps an ordinary name and extension intact', () => { expect(safeDisplayName('Engagement Letter.pdf')).toBe('Engagement Letter.pdf'); expect(safeExtension('Engagement Letter.pdf')).toBe('.pdf'); }); it('strips directory components from posix and windows paths', () => { expect(safeDisplayName('../../etc/passwd')).toBe('passwd'); expect(safeDisplayName('..\\..\\windows\\system32\\config')).toBe('config'); expect(safeDisplayName('/absolute/path/deed.pdf')).toBe('deed.pdf'); }); it('never lets a path separator reach the storage key', () => { for (const name of ['a/b.pdf', 'a\\b.pdf', '../x.pdf', '..%2fx.pdf']) { expect(safeExtension(name)).not.toContain('/'); expect(safeExtension(name)).not.toContain('\\'); expect(safeDisplayName(name)).not.toContain('/'); expect(safeDisplayName(name)).not.toContain('\\'); } }); it('removes control characters, including NUL and CRLF', () => { expect(safeDisplayName('in\rvoice\n.pdf')).toBe('invoice.pdf'); expect(safeDisplayName('a\u0000b.pdf')).toBe('ab.pdf'); expect(safeDisplayName('tab\tsep.pdf')).toBe('tabsep.pdf'); expect(safeDisplayName('del\u007fx.pdf')).toBe('delx.pdf'); }); it('bounds the stored display name', () => { const long = `${'x'.repeat(500)}.pdf`; expect(safeDisplayName(long).length).toBe(MAX_NAME_LENGTH); }); it('falls back to a placeholder rather than an empty name', () => { expect(safeDisplayName('')).toBe('document'); expect(safeDisplayName(' ')).toBe('document'); expect(safeDisplayName('')).toBe('document'); }); it('drops unusual or overlong extensions instead of concatenating them', () => { expect(safeExtension('payload.')).toBe(''); expect(safeExtension('noextension')).toBe(''); expect(safeExtension('x.thisextensioniswaytoolong')).toBe(''); }); it('normalises extension case', () => { expect(safeExtension('SCAN.PDF')).toBe('.pdf'); }); it('produces a storage key that stays inside the firm/case prefix', () => { const firmId = '11111111-1111-1111-1111-111111111111'; const caseId = '22222222-2222-2222-2222-222222222222'; const docId = '33333333-3333-3333-3333-333333333333'; for (const hostile of ['../../../../etc/passwd', 'a\\..\\..\\b.pdf', 'x.pdf/../../y']) { const key = `${firmId}/${caseId}/${docId}${safeExtension(hostile)}`; expect(key.startsWith(`${firmId}/${caseId}/`)).toBe(true); expect(key.split('/').length).toBe(3); expect(key).not.toContain('..'); } }); });