moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
/**
* R1-012 regression — upload size limit must be enforced WHILE reading the
* request body, not after the whole thing has been buffered into memory.
*
* The old code did `const rawBody = await getRawBody(request)` (buffering the
* ENTIRE stream) and only THEN compared `rawBody.length > MAX_UPLOAD_SIZE`.
* A multi-GB upload would OOM the process before the size guard ever ran.
*
* These tests drive `getRawBody` directly with a fake stream so we don't need
* to allocate gigabytes: they assert the helper rejects as soon as the
* accumulated byte count exceeds the limit (and honours Content-Length up
* front), instead of buffering-then-checking.
*/
import { describe, it, expect } from 'vitest';
import { PassThrough } from 'node:stream';
import type { FastifyRequest } from 'fastify';
import { getRawBody, MAX_BODY_BYTES_EXCEEDED } from '../../src/local/routes/files.js';
/**
* Build a fake FastifyRequest whose `.raw` is a real Readable stream
* (PassThrough) — so we exercise the same `.on('data'|'end'|'error')` +
* `.destroy()` surface as a Node IncomingMessage. `contentLength` is optional
* and sets the content-length header (the up-front guard).
*/
function fakeRequest(contentLength?: number) {
const raw = new PassThrough();
const headers: Record<string, string> = {};
if (contentLength != null) headers['content-length'] = String(contentLength);
const request = { raw, headers } as unknown as FastifyRequest;
return { request, raw };
}
describe('R1-012 — getRawBody streaming size guard', () => {
it('rejects via Content-Length before reading any body bytes', async () => {
const limit = 1024;
const { request, raw } = fakeRequest(limit + 1);
const promise = getRawBody(request, limit);
// We never emit any 'data'/'end' — if the guard works it rejects up front.
await expect(promise).rejects.toMatchObject({ code: MAX_BODY_BYTES_EXCEEDED });
// No listeners should have been left dangling on the raw stream.
expect(raw.listenerCount('data')).toBe(0);
});
it('aborts mid-stream once accumulated bytes exceed the limit (no full buffering)', async () => {
const limit = 1024;
const { request, raw } = fakeRequest(); // no content-length header
const promise = getRawBody(request, limit);
// Emit chunks that together exceed the limit. The guard should reject
// BEFORE we ever emit 'end' (i.e. without buffering the whole body).
const chunk = Buffer.alloc(600, 0x61);
raw.emit('data', chunk); // 600 bytes — under limit, fine
raw.emit('data', chunk); // 1200 bytes total — over limit, must reject now
await expect(promise).rejects.toMatchObject({ code: MAX_BODY_BYTES_EXCEEDED });
});
it('resolves with the full buffer for a within-limit body', async () => {
const limit = 1024;
const { request, raw } = fakeRequest();
const promise = getRawBody(request, limit);
const a = Buffer.from('hello ');
const b = Buffer.from('waggle');
raw.emit('data', a);
raw.emit('data', b);
raw.emit('end');
const body = await promise;
expect(body.equals(Buffer.concat([a, b]))).toBe(true);
});
it('propagates stream errors via reject', async () => {
const limit = 1024;
const { request, raw } = fakeRequest();
const promise = getRawBody(request, limit);
const boom = new Error('socket reset');
raw.emit('error', boom);
await expect(promise).rejects.toBe(boom);
});
});