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) => { 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 }); }); });