use-api.test.ts 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. import { describe, expect, it, vi } from "vitest";
  2. import { buildApiUrl, deriveInvalidationPaths, fetchJson } from "./use-api";
  3. describe("buildApiUrl", () => {
  4. it("returns null for blank paths so callers can skip requests", () => {
  5. expect(buildApiUrl("")).toBeNull();
  6. expect(buildApiUrl(" ")).toBeNull();
  7. });
  8. it("prefixes api paths once", () => {
  9. expect(buildApiUrl("/books")).toBe("/api/books");
  10. expect(buildApiUrl("books")).toBe("/api/books");
  11. expect(buildApiUrl("/api/books")).toBe("/api/books");
  12. });
  13. });
  14. describe("fetchJson", () => {
  15. it("surfaces API error payloads on non-ok responses", async () => {
  16. const fetchImpl = vi.fn(async () =>
  17. new Response(JSON.stringify({ error: "Bad request" }), {
  18. status: 400,
  19. headers: { "Content-Type": "application/json" },
  20. }),
  21. );
  22. await expect(fetchJson("/books", {}, { fetchImpl })).rejects.toThrow("Bad request");
  23. });
  24. it("falls back to status text when the body is not JSON", async () => {
  25. const fetchImpl = vi.fn(async () =>
  26. new Response("boom", {
  27. status: 500,
  28. statusText: "Internal Server Error",
  29. headers: { "Content-Type": "text/plain" },
  30. }),
  31. );
  32. await expect(fetchJson("/books", {}, { fetchImpl })).rejects.toThrow("500 Internal Server Error");
  33. });
  34. it("surfaces nested api error messages from structured error payloads", async () => {
  35. const fetchImpl = vi.fn(async () =>
  36. new Response(JSON.stringify({ error: { code: "INVALID_BOOK_ID", message: "Invalid book ID: ../bad" } }), {
  37. status: 400,
  38. headers: { "Content-Type": "application/json" },
  39. }),
  40. );
  41. await expect(fetchJson("/books/../bad", {}, { fetchImpl })).rejects.toThrow("Invalid book ID: ../bad");
  42. });
  43. });
  44. describe("deriveInvalidationPaths", () => {
  45. it("refreshes book collections after creating a book", () => {
  46. expect(deriveInvalidationPaths("/books/create")).toEqual(["/api/books"]);
  47. });
  48. it("refreshes both collections and the current book after book mutations", () => {
  49. expect(deriveInvalidationPaths("/books/demo/write-next")).toEqual([
  50. "/api/books",
  51. "/api/books/demo",
  52. ]);
  53. expect(deriveInvalidationPaths("/books/demo/chapters/3/approve")).toEqual([
  54. "/api/books",
  55. "/api/books/demo",
  56. ]);
  57. });
  58. it("refreshes daemon state after daemon mutations", () => {
  59. expect(deriveInvalidationPaths("/daemon/start")).toEqual(["/api/daemon"]);
  60. expect(deriveInvalidationPaths("/daemon/stop")).toEqual(["/api/daemon"]);
  61. });
  62. it("refreshes project data after project mutations", () => {
  63. expect(deriveInvalidationPaths("/project")).toEqual(["/api/project"]);
  64. expect(deriveInvalidationPaths("/project/language")).toEqual(["/api/project", "/api/project/language"]);
  65. });
  66. });