httpapi-authorization.test.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. import { NodeHttpServer } from "@effect/platform-node"
  2. import { describe, expect } from "bun:test"
  3. import { Effect, Layer, Option, Schema } from "effect"
  4. import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http"
  5. import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"
  6. import { ServerAuth } from "../../src/server/auth"
  7. import { Authorization, authorizationLayer } from "../../src/server/routes/instance/httpapi/middleware/authorization"
  8. import { testEffect } from "../lib/effect"
  9. const Api = HttpApi.make("test-authorization").add(
  10. HttpApiGroup.make("test")
  11. .add(
  12. HttpApiEndpoint.get("probe", "/probe", {
  13. success: Schema.String,
  14. }),
  15. )
  16. .middleware(Authorization),
  17. )
  18. const handlers = HttpApiBuilder.group(Api, "test", (handlers) => handlers.handle("probe", () => Effect.succeed("ok")))
  19. const apiLayer = HttpRouter.serve(
  20. HttpApiBuilder.layer(Api).pipe(Layer.provide(handlers), Layer.provide(authorizationLayer)),
  21. { disableListenLog: true, disableLogger: true },
  22. ).pipe(Layer.provideMerge(NodeHttpServer.layerTest))
  23. const noAuthLayer = ServerAuth.Config.layer({ password: Option.none(), username: "opencode" })
  24. const secretLayer = ServerAuth.Config.layer({ password: Option.some("secret"), username: "opencode" })
  25. const kitSecretLayer = ServerAuth.Config.layer({ password: Option.some("secret"), username: "kit" })
  26. const it = testEffect(apiLayer.pipe(Layer.provide(noAuthLayer)))
  27. const itSecret = testEffect(apiLayer.pipe(Layer.provide(secretLayer)))
  28. const itKitSecret = testEffect(apiLayer.pipe(Layer.provide(kitSecretLayer)))
  29. const basic = (username: string, password: string) =>
  30. `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
  31. const token = (username: string, password: string) => Buffer.from(`${username}:${password}`).toString("base64")
  32. const getProbe = (headers?: Record<string, string>) =>
  33. HttpClientRequest.get("/probe").pipe(
  34. headers ? HttpClientRequest.setHeaders(headers) : (request) => request,
  35. HttpClient.execute,
  36. )
  37. describe("HttpApi authorization middleware", () => {
  38. it.live("allows requests when server password is not configured", () =>
  39. Effect.gen(function* () {
  40. const response = yield* getProbe()
  41. expect(response.status).toBe(200)
  42. expect(yield* response.json).toBe("ok")
  43. }),
  44. )
  45. itSecret.live("requires configured password for basic auth", () =>
  46. Effect.gen(function* () {
  47. const [missing, badPassword, good] = yield* Effect.all(
  48. [
  49. getProbe(),
  50. getProbe({ authorization: basic("opencode", "wrong") }),
  51. getProbe({ authorization: basic("opencode", "secret") }),
  52. ],
  53. { concurrency: "unbounded" },
  54. )
  55. expect(missing.status).toBe(401)
  56. expect(badPassword.status).toBe(401)
  57. expect(good.status).toBe(200)
  58. }),
  59. )
  60. itKitSecret.live("respects configured basic auth username", () =>
  61. Effect.gen(function* () {
  62. const [defaultUser, configuredUser] = yield* Effect.all(
  63. [getProbe({ authorization: basic("opencode", "secret") }), getProbe({ authorization: basic("kit", "secret") })],
  64. { concurrency: "unbounded" },
  65. )
  66. expect(defaultUser.status).toBe(401)
  67. expect(configuredUser.status).toBe(200)
  68. }),
  69. )
  70. itSecret.live("accepts auth token query credentials", () =>
  71. Effect.gen(function* () {
  72. const response = yield* HttpClient.get(`/probe?auth_token=${encodeURIComponent(token("opencode", "secret"))}`)
  73. expect(response.status).toBe(200)
  74. }),
  75. )
  76. itSecret.live("rejects malformed auth token query credentials", () =>
  77. Effect.gen(function* () {
  78. const response = yield* HttpClient.get("/probe?auth_token=not-base64")
  79. expect(response.status).toBe(401)
  80. }),
  81. )
  82. })