78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
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);
|
|
});
|
|
});
|