Error statuses follow one rule: APIException takes a status and derives the code from it (BAD_REQUEST 400, UNAUTHORIZED 401, FORBIDDEN 403, NOT_FOUND 404, CONFLICT 409, RATE_LIMITED 429, SERVER_ERROR 500, BAD_GATEWAY 502), so a code can't contradict its status. That fixed real bugs: a wrong current password on change-password answered 401, on which authFetch signs the user out; editing or deleting someone else's comment or chat message answered 401 rather than 403; one invalid dump edit answered 422. A taken username is 409, like a taken email. Along the way: the database is created from schema.sql by connection.ts when the file doesn't exist, so every task works on a fresh checkout (dev and serve never ran db/init.ts, which is gone); the startup purge compared ISO dates with SQLite's datetime() as text and kept anything from the current UTC day, and now compares ISO cutoffs; the seeded admin's hash comes from lib/password.ts (split out of jwt.ts, which needs the JWT secret) rather than a copy of its format; avatar, thumbnail and playlist-cover uploads go through replaceUploadedFile, which writes beside the old file and renames over it before the record announces it (a failed avatar write lost the old avatar; a thumbnail was announced before it existed); upload.ts derives its size message from MAX_IMAGE_SIZE_BYTES and stops re-exporting the upload folders; ALLOWED_IMAGE_MIMES, which duplicated detectImageMime, is gone; the SPA fallback no longer calls next() after answering; a reply must answer a comment on the same dump (a foreign or unknown parent was a 500 or a cross-dump thread). 211 backend and 628 frontend tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
import { BASE_URL } from "../helpers.ts";
|
|
import { assertEquals } from "@std/assert";
|
|
import { Application } from "@oak/oak";
|
|
import { errorMiddleware } from "../../middleware/error.ts";
|
|
import { APIException } from "../../lib/api-exception.ts";
|
|
|
|
async function handle(
|
|
app: Application,
|
|
path: string,
|
|
headers: HeadersInit = {},
|
|
) {
|
|
const res = await app.handle(new Request(`${BASE_URL}${path}`, { headers }));
|
|
if (!res) throw new Error("no response");
|
|
return { res, text: await res.text() };
|
|
}
|
|
|
|
Deno.test("errorMiddleware: APIExceptions keep their status, anything else is a bare 500", async () => {
|
|
const app = new Application();
|
|
app.use(errorMiddleware);
|
|
app.use((ctx) => {
|
|
if (ctx.request.url.pathname === "/api-error") {
|
|
throw new APIException(404, "Nope");
|
|
}
|
|
throw new Error("secret internals");
|
|
});
|
|
const known = await handle(app, "/api-error");
|
|
assertEquals(known.res.status, 404);
|
|
assertEquals(JSON.parse(known.text), {
|
|
success: false,
|
|
error: { code: "NOT_FOUND", message: "Nope" },
|
|
});
|
|
const unknown = await handle(app, "/boom");
|
|
assertEquals(unknown.res.status, 500);
|
|
assertEquals(JSON.parse(unknown.text).error, {
|
|
code: "SERVER_ERROR",
|
|
message: "Unexpected server error",
|
|
});
|
|
});
|