evOo-local/apps/api/src/request-body-limit.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

116 lines
4.2 KiB
TypeScript

import { ATTACHMENT_MAX_BASE64_LENGTH } from "@rakazo/contracts";
import { Hono } from "hono";
import { describe, expect, it, vi } from "vitest";
import {
MAX_AUTH_REQUEST_BYTES,
MAX_RPC_REQUEST_BYTES,
mountApiRequestBodyLimits,
requestBodyLimit,
} from "./request-body-limit.js";
function testApp(maxSize: number, parse = vi.fn(async (request: Request) => request.json())) {
const app = new Hono();
app.use("/*", requestBodyLimit(maxSize));
app.post("/parse", async (c) => c.json(await parse(c.req.raw)));
return { app, parse };
}
describe("API request body limits", () => {
it("keeps the RPC allowance above the largest supported attachment envelope", () => {
expect(MAX_RPC_REQUEST_BYTES).toBeGreaterThan(ATTACHMENT_MAX_BASE64_LENGTH);
expect(MAX_AUTH_REQUEST_BYTES).toBeLessThan(MAX_RPC_REQUEST_BYTES);
});
it("mounts the limits on Auth and RPC without intercepting independently bounded routes", async () => {
const parse = vi.fn(async (request: Request) => request.json());
const app = new Hono();
mountApiRequestBodyLimits(app);
app.post("/api/auth/sign-in/email", async (c) => c.json(await parse(c.req.raw)));
app.post("/rpc/test", async (c) => c.json(await parse(c.req.raw)));
app.post("/api/voice/speak", async (c) => c.json(await parse(c.req.raw)));
const request = (path: string, contentLength: number) =>
app.request(path, {
method: "POST",
headers: {
"content-type": "application/json",
"content-length": String(contentLength),
},
body: "{}",
});
const auth = await request("/api/auth/sign-in/email", MAX_AUTH_REQUEST_BYTES + 1);
const rpc = await request("/rpc/test", MAX_RPC_REQUEST_BYTES + 1);
const voice = await request("/api/voice/speak", MAX_RPC_REQUEST_BYTES + 1);
expect(auth.status).toBe(413);
expect(rpc.status).toBe(413);
expect(voice.status).toBe(200);
expect(parse).toHaveBeenCalledOnce();
});
it("rejects an oversized declared body before the route parser", async () => {
const { app, parse } = testApp(8);
const response = await app.request("/parse", {
method: "POST",
headers: { "content-type": "application/json", "content-length": "9" },
body: JSON.stringify({ ok: true }),
});
expect(response.status).toBe(413);
await expect(response.json()).resolves.toEqual({ error: "Request body is too large." });
expect(parse).not.toHaveBeenCalled();
});
it.each(["", "8.0", "0x8", "invalid"])(
"rejects invalid declared length %j before the route parser",
async (contentLength) => {
const { app, parse } = testApp(8);
const response = await app.request("/parse", {
method: "POST",
headers: { "content-type": "application/json", "content-length": contentLength },
body: "{}",
});
expect(response.status).toBe(413);
await expect(response.json()).resolves.toEqual({ error: "Request body is too large." });
expect(parse).not.toHaveBeenCalled();
},
);
it("stops an oversized streamed body before the route parser", async () => {
const cancel = vi.fn();
const { app, parse } = testApp(8);
const response = await app.request(
new Request("http://localhost/parse", {
method: "POST",
headers: { "content-type": "application/json" },
body: new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('{"value":"'));
controller.enqueue(new TextEncoder().encode('too large"}'));
},
cancel,
}),
duplex: "half",
} as RequestInit & { duplex: "half" }),
);
expect(response.status).toBe(413);
await expect(response.json()).resolves.toEqual({ error: "Request body is too large." });
expect(parse).not.toHaveBeenCalled();
expect(cancel).toHaveBeenCalledOnce();
});
it("passes bodies at the configured boundary to the route parser", async () => {
const { app, parse } = testApp(2);
const response = await app.request("/parse", {
method: "POST",
headers: { "content-type": "application/json" },
body: "{}",
});
expect(response.status).toBe(200);
expect(parse).toHaveBeenCalledOnce();
});
});