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

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