UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

364 lines (309 loc) 9.96 kB
import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib"; import { Alepha, z } from "alepha"; import { describe, it } from "vitest"; import { $action, HttpError } from "../index.ts"; import { bodyParserOptions, ServerBodyParserProvider, } from "../providers/ServerBodyParserProvider.ts"; describe("ServerBodyParserProvider", () => { it("should parse JSON body", async ({ expect }) => { const alepha = Alepha.create(); class TestApp { json = $action({ schema: { body: z.object({ message: z.text(), }), response: z.object({ received: z.text(), }), }, handler: ({ body }) => ({ received: body.message }), }); } const app = alepha.inject(TestApp); await alepha.start(); expect( await app.json.run({ body: { message: "Hello, World!" }, }), ).toEqual({ received: "Hello, World!", }); expect(app.json.getBodyContentType()).toBe("application/json"); }); it("should parse text body", async ({ expect }) => { const alepha = Alepha.create(); class TestApp { text = $action({ schema: { body: z.text(), response: z.object({ received: z.text(), }), }, handler: ({ body }) => ({ received: body }), }); } const app = alepha.inject(TestApp); await alepha.start(); expect( await app.text.run({ body: "Hello, World!", }), ).toEqual({ received: "Hello, World!", }); expect(app.text.getBodyContentType()).toBe("text/plain"); }); it("should reject payload exceeding size limit", async ({ expect }) => { const alepha = Alepha.create(); class TestApp { test = $action({ schema: { body: z.object({ message: z.text({ maxLength: 1_000_000, }), }), response: z.object({ received: z.text(), }), }, handler: ({ body }) => { return { received: body.message }; }, }); } const app = alepha.inject(TestApp); await alepha.start(); expect( await app.test .fetch({ body: { message: "A".repeat(1_000_000) }, }) .catch((e) => HttpError.toJSON(e)), ).toEqual({ error: "PayloadTooLargeError", status: 413, message: "Request body size limit exceeded", requestId: expect.any(String), }); }); it("should decompress gzip body", async ({ expect }) => { const alepha = Alepha.create(); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const original = JSON.stringify({ hello: "world" }); const compressed = gzipSync(original); const stream = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(compressed)); controller.close(); }, }); const result = await parser.parseJson(stream, "gzip"); expect(result).toEqual({ hello: "world" }); }); it("should decompress deflate body", async ({ expect }) => { const alepha = Alepha.create(); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const original = JSON.stringify({ deflated: true }); const compressed = deflateSync(original); const stream = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(compressed)); controller.close(); }, }); const result = await parser.parseJson(stream, "deflate"); expect(result).toEqual({ deflated: true }); }); it("should decompress brotli body", async ({ expect }) => { const alepha = Alepha.create(); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const original = JSON.stringify({ brotli: true }); const compressed = brotliCompressSync(original); const makeStream = () => new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(compressed)); controller.close(); }, }); const result = await parser.parseJson(makeStream(), "br"); expect(result).toEqual({ brotli: true }); // "brotli" is not a valid encoding name — only "br" is await expect( parser.parseJson(makeStream(), "brotli" as string), ).rejects.toThrowError("Unsupported Content-Encoding: brotli"); }); it("should collect all decompressed chunks for large payloads", async ({ expect, }) => { const alepha = Alepha.create(); alepha.store.mut(bodyParserOptions, () => ({ inflate: true, limit: 200_000, })); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); // Generate a payload large enough that zlib may emit multiple chunks const original = JSON.stringify({ data: "X".repeat(50_000) }); const compressed = gzipSync(original); const stream = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(compressed)); controller.close(); }, }); const result = (await parser.parseJson(stream, "gzip")) as { data: string }; expect(result.data).toBe("X".repeat(50_000)); }); it("should reject compressed body when inflate is disabled", async ({ expect, }) => { const alepha = Alepha.create(); alepha.store.mut(bodyParserOptions, () => ({ inflate: false, limit: 100_000, })); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const compressed = gzipSync("hello"); const stream = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(compressed)); controller.close(); }, }); await expect(parser.parseText(stream, "gzip")).rejects.toThrowError( "Content-Encoding gzip not allowed", ); }); it("should reject unsupported Content-Encoding with 415", async ({ expect, }) => { const alepha = Alepha.create(); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("data")); controller.close(); }, }); const error = await parser.parseText(stream, "zstd").catch((e) => e); expect(error).toBeInstanceOf(HttpError); expect(error.status).toBe(415); }); it("should parse url-encoded body with multi-value keys", async ({ expect, }) => { const alepha = Alepha.create(); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const body = "name=Alice&tag=a&tag=b&tag=c"; const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(body)); controller.close(); }, }); const result = await parser.parseUrlEncoded(stream); expect(result).toEqual({ name: "Alice", tag: ["a", "b", "c"], }); }); it("should parse url-encoded body with single values", async ({ expect }) => { const alepha = Alepha.create(); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const body = "foo=bar&baz=qux"; const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode(body)); controller.close(); }, }); const result = await parser.parseUrlEncoded(stream); expect(result).toEqual({ foo: "bar", baz: "qux", }); }); it("should pass through identity encoding", async ({ expect }) => { const alepha = Alepha.create(); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("plain text")); controller.close(); }, }); const result = await parser.parseText(stream, "identity"); expect(result).toBe("plain text"); }); it("should return undefined for missing content-type", async ({ expect }) => { const alepha = Alepha.create(); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("data")); controller.close(); }, }); const result = await parser.parse(stream, {}, z.object({})); expect(result).toBeUndefined(); }); it("should cancel stream reader when body exceeds limit", async ({ expect, }) => { const alepha = Alepha.create(); alepha.store.mut(bodyParserOptions, () => ({ inflate: true, limit: 10, })); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); let readerCancelled = false; // Stream must stay open (not closed) so cancel() is invocable const stream = new ReadableStream({ pull(controller) { controller.enqueue(new TextEncoder().encode("A".repeat(20))); }, cancel() { readerCancelled = true; }, }); await expect(parser.parseText(stream)).rejects.toThrowError( "Request body size limit exceeded", ); expect(readerCancelled).toBe(true); }); it("should return undefined for unsupported content-type", async ({ expect, }) => { const alepha = Alepha.create(); const parser = alepha.inject(ServerBodyParserProvider); await alepha.start(); const stream = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("data")); controller.close(); }, }); const result = await parser.parse( stream, { "content-type": "application/octet-stream" }, z.object({}), ); expect(result).toBeUndefined(); }); });