httpapi-instance-route-auth.test.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. import { afterEach, describe, expect, test } from "bun:test"
  2. import { ConfigProvider, Layer } from "effect"
  3. import { HttpRouter } from "effect/unstable/http"
  4. import { EventPaths } from "../../src/server/routes/instance/httpapi/groups/event"
  5. import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
  6. import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
  7. import { ServerAuth } from "../../src/server/auth"
  8. import { PtyID } from "@opencode-ai/core/pty/schema"
  9. import { resetDatabase } from "../fixture/db"
  10. import { disposeAllInstances, tmpdir } from "../fixture/fixture"
  11. import * as Log from "@opencode-ai/core/util/log"
  12. void Log.init({ print: false })
  13. function app(input: { password?: string; username?: string }) {
  14. const handler = HttpRouter.toWebHandler(
  15. HttpApiApp.routes.pipe(
  16. Layer.provide(
  17. ConfigProvider.layer(
  18. ConfigProvider.fromUnknown({
  19. OPENCODE_SERVER_PASSWORD: input.password,
  20. OPENCODE_SERVER_USERNAME: input.username,
  21. }),
  22. ),
  23. ),
  24. ),
  25. { disableLogger: true },
  26. ).handler
  27. return {
  28. fetch: (request: Request) => handler(request, HttpApiApp.context),
  29. request(input: string | URL | Request, init?: RequestInit) {
  30. return this.fetch(input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init))
  31. },
  32. }
  33. }
  34. function basic(username: string, password: string) {
  35. return ServerAuth.header({ username, password }) ?? ""
  36. }
  37. async function cancelBody(response: Response) {
  38. await response.body?.cancel().catch(() => {})
  39. }
  40. afterEach(async () => {
  41. await disposeAllInstances()
  42. await resetDatabase()
  43. })
  44. describe("HttpApi instance route authorization", () => {
  45. test("requires configured auth before opening the instance event stream", async () => {
  46. await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
  47. const server = app({ password: "secret" })
  48. const headers = { "x-opencode-directory": tmp.path }
  49. const missing = await server.request(EventPaths.event, { headers })
  50. await cancelBody(missing)
  51. expect(missing.status).toBe(401)
  52. const authed = await server.request(EventPaths.event, {
  53. headers: { ...headers, authorization: basic("opencode", "secret") },
  54. })
  55. await cancelBody(authed)
  56. expect(authed.status).toBe(200)
  57. })
  58. test("requires configured auth before resolving the PTY websocket route", async () => {
  59. await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
  60. const server = app({ password: "secret" })
  61. const route = PtyPaths.connect.replace(":ptyID", PtyID.ascending())
  62. const headers = { "x-opencode-directory": tmp.path }
  63. const missing = await server.request(route, { headers })
  64. await cancelBody(missing)
  65. expect(missing.status).toBe(401)
  66. const authed = await server.request(route, {
  67. headers: { ...headers, authorization: basic("opencode", "secret") },
  68. })
  69. await cancelBody(authed)
  70. expect(authed.status).toBe(404)
  71. })
  72. })