httpapi-authorization.test.ts 3.7 KB

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