UNPKG

@typespec/http-server-js

Version:

TypeSpec HTTP server code generator for JavaScript

170 lines (147 loc) 4.44 kB
import { EventEmitter } from "stream"; import { assert, describe, it } from "vitest"; import { createMultipartReadable } from "../src/helpers/multipart.js"; import type * as http from "node:http"; interface StringChunkOptions { sizeConstraint: [number, number]; timeConstraintMs: [number, number]; } function chunkString(s: string, options: StringChunkOptions): EventEmitter { const [min, max] = options.sizeConstraint; const [minTime, maxTime] = options.timeConstraintMs; const emitter = new EventEmitter(); let i = 0; function emitChunk() { const chunkSize = Math.floor(Math.random() * (max - min + 1) + min); emitter.emit("data", Buffer.from(s.slice(i, i + chunkSize))); i += chunkSize; } setTimeout( function tick() { emitChunk(); if (i < s.length) { setTimeout(tick, Math.floor(Math.random() * (maxTime - minTime + 1) + minTime)); } else { emitter.emit("end"); } }, Math.floor(Math.random() * (maxTime - minTime + 1) + minTime), ); return emitter; } const exampleMultipart = [ "This is the preamble text. It should be ignored.", "--boundary", 'Content-Disposition: form-data; name="field1"', "Content-Type: application/json", "", '"value1"', "--boundary", 'Content-Disposition: form-data; name="field2"', "", "value2", "--boundary--", ].join("\r\n"); function createMultipartRequestLike( text: string, boundary: string = "boundary", ): http.IncomingMessage { return Object.assign( chunkString(text, { sizeConstraint: [40, 90], timeConstraintMs: [20, 30] }), { headers: { "content-type": `multipart/form-data; boundary=${boundary}` }, }, ) as any; } describe("multipart", () => { it("correctly chunks multipart data", async () => { const request = createMultipartRequestLike(exampleMultipart); const stream = createMultipartReadable(request); const parts: Array<{ headers: { [k: string]: string | undefined }; body: string }> = []; for await (const part of stream) { parts.push({ headers: part.headers, body: await (async () => { const chunks = []; for await (const chunk of part.body) { chunks.push(chunk); } return Buffer.concat(chunks).toString(); })(), }); } assert.deepStrictEqual(parts, [ { headers: { "content-disposition": 'form-data; name="field1"', "content-type": "application/json", }, body: '"value1"', }, { headers: { "content-disposition": 'form-data; name="field2"' }, body: "value2", }, ]); }); it("detects missing boundary", () => { assert.throws(() => { createMultipartReadable({ headers: {} } as any); }, "missing boundary"); assert.throws(() => { createMultipartReadable({ headers: { "content-type": "multipart/form-data" }, } as any); }, "missing boundary"); }); it("detects unexpected termination", async () => { const request = createMultipartRequestLike( [ "--boundary", 'Content-Disposition: form-data; name="field1"', "Content-Type: application/json", "", '"value1"', "--boundary asdf asdf", ].join("\r\n"), ); const stream = createMultipartReadable(request); try { for await (const part of stream) { for await (const _ of part.body) { // Do nothing } } assert.fail(); } catch (e) { assert.equal((e as Error).message, "Unexpected characters after final boundary."); } }); it("detects invalid preamble text", async () => { const request = createMultipartRequestLike( [ "This is the preamble text. It should be ignored.--boundary", 'Content-Disposition: form-data; name="field1"', "Content-Type: application/json", "", '"value1"', "--boundary", 'Content-Disposition: form-data; name="field2"', "", "value2", "--boundary--", ].join("\r\n"), ); const stream = createMultipartReadable(request); try { for await (const part of stream) { for await (const _ of part.body) { // Do nothing } } assert.fail(); } catch (e) { assert.equal((e as Error).message, "Invalid preamble in multipart body."); } }); });