feat(tests): add unit and integration test suite (node:test)
Adds 215 tests across three tiers using node:test + node:assert/strict (no new test framework dependencies): - test/tools/ — middleware and utility tests (token, hash, authenticate, jsonBody, convertError, validators) - test/shared/ — DbInterface/BackendDbInterface contract suites reusable across backends (users, abodes, residents, apikeys, sessions, auth) - test/backends/sqlite/ — SQLite-private tests (sql builder, WrappedDb, migrator) + shared suites via SqliteInterface - test/backends/api/ — ApiInterface unit tests + shared suites via a live Koa server backed by SQLite Also fixes four bugs uncovered by the tests: - ApiInterface: path params leaked into query string (slice(1) fix) - ApiInterface: calling res.json() on 204 No Content responses - SqliteInterface.updateResident: missing comma in SET clause - WrappedBetterSqlite3Db: readonly:undefined rejected by better-sqlite3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user