99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
dbUnavailable,
|
|
errorJson,
|
|
internalError,
|
|
notFound,
|
|
rateLimited,
|
|
serverMisconfigured,
|
|
unauthorized,
|
|
} from "../../lib/server/responses";
|
|
|
|
async function readBody(res: Response): Promise<{
|
|
error: { code: string; message: string };
|
|
details?: unknown;
|
|
}> {
|
|
return (await res.json()) as {
|
|
error: { code: string; message: string };
|
|
details?: unknown;
|
|
};
|
|
}
|
|
|
|
describe("lib/server/responses", () => {
|
|
it("errorJson returns the canonical shape, status, and details", async () => {
|
|
const res = errorJson("validation_error", "Bad input", 400, {
|
|
details: { field: "email" },
|
|
headers: { "x-custom": "1" },
|
|
});
|
|
expect(res.status).toBe(400);
|
|
expect(res.headers.get("x-custom")).toBe("1");
|
|
const body = await readBody(res);
|
|
expect(body).toEqual({
|
|
error: { code: "validation_error", message: "Bad input" },
|
|
details: { field: "email" },
|
|
});
|
|
});
|
|
|
|
it("errorJson omits details when not provided", async () => {
|
|
const res = errorJson("internal_error", "Boom", 500);
|
|
const body = await readBody(res);
|
|
expect(body.details).toBeUndefined();
|
|
expect(body.error).toEqual({ code: "internal_error", message: "Boom" });
|
|
});
|
|
|
|
it("dbUnavailable → 503 db_unavailable", async () => {
|
|
const res = dbUnavailable();
|
|
expect(res.status).toBe(503);
|
|
const body = await readBody(res);
|
|
expect(body.error.code).toBe("db_unavailable");
|
|
});
|
|
|
|
it("unauthorized → 401 unauthorized", async () => {
|
|
const res = unauthorized();
|
|
expect(res.status).toBe(401);
|
|
const body = await readBody(res);
|
|
expect(body.error).toEqual({
|
|
code: "unauthorized",
|
|
message: "Unauthorized",
|
|
});
|
|
});
|
|
|
|
it("notFound → 404 not_found with optional message", async () => {
|
|
const res = notFound("Rule not found");
|
|
expect(res.status).toBe(404);
|
|
const body = await readBody(res);
|
|
expect(body.error).toEqual({
|
|
code: "not_found",
|
|
message: "Rule not found",
|
|
});
|
|
});
|
|
|
|
it("rateLimited → 429 with Retry-After header (seconds, ceil) and details", async () => {
|
|
const res = rateLimited(2500);
|
|
expect(res.status).toBe(429);
|
|
expect(res.headers.get("retry-after")).toBe("3");
|
|
const body = await readBody(res);
|
|
expect(body.error.code).toBe("rate_limited");
|
|
expect(body.details).toEqual({ retryAfterMs: 2500 });
|
|
});
|
|
|
|
it("rateLimited clamps Retry-After to at least 1 second", () => {
|
|
const res = rateLimited(0);
|
|
expect(res.headers.get("retry-after")).toBe("1");
|
|
});
|
|
|
|
it("serverMisconfigured → 500 server_misconfigured", async () => {
|
|
const res = serverMisconfigured();
|
|
expect(res.status).toBe(500);
|
|
const body = await readBody(res);
|
|
expect(body.error.code).toBe("server_misconfigured");
|
|
});
|
|
|
|
it("internalError → 500 internal_error", async () => {
|
|
const res = internalError();
|
|
expect(res.status).toBe(500);
|
|
const body = await readBody(res);
|
|
expect(body.error.code).toBe("internal_error");
|
|
});
|
|
});
|