allowlist.test.ts4.2 KBView on GitHub /**
* Tests: apps/mail/modules/files/upload/allowlist.ts
*
* The frontend allowlist MIRRORS the server one (apps/server/src/services/
* file-system/uploads/allowlist.ts). The two are intentionally duplicated
* (no shared package today — see design §1 follow-up). These tests serve as
* a drift detector: every change to one must be mirrored in the other.
*
* Covers:
* - validateUpload size + type rules (matches server)
* - the table of allowed (extension, MIME) pairs
* - inputAcceptAttribute() returns a comma-separated allowlist for <input>
* - sha256OfFile produces lowercase hex of length 64 (Web Crypto smoke)
*/
import {
ALLOWED_EXTENSIONS,
ALLOWED_MIME_TYPES,
MAX_FILES_PER_DROP,
MAX_UPLOAD_BYTES,
UPLOAD_ALLOWLIST,
categoryFor,
extOf,
inputAcceptAttribute,
validateUpload,
} from '@/modules/files/upload/allowlist';
describe('extOf', () => {
it('returns the lowercased extension after the last dot', () => {
expect(extOf('Foo.PDF')).toBe('.pdf');
expect(extOf('archive.tar.gz')).toBe('.gz');
});
it('returns empty string when no dot is present', () => {
expect(extOf('README')).toBe('');
});
});
describe('categoryFor', () => {
it('classifies by MIME first, then extension', () => {
expect(categoryFor('application/pdf', 'doc.pdf')).toBe('pdf');
expect(categoryFor('application/octet-stream', 'doc.pdf')).toBe('pdf');
expect(categoryFor('application/octet-stream', 'malware.exe')).toBeNull();
});
it('is case-insensitive on MIME and extension', () => {
expect(categoryFor('IMAGE/PNG', 'x')).toBe('image');
expect(categoryFor('application/octet-stream', 'pic.PNG')).toBe('image');
});
});
describe('validateUpload', () => {
it('accepts a typical PDF', () => {
expect(
validateUpload({ filename: 'doc.pdf', mimeType: 'application/pdf', sizeBytes: 1024 }),
).toMatchObject({ ok: true, category: 'pdf' });
});
it.each([
[0, 'empty'],
[-1, 'empty'],
[MAX_UPLOAD_BYTES + 1, 'too_large'],
])('rejects size=%i with code=%s', (sizeBytes, code) => {
expect(
validateUpload({ filename: 'doc.pdf', mimeType: 'application/pdf', sizeBytes }),
).toMatchObject({ ok: false, code });
});
it('rejects unknown types with code=bad_type', () => {
expect(
validateUpload({ filename: 'pwn.exe', mimeType: 'application/octet-stream', sizeBytes: 1 }),
).toMatchObject({ ok: false, code: 'bad_type' });
});
it('runs size check before type check', () => {
expect(
validateUpload({
filename: 'pwn.exe',
mimeType: 'application/octet-stream',
sizeBytes: MAX_UPLOAD_BYTES + 1,
}),
).toMatchObject({ ok: false, code: 'too_large' });
});
it('lowercases the returned mimeType', () => {
const result = validateUpload({
filename: 'doc.pdf',
mimeType: 'APPLICATION/PDF',
sizeBytes: 1,
});
expect(result.ok).toBe(true);
if (result.ok) expect(result.mimeType).toBe('application/pdf');
});
});
describe('drift detector vs allowlist table', () => {
it('ALLOWED_EXTENSIONS is derived from UPLOAD_ALLOWLIST', () => {
const flat = UPLOAD_ALLOWLIST.flatMap((e) => e.extensions);
for (const ext of flat) expect(ALLOWED_EXTENSIONS.has(ext)).toBe(true);
expect(ALLOWED_EXTENSIONS.size).toBe(new Set(flat).size);
});
it('ALLOWED_MIME_TYPES is derived from UPLOAD_ALLOWLIST', () => {
const flat = UPLOAD_ALLOWLIST.flatMap((e) => e.mimeTypes);
for (const mime of flat) expect(ALLOWED_MIME_TYPES.has(mime)).toBe(true);
expect(ALLOWED_MIME_TYPES.size).toBe(new Set(flat).size);
});
});
describe('inputAcceptAttribute', () => {
it('returns a comma-separated allowlist suitable for <input accept>', () => {
const accept = inputAcceptAttribute();
// Every allowed extension and every allowed MIME must appear.
for (const ext of ALLOWED_EXTENSIONS) expect(accept).toContain(ext);
for (const mime of ALLOWED_MIME_TYPES) expect(accept).toContain(mime);
expect(accept).toContain(',');
});
});
describe('limits', () => {
it('MAX_UPLOAD_BYTES is 25 MB', () => {
expect(MAX_UPLOAD_BYTES).toBe(25 * 1024 * 1024);
});
it('MAX_FILES_PER_DROP is 10', () => {
expect(MAX_FILES_PER_DROP).toBe(10);
});
});