Files
docmost/apps/server/src/common/helpers/utils.spec.ts
T
2026-04-27 15:16:26 +01:00

90 lines
3.2 KiB
TypeScript

import { sanitizeFileName } from './utils';
describe('sanitizeFileName', () => {
describe('default (storage-safe)', () => {
it.each([
['simple.txt', 'simple.txt'],
['my page.md', 'my_page.md'],
['hash#tag.md', 'hash_tag.md'],
['Q4 25% growth.pdf', 'Q4_25%_growth.pdf'],
['résumé.docx', 'résumé.docx'],
])('keeps legitimate input "%s" → "%s"', (input, expected) => {
expect(sanitizeFileName(input)).toBe(expected);
});
it.each([
['file<script>.svg', 'filescript.svg'],
['file:name?.pdf', 'filename.pdf'],
['evil*pipe|file.txt', 'evilpipefile.txt'],
['../traversal.svg', '..traversal.svg'],
['..\\windows.svg', '..windows.svg'],
['nullbyte.txt', 'nullbyte.txt'],
])('strips illegal chars from "%s" → "%s"', (input, expected) => {
expect(sanitizeFileName(input)).toBe(expected);
});
it.each([
['..%2Fevil.svg', '..evil.svg'],
['..%5Cevil.svg', '..evil.svg'],
['..%2Ffiles%2Fa%2Fevil.svg', '..filesaevil.svg'],
['file%3Aname.txt', 'filename.txt'],
['file%00.txt', 'file.txt'],
])(
'decodes percent-encoded path chars before sanitizing "%s" → "%s"',
(input, expected) => {
expect(sanitizeFileName(input)).toBe(expected);
},
);
it('handles double-encoded payloads after one decode pass', () => {
// %252F decodes once to %2F (left as literal — not a path separator
// after only one decode). Fastify already does one decode at the route
// layer, so by the time this helper sees it, %252F has become %2F,
// which we then decode to "/" and strip.
expect(sanitizeFileName('..%252Fevil.svg')).toBe('..%2Fevil.svg');
expect(sanitizeFileName('..%2Fevil.svg')).toBe('..evil.svg');
});
it('leaves malformed percent sequences alone (not decodable)', () => {
// %ZZ is not a valid percent encoding — the regex won't match it.
expect(sanitizeFileName('100%ZZ.txt')).toBe('100%ZZ.txt');
// standalone % with no hex pair after it
expect(sanitizeFileName('100% off.txt')).toBe('100%_off.txt');
});
it('returns empty for inputs that are entirely illegal', () => {
expect(sanitizeFileName('..')).toBe('');
expect(sanitizeFileName('...')).toBe('');
expect(sanitizeFileName('CON.txt')).toBe('');
});
it('caps output at 255 bytes (handled by sanitize-filename internally)', () => {
const long = 'a'.repeat(500) + '.txt';
expect(sanitizeFileName(long).length).toBeLessThanOrEqual(255);
});
});
describe('preserveSpaces option', () => {
it('keeps spaces and # untouched', () => {
expect(
sanitizeFileName('My Page #1.md', { preserveSpaces: true }),
).toBe('My Page #1.md');
});
it('still strips illegal chars and decodes percent-encoding', () => {
expect(
sanitizeFileName('../my page.svg', { preserveSpaces: true }),
).toBe('..my page.svg');
expect(
sanitizeFileName('..%2Fmy page.svg', { preserveSpaces: true }),
).toBe('..my page.svg');
});
it('preserves accented and unicode chars', () => {
expect(
sanitizeFileName('Café & résumé.pdf', { preserveSpaces: true }),
).toBe('Café & résumé.pdf');
});
});
});