CI / install-and-build (pull_request) Successful in 1m29s
CI / format (pull_request) Successful in 51s
CI / typecheck-source (pull_request) Successful in 41s
CI / typecheck-tests (pull_request) Successful in 39s
CI / test (pull_request) Successful in 51s
CI / lint (pull_request) Successful in 21s
Co-Authored-By: gpt-5.6-terra <noreply@openai.com>
147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
import { describe, it, before, after } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import Koa from "koa";
|
|
import KoaRouter from "@koa/router";
|
|
import { createServer } from "node:http";
|
|
import type { AddressInfo } from "node:net";
|
|
import { jsonBody } from "../../src/webapi/middleware/jsonBody.js";
|
|
|
|
async function request(
|
|
url: string,
|
|
opts: { method?: string; body?: unknown; contentType?: string } = {},
|
|
): Promise<{ status: number; body: unknown }> {
|
|
const method = opts.method ?? "POST";
|
|
const bodyStr =
|
|
opts.body !== undefined ? JSON.stringify(opts.body) : undefined;
|
|
const headers: Record<string, string> = {};
|
|
if (bodyStr !== undefined) {
|
|
headers["Content-Type"] = opts.contentType ?? "application/json";
|
|
headers["Content-Length"] = String(bodyStr.length);
|
|
}
|
|
const res = await fetch(url, { method, headers, body: bodyStr });
|
|
const text = await res.text();
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(text);
|
|
} catch {
|
|
parsed = text;
|
|
}
|
|
return { status: res.status, body: parsed };
|
|
}
|
|
|
|
async function makeTestServer() {
|
|
const failValidator = Object.assign((_obj: unknown): _obj is never => false, {
|
|
errors: [{ message: "required" }] as unknown[],
|
|
schema: { $id: "test-schema", title: "Test", description: "" },
|
|
});
|
|
|
|
const app = new Koa();
|
|
const router = new KoaRouter();
|
|
|
|
router.post("/echo", jsonBody(), async (ctx) => {
|
|
ctx.status = 200;
|
|
ctx.body = { ok: true, received: ctx.request.body };
|
|
});
|
|
|
|
router.post(
|
|
"/fail-validate",
|
|
jsonBody({ validate: failValidator as any }),
|
|
async (ctx) => {
|
|
ctx.status = 200;
|
|
ctx.body = { ok: true };
|
|
},
|
|
);
|
|
|
|
router.post(
|
|
"/with-params/:uid",
|
|
jsonBody({ includeParams: ["uid"] }),
|
|
async (ctx) => {
|
|
ctx.status = 200;
|
|
ctx.body = { ok: true, body: ctx.request.body };
|
|
},
|
|
);
|
|
|
|
app.use(router.routes());
|
|
app.use(router.allowedMethods());
|
|
|
|
const server = createServer(app.callback());
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const { port } = server.address() as AddressInfo;
|
|
const url = `http://127.0.0.1:${port}`;
|
|
const close = () =>
|
|
new Promise<void>((resolve, reject) =>
|
|
server.close((err) => (err ? reject(err) : resolve())),
|
|
);
|
|
return { url, close };
|
|
}
|
|
|
|
describe("jsonBody middleware", () => {
|
|
let url: string;
|
|
let closeServer: () => Promise<void>;
|
|
|
|
before(async () => {
|
|
({ url, close: closeServer } = await makeTestServer());
|
|
});
|
|
|
|
after(() => closeServer());
|
|
|
|
it("no Content-Type → 200 with empty body (bodyparser sets body to {})", async () => {
|
|
const res = await fetch(url + "/echo", { method: "POST" });
|
|
const json = await res.json();
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual(json.received, {});
|
|
});
|
|
|
|
it("invalid JSON with Content-Type: application/json → 400 from bodyparser", async () => {
|
|
const res = await fetch(url + "/echo", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: "not-valid-json",
|
|
});
|
|
assert.equal(res.status, 400);
|
|
});
|
|
|
|
it("valid JSON body → 200 with echoed body", async () => {
|
|
const res = await request(url + "/echo", { body: { hello: "world" } });
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual((res.body as any).received, { hello: "world" });
|
|
});
|
|
|
|
it("failing validator → 400 jsonchema_validation_failed with schema and errors", async () => {
|
|
const res = await request(url + "/fail-validate", {
|
|
body: { any: "thing" },
|
|
});
|
|
assert.equal(res.status, 400);
|
|
assert.equal((res.body as any).error, "jsonchema_validation_failed");
|
|
assert.ok((res.body as any).schema, "response includes schema");
|
|
assert.ok(
|
|
Array.isArray((res.body as any).errors),
|
|
"response includes errors",
|
|
);
|
|
});
|
|
|
|
it("includeParams: param absent from body → merged in", async () => {
|
|
const res = await request(url + "/with-params/user-42", {
|
|
body: { other: "field" },
|
|
});
|
|
assert.equal(res.status, 200);
|
|
assert.equal((res.body as any).body?.uid, "user-42");
|
|
});
|
|
|
|
it("includeParams: param present with matching value → ok", async () => {
|
|
const res = await request(url + "/with-params/user-42", {
|
|
body: { uid: "user-42" },
|
|
});
|
|
assert.equal(res.status, 200);
|
|
});
|
|
|
|
it("includeParams: mismatching param value → 400 mismatch_params", async () => {
|
|
const res = await request(url + "/with-params/user-42", {
|
|
body: { uid: "different-uid" },
|
|
});
|
|
assert.equal(res.status, 400);
|
|
assert.equal((res.body as any).error, "mismatch_params");
|
|
assert.equal((res.body as any).param, "uid");
|
|
});
|
|
});
|