1
0

fetch.test.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { expect } from "bun:test"
  2. import { Effect } from "effect"
  3. import { it } from "../../core/test/lib/effect"
  4. import { ServerFetch } from "../src/fetch"
  5. const options = {
  6. app: { version: "test-version" },
  7. database: { path: ":memory:" },
  8. fs: { filewatcher: false },
  9. } as const
  10. it.live("serves the HttpApi and enforces Basic auth like the Node server", () =>
  11. Effect.gen(function* () {
  12. const handler = yield* ServerFetch.make({ ...options, password: "secret" })
  13. const denied = yield* Effect.promise(() => handler(new Request("http://opencode.local/api/health")))
  14. expect(denied.status).toBe(401)
  15. const response = yield* Effect.promise(() =>
  16. handler(
  17. new Request("http://opencode.local/api/health", {
  18. headers: { authorization: `Basic ${btoa("opencode:secret")}` },
  19. }),
  20. ),
  21. )
  22. expect(response.status).toBe(200)
  23. const body: unknown = yield* Effect.promise(() => response.json())
  24. if (typeof body !== "object" || body === null) throw new Error("Expected a health response object")
  25. expect((body as Record<string, unknown>)["healthy"]).toBe(true)
  26. }).pipe(Effect.scoped),
  27. )
  28. it.live("serves unauthenticated and answers CORS preflight when no password is configured", () =>
  29. Effect.gen(function* () {
  30. const handler = yield* ServerFetch.make(options)
  31. const response = yield* Effect.promise(() => handler(new Request("http://opencode.local/api/health")))
  32. expect(response.status).toBe(200)
  33. const preflight = yield* Effect.promise(() =>
  34. handler(
  35. new Request("http://opencode.local/api/health", {
  36. method: "OPTIONS",
  37. headers: {
  38. origin: "http://localhost:3000",
  39. "access-control-request-method": "GET",
  40. },
  41. }),
  42. ),
  43. )
  44. expect(preflight.headers.get("access-control-allow-origin")).toBe("http://localhost:3000")
  45. }).pipe(Effect.scoped),
  46. )
  47. it.live("serves the session view operation and missing-session error", () =>
  48. Effect.gen(function* () {
  49. const handler = yield* ServerFetch.make(options)
  50. const created = yield* Effect.promise(() =>
  51. handler(
  52. new Request("http://opencode.local/api/session", {
  53. method: "POST",
  54. headers: { "content-type": "application/json" },
  55. body: "{}",
  56. }),
  57. ).then((response) => response.json()),
  58. )
  59. if (typeof created !== "object" || created === null || !("data" in created))
  60. return yield* Effect.die(new Error("Expected a session response"))
  61. const data = created.data
  62. if (typeof data !== "object" || data === null || !("id" in data) || typeof data.id !== "string")
  63. return yield* Effect.die(new Error("Expected a session ID"))
  64. const viewed = yield* Effect.promise(() =>
  65. handler(new Request(`http://opencode.local/api/session/${data.id}/view`, { method: "POST" })),
  66. )
  67. expect(viewed.status).toBe(204)
  68. const missing = yield* Effect.promise(() =>
  69. handler(new Request("http://opencode.local/api/session/ses_missing_view/view", { method: "POST" })),
  70. )
  71. expect(missing.status).toBe(404)
  72. }).pipe(Effect.scoped),
  73. )
  74. // Pins the eager-boot guarantee: the application layer is built before the handler returns, so
  75. // an aborted first request cannot interrupt layer construction and wedge every later request
  76. // (the Effect-TS/effect#6319 failure class that lazy first-request builds are prone to).
  77. it.live("stays serviceable when the first request aborts", () =>
  78. Effect.gen(function* () {
  79. const handler = yield* ServerFetch.make(options)
  80. const aborted = yield* Effect.promise(() => {
  81. const controller = new AbortController()
  82. const first = handler(new Request("http://opencode.local/api/health", { signal: controller.signal }))
  83. controller.abort()
  84. return first.then(
  85. () => "resolved" as const,
  86. () => "rejected" as const,
  87. )
  88. })
  89. expect(["resolved", "rejected"]).toContain(aborted)
  90. const second = yield* Effect.promise(() => handler(new Request("http://opencode.local/api/health")))
  91. expect(second.status).toBe(200)
  92. }).pipe(Effect.scoped),
  93. )