Files
TREK/server/tests/unit/nest/content-disposition.test.ts
T
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +02:00

93 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { contentDisposition } from '../../../src/nest/common/content-disposition';
import { describe, it, expect } from 'vitest';
// Node refuses header values with codepoints above 0xFF (ERR_INVALID_CHAR), so
// the builder must hand back printable ASCII no matter what the name holds —
// and stay byte-identical to the old hand-built header for plain ASCII names.
describe('contentDisposition (#2165)', () => {
it('keeps a plain ASCII name byte-identical to the hand-built header', () => {
expect(contentDisposition('Alpine-week.gpx', 'attachment')).toBe('attachment; filename="Alpine-week.gpx"');
expect(contentDisposition('all-trips.ics', 'inline')).toBe('inline; filename="all-trips.ics"');
// Spaces are fine inside a quoted-string; no reason to grow a filename*.
expect(contentDisposition('My Trip 2025.ics', 'attachment')).toBe('attachment; filename="My Trip 2025.ics"');
expect(contentDisposition('', 'attachment')).toBe('attachment; filename=""');
});
it('adds filename* only when the quoted fallback loses something', () => {
expect(contentDisposition('沖縄-4泊5日.gpx', 'attachment')).toBe(
'attachment; filename="__-4_5_.gpx"; filename*=UTF-8\'\'%E6%B2%96%E7%B8%84-4%E6%B3%8A5%E6%97%A5.gpx',
);
});
it('folds an accent in both its composed and combining forms', () => {
expect(contentDisposition('Café.gpx', 'attachment')).toBe(
'attachment; filename="Caf_.gpx"; filename*=UTF-8\'\'Caf%C3%A9.gpx',
);
// NFKD output: 'e' + U+0301 — the combining mark alone is already > 0xFF.
expect(contentDisposition('Café.gpx', 'attachment')).toBe(
'attachment; filename="Cafe_.gpx"; filename*=UTF-8\'\'Cafe%CC%81.gpx',
);
});
it('neutralizes CR/LF and quotes — nothing escapes the quoted-string', () => {
expect(contentDisposition('evil\r\nContent-Length: 0"; x="y.gpx', 'attachment')).toBe(
'attachment; filename="evil Content-Length: 0_; x=_y.gpx"; filename*=UTF-8\'\'evil%20%20Content-Length%3A%200%22%3B%20x%3D%22y.gpx',
);
});
it('folds a tab to a space instead of riding it into the header', () => {
expect(contentDisposition('a\tb.ics', 'attachment')).toBe(
'attachment; filename="a b.ics"; filename*=UTF-8\'\'a%20b.ics',
);
});
it('catches the JS-\\s stragglers like U+3000', () => {
expect(contentDisposition('a b.ics', 'inline')).toBe(
'inline; filename="a_b.ics"; filename*=UTF-8\'\'a%E3%80%80b.ics',
);
});
it("escapes the RFC 5987 stragglers encodeURIComponent leaves bare: ' ( ) *", () => {
expect(contentDisposition("日(1)'*.gpx", 'attachment')).toBe(
"attachment; filename=\"_(1)'*.gpx\"; filename*=UTF-8''%E6%97%A5%281%29%27%2A.gpx",
);
});
it('falls back to download when the folding erased everything readable, keeping the extension', () => {
expect(contentDisposition('日本語.gpx', 'attachment')).toBe(
'attachment; filename="download.gpx"; filename*=UTF-8\'\'%E6%97%A5%E6%9C%AC%E8%AA%9E.gpx',
);
expect(contentDisposition('日本語', 'attachment')).toBe(
'attachment; filename="download"; filename*=UTF-8\'\'%E6%97%A5%E6%9C%AC%E8%AA%9E',
);
});
it('carries an emoji through filename* and survives a name cut mid-emoji', () => {
expect(contentDisposition('\u{1F5FE}.gpx', 'attachment')).toBe(
'attachment; filename="download.gpx"; filename*=UTF-8\'\'%F0%9F%97%BE.gpx',
);
// A lone surrogate would make encodeURIComponent throw "URI malformed" —
// exactly the crash class this helper exists to end.
expect(contentDisposition('\uD83D.gpx', 'attachment')).toBe(
'attachment; filename="download.gpx"; filename*=UTF-8\'\'%EF%BF%BD.gpx',
);
});
it('always yields a value Node accepts: printable ASCII, whatever comes in', () => {
const nasty = [
'沖縄 4泊5日.gpx',
'a
b
c.ics',
'"\\.pkpass',
'\x00\x1f\x7f.gpx',
'\u{1F9ED}'.repeat(40),
'plain.txt',
'',
];
for (const name of nasty) {
expect(contentDisposition(name, 'attachment')).toMatch(/^[\x20-\x7e]+$/);
expect(contentDisposition(name, 'inline')).toMatch(/^[\x20-\x7e]+$/);
}
});
});