Files
2026-10-09 16:52:37 -04:00

146 lines
6.0 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
artifactFormBody,
boundedBody,
jsonBody,
BETA_REVISION_FORM_LIMIT
} from '../../src/lib/server/request-body.js';
import { MAX_ARTIFACT_BYTES } from '../../src/lib/artifact-limits.js';
function streamedRequest(chunks: Uint8Array[], headers?: HeadersInit) {
let index = 0;
const pull = vi.fn((controller: ReadableStreamDefaultController<Uint8Array>) => {
if (index === chunks.length) controller.close();
else controller.enqueue(chunks[index++]);
});
const cancel = vi.fn();
const request = new Request('http://localhost', {
method: 'POST',
headers,
body: new ReadableStream({ pull, cancel }, { highWaterMark: 0 }),
duplex: 'half'
} as RequestInit);
return { request, pull, cancel };
}
describe('bounded request bodies', () => {
it('rejects excessive Content-Length before reading any bytes', async () => {
const { request, pull, cancel } = streamedRequest([Buffer.from('{}')], {
'Content-Length': '11'
});
await expect(jsonBody(request, 10)).rejects.toMatchObject({ status: 413 });
expect(pull).not.toHaveBeenCalled();
expect(cancel).toHaveBeenCalledOnce();
expect(request.body?.locked).toBe(false);
});
it.each([undefined, '1'])(
'counts actual bytes with Content-Length %s and cancels before the remaining chunks',
async (length) => {
const { request, pull, cancel } = streamedRequest(
[Buffer.from('{"value":'), Buffer.from('"too long"'), Buffer.from('}')],
length ? { 'Content-Length': length } : undefined
);
await expect(jsonBody(request, 10)).rejects.toMatchObject({ status: 413 });
expect(pull).toHaveBeenCalledTimes(2);
expect(cancel).toHaveBeenCalledOnce();
expect(request.body?.locked).toBe(false);
}
);
it('accepts the exact UTF-8 byte limit across tiny chunks', async () => {
const bytes = Buffer.from('{"value":"é🍒"}');
const chunks = Array.from(bytes, (byte) => Uint8Array.of(byte));
await expect(jsonBody(streamedRequest(chunks).request, bytes.length)).resolves.toEqual({
value: 'é🍒'
});
await expect(
jsonBody(streamedRequest(chunks).request, bytes.length - 1)
).rejects.toMatchObject({
status: 413
});
});
it('preserves bytes across buffer blocks and a partial final block', async () => {
const bytes = Buffer.from('abc'.repeat(50_000));
const chunks = [bytes.subarray(0, 7), bytes.subarray(7, 70_000), bytes.subarray(70_000)];
await expect(boundedBody(streamedRequest(chunks).request, bytes.length)).resolves.toEqual(
bytes
);
});
it('keeps malformed and empty JSON distinct from size failures', async () => {
for (const body of ['', '{']) {
await expect(
jsonBody(new Request('http://localhost', { method: 'POST', body }))
).rejects.toBeInstanceOf(SyntaxError);
}
});
});
describe('bounded artifact forms', () => {
it('accepts exact request limits and rejects overflow before parsing the form', async () => {
const form = new FormData();
form.set('content', new File(['main(){}'], 'hud.gsc'));
form.set('notes', 'review');
const encoded = new Response(form);
const headers = encoded.headers;
const bytes = new Uint8Array(await encoded.arrayBuffer());
const request = () =>
streamedRequest([bytes.subarray(0, 5), bytes.subarray(5)], headers).request;
const parsed = await artifactFormBody(request(), bytes.length);
expect(parsed.get('notes')).toBe('review');
expect(await (parsed.get('content') as File).text()).toBe('main(){}');
await expect(artifactFormBody(request(), bytes.length - 1)).rejects.toMatchObject({
status: 413
});
await expect(
artifactFormBody(streamedRequest([Buffer.alloc(11)], headers).request, 10)
).rejects.toMatchObject({ status: 413 });
});
it('accepts a 32 MiB file plus multipart overhead', async () => {
const form = new FormData();
form.set('content', new File([new Uint8Array(MAX_ARTIFACT_BYTES)], 'plugin.dll'));
const parsed = await artifactFormBody(
new Request('http://localhost', { method: 'POST', body: form })
);
expect((parsed.get('content') as File).size).toBe(MAX_ARTIFACT_BYTES);
});
it.each(['file', 'text'])(
'rejects an oversized %s artifact within the overall form limit',
async (kind) => {
const form = new FormData();
if (kind === 'file')
form.set(
'content',
new File([new Uint8Array(MAX_ARTIFACT_BYTES + 1)], 'plugin.dll')
);
else form.set('text', 'é'.repeat(MAX_ARTIFACT_BYTES / 2 + 1));
await expect(
artifactFormBody(new Request('http://localhost', { method: 'POST', body: form }))
).rejects.toMatchObject({
status: 413,
body: { message: 'Artifacts must not exceed 32 MiB.' }
});
}
);
it('bounds the combined beta revision body even with no declared length', async () => {
const chunk = new Uint8Array(1024 * 1024);
const { request, cancel } = streamedRequest(Array(67).fill(chunk), {
'Content-Type': 'multipart/form-data; boundary=test'
});
await expect(artifactFormBody(request, BETA_REVISION_FORM_LIMIT)).rejects.toMatchObject({
status: 413
});
expect(cancel).toHaveBeenCalledOnce();
});
it('reports malformed multipart bodies as bad requests', async () => {
const request = new Request('http://localhost', { method: 'POST', body: 'invalid form' });
await expect(artifactFormBody(request)).rejects.toMatchObject({ status: 400 });
});
});