errors.test.js 2.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. /* @vitest-environment node */
  2. import { describe, it, expect } from "vitest";
  3. import { jsonError, withErrorHandling, badRequest, json } from "./errors.js";
  4. it("json sets Cache-Control: no-store by default (RHL-006)", async () => {
  5. const res = json({ ok: true }, 200);
  6. expect(res.headers.get("Cache-Control")).toBe("no-store");
  7. });
  8. describe("lib/api/errors", () => {
  9. it("jsonError returns the standardized error shape without details", async () => {
  10. const res = jsonError(401, "AUTH_UNAUTHENTICATED", "Unauthorized");
  11. expect(res.status).toBe(401);
  12. expect(await res.json()).toEqual({
  13. error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
  14. });
  15. });
  16. it("jsonError includes details when provided", async () => {
  17. const res = jsonError(
  18. 400,
  19. "VALIDATION_MISSING_PARAM",
  20. "Missing required route parameter(s)",
  21. { params: ["branch"] }
  22. );
  23. expect(res.status).toBe(400);
  24. expect(await res.json()).toEqual({
  25. error: {
  26. message: "Missing required route parameter(s)",
  27. code: "VALIDATION_MISSING_PARAM",
  28. details: { params: ["branch"] },
  29. },
  30. });
  31. });
  32. it("withErrorHandling converts ApiError into a standardized response", async () => {
  33. // The wrapped handler throws an expected error (ApiError).
  34. // The wrapper must convert it into { error: { message, code } } with status 400.
  35. const handler = withErrorHandling(async () => {
  36. throw badRequest("VALIDATION_TEST", "Bad Request");
  37. });
  38. const res = await handler();
  39. expect(res.status).toBe(400);
  40. expect(await res.json()).toEqual({
  41. error: { message: "Bad Request", code: "VALIDATION_TEST" },
  42. });
  43. });
  44. it("withErrorHandling converts unknown errors into a safe 500 response", async () => {
  45. // Unknown errors must never leak internal messages/stacks.
  46. // We always return a generic 500 payload.
  47. const handler = withErrorHandling(async () => {
  48. throw new Error("boom");
  49. });
  50. const res = await handler();
  51. expect(res.status).toBe(500);
  52. expect(await res.json()).toEqual({
  53. error: {
  54. message: "Internal server error",
  55. code: "INTERNAL_SERVER_ERROR",
  56. },
  57. });
  58. });
  59. });