| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- import { NodeHttpServer } from "@effect/platform-node"
- import { describe, expect } from "bun:test"
- import { Effect, Layer, Option, Schema } from "effect"
- import { HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http"
- import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup } from "effect/unstable/httpapi"
- import { ServerAuth } from "../../src/server/auth"
- import {
- Authorization,
- authorizationLayer,
- ServerAuthorization,
- serverAuthorizationLayer,
- } from "../../src/server/routes/instance/httpapi/middleware/authorization"
- import { testEffect } from "../lib/effect"
- const Api = HttpApi.make("test-authorization").add(
- HttpApiGroup.make("test")
- .add(
- HttpApiEndpoint.get("probe", "/probe", {
- success: Schema.String,
- }),
- HttpApiEndpoint.get("missing", "/missing", {
- success: Schema.String,
- error: HttpApiError.NotFound,
- }),
- )
- .middleware(Authorization),
- )
- const ServerApi = HttpApi.make("test-server-authorization").add(
- HttpApiGroup.make("test.v2")
- .add(
- HttpApiEndpoint.get("probe", "/api/probe", {
- success: Schema.String,
- }),
- )
- .middleware(ServerAuthorization),
- )
- const handlers = HttpApiBuilder.group(Api, "test", (handlers) =>
- handlers
- .handle("probe", () => Effect.succeed("ok"))
- .handle("missing", () => Effect.fail(new HttpApiError.NotFound({}))),
- )
- const serverHandlers = HttpApiBuilder.group(ServerApi, "test.v2", (handlers) =>
- handlers.handle("probe", () => Effect.succeed("ok")),
- )
- const apiLayer = HttpRouter.serve(
- HttpApiBuilder.layer(Api).pipe(Layer.provide(handlers), Layer.provide(authorizationLayer)),
- { disableListenLog: true, disableLogger: true },
- ).pipe(Layer.provideMerge(NodeHttpServer.layerTest))
- const v2ApiLayer = HttpRouter.serve(
- HttpApiBuilder.layer(ServerApi).pipe(Layer.provide(serverHandlers), Layer.provide(serverAuthorizationLayer)),
- { disableListenLog: true, disableLogger: true },
- ).pipe(Layer.provideMerge(NodeHttpServer.layerTest))
- const noAuthLayer = ServerAuth.Config.configLayer({ password: Option.none(), username: "kirincode" })
- const secretLayer = ServerAuth.Config.configLayer({ password: Option.some("secret"), username: "kirincode" })
- const kitSecretLayer = ServerAuth.Config.configLayer({ password: Option.some("secret"), username: "kit" })
- const it = testEffect(apiLayer.pipe(Layer.provide(noAuthLayer)))
- const itSecret = testEffect(apiLayer.pipe(Layer.provide(secretLayer)))
- const itKitSecret = testEffect(apiLayer.pipe(Layer.provide(kitSecretLayer)))
- const itV2Secret = testEffect(v2ApiLayer.pipe(Layer.provide(secretLayer)))
- const basic = (username: string, password: string) => ServerAuth.header({ username, password }) ?? ""
- const token = (username: string, password: string) => Buffer.from(`${username}:${password}`).toString("base64")
- const getProbe = (headers?: Record<string, string>) =>
- HttpClientRequest.get("/probe").pipe(
- headers ? HttpClientRequest.setHeaders(headers) : (request) => request,
- HttpClient.execute,
- )
- describe("HttpApi authorization middleware", () => {
- it.live("allows requests when server password is not configured", () =>
- Effect.gen(function* () {
- const response = yield* getProbe()
- expect(response.status).toBe(200)
- expect(yield* response.json).toBe("ok")
- }),
- )
- itSecret.live("requires configured password for basic auth", () =>
- Effect.gen(function* () {
- const [missing, badPassword, good] = yield* Effect.all(
- [
- getProbe(),
- getProbe({ authorization: basic("kirincode", "wrong") }),
- getProbe({ authorization: basic("kirincode", "secret") }),
- ],
- { concurrency: "unbounded" },
- )
- expect(missing.status).toBe(401)
- expect(missing.headers["www-authenticate"] ?? "").toContain("Basic")
- expect(badPassword.status).toBe(401)
- expect(badPassword.headers["www-authenticate"] ?? "").toContain("Basic")
- expect(good.status).toBe(200)
- }),
- )
- itKitSecret.live("respects configured basic auth username", () =>
- Effect.gen(function* () {
- const [defaultUser, configuredUser] = yield* Effect.all(
- [getProbe({ authorization: basic("kirincode", "secret") }), getProbe({ authorization: basic("kit", "secret") })],
- { concurrency: "unbounded" },
- )
- expect(defaultUser.status).toBe(401)
- expect(configuredUser.status).toBe(200)
- }),
- )
- itSecret.live("accepts auth token query credentials", () =>
- Effect.gen(function* () {
- const response = yield* HttpClient.get(`/probe?auth_token=${encodeURIComponent(token("kirincode", "secret"))}`)
- expect(response.status).toBe(200)
- }),
- )
- itSecret.live("prefers auth token query credentials over basic auth", () =>
- Effect.gen(function* () {
- const response = yield* HttpClientRequest.get(
- `/probe?auth_token=${encodeURIComponent(token("kirincode", "secret"))}`,
- ).pipe(HttpClientRequest.setHeader("authorization", basic("kirincode", "wrong")), HttpClient.execute)
- expect(response.status).toBe(200)
- }),
- )
- itSecret.live("preserves handler errors when basic auth succeeds", () =>
- Effect.gen(function* () {
- const response = yield* HttpClientRequest.get("/missing").pipe(
- HttpClientRequest.setHeader("authorization", basic("kirincode", "secret")),
- HttpClient.execute,
- )
- expect(response.status).toBe(404)
- }),
- )
- itSecret.live("preserves handler errors when auth token query succeeds", () =>
- Effect.gen(function* () {
- const response = yield* HttpClient.get(`/missing?auth_token=${encodeURIComponent(token("kirincode", "secret"))}`)
- expect(response.status).toBe(404)
- }),
- )
- itSecret.live("rejects malformed auth token query credentials", () =>
- Effect.gen(function* () {
- const response = yield* HttpClient.get("/probe?auth_token=not-base64")
- expect(response.status).toBe(401)
- }),
- )
- itV2Secret.live("returns bodyful v2 unauthorized errors", () =>
- Effect.gen(function* () {
- const response = yield* HttpClient.get("/api/probe")
- const body = yield* response.json
- expect(response.status).toBe(401)
- expect(response.headers["www-authenticate"] ?? "").toContain("Basic")
- expect(body).toEqual({ _tag: "UnauthorizedError", message: "Authentication required" })
- }),
- )
- })
|