import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { parseRange, safeResolve, streamFile } from './files'; let root: string; beforeAll(async () => { root = await mkdtemp(path.join(tmpdir(), 'books-')); await mkdir(path.join(root, 'library', 'a'), { recursive: true }); await writeFile(path.join(root, 'library', 'a', 'book.epub'), '0123456789'); await writeFile(path.join(root, 'secret.txt'), 'nope'); await symlink(path.join(root, 'secret.txt'), path.join(root, 'library', 'escape.epub')); }); afterAll(() => rm(root, { recursive: true, force: true })); describe('safeResolve', () => { it('finds files inside the library', async () => { expect(await safeResolve('library', 'a/book.epub', root)).toBe( path.join(root, 'library', 'a', 'book.epub') ); }); it.each([ '../secret.txt', 'a/../../secret.txt', '/etc/passwd', 'escape.epub', 'a', '', 'missing.pdf' ])('refuses %s', async (relative) => { expect(await safeResolve('library', relative, root)).toBeNull(); }); }); describe('parseRange', () => { it('parses single ranges', () => { expect(parseRange('bytes=0-3', 10)).toEqual({ start: 0, end: 3 }); expect(parseRange('bytes=5-', 10)).toEqual({ start: 5, end: 9 }); expect(parseRange('bytes=-4', 10)).toEqual({ start: 6, end: 9 }); expect(parseRange('bytes=8-100', 10)).toEqual({ start: 8, end: 9 }); }); it('ignores or rejects bad ranges', () => { expect(parseRange(null, 10)).toBeNull(); expect(parseRange('bytes=0-1,4-5', 10)).toBeNull(); expect(parseRange('bytes=10-', 10)).toBe('invalid'); expect(parseRange('bytes=5-2', 10)).toBe('invalid'); }); }); describe('streamFile', () => { const file = () => path.join(root, 'library', 'a', 'book.epub'); it('serves the whole file', async () => { const res = await streamFile(file(), new Request('http://x/'), {}); expect(res.status).toBe(200); expect(res.headers.get('content-type')).toBe('application/epub+zip'); expect(await res.text()).toBe('0123456789'); }); it('serves partial content', async () => { const res = await streamFile( file(), new Request('http://x/', { headers: { range: 'bytes=2-4' } }), {} ); expect(res.status).toBe(206); expect(res.headers.get('content-range')).toBe('bytes 2-4/10'); expect(await res.text()).toBe('234'); }); it('answers 416 for unsatisfiable ranges', async () => { const res = await streamFile( file(), new Request('http://x/', { headers: { range: 'bytes=20-' } }), {} ); expect(res.status).toBe(416); }); });