| 1 | /** |
| 2 | * @title Testing HttpApi implementations |
| 3 | * |
| 4 | * Test handlers through an in-memory typed client with `HttpApiTest`, without |
| 5 | * starting an HTTP server or touching a real database. |
| 6 | */ |
| 7 | import { assert, layer } from "@effect/vitest" |
| 8 | import { Effect, Layer } from "effect" |
| 9 | import { HttpClientRequest, HttpServer } from "effect/http" |
| 10 | import { HttpApiMiddleware, HttpApiTest } from "effect/http-api" |
| 11 | import { Api } from "./fixtures/api/Api.ts" |
| 12 | import { Authorization } from "./fixtures/api/Authorization.ts" |
| 13 | import { UserId } from "./fixtures/domain/User.ts" |
| 14 | import { AuthorizationLayer } from "./fixtures/server/Authorization.ts" |
| 15 | import { Users } from "./fixtures/server/Users.ts" |
| 16 | import { UsersApiHandlersNoDeps } from "./fixtures/server/Users/http.ts" |
| 17 | |
| 18 | // Provide the handlers with the in-memory `Users` implementation, so the full |
| 19 | // HTTP pipeline is exercised without any SQL. The Authorization middleware is |
| 20 | // provided with `Layer.provideMerge`, because the HTTP pipeline also resolves |
| 21 | // it when the routes are built. |
| 22 | const HandlersLayer = UsersApiHandlersNoDeps.pipe( |
| 23 | Layer.provide(Users.layerMemory), |
| 24 | Layer.provideMerge(AuthorizationLayer) |
| 25 | ) |
| 26 | |
| 27 | // The client-side Authorization middleware supplies the bearer token. |
| 28 | // Providing different middleware implementations lets the tests cover both |
| 29 | // authorized and unauthorized requests. |
| 30 | const AuthorizationMiddlewareGood = HttpApiMiddleware.layerClient( |
| 31 | Authorization, |
| 32 | ({ next, request }) => next(HttpClientRequest.bearerToken(request, "dev-token")) |
| 33 | ) |
| 34 | |
| 35 | const AuthorizationMiddlewareBad = HttpApiMiddleware.layerClient( |
| 36 | Authorization, |
| 37 | // Forward the request without attaching a token |
| 38 | ({ next, request }) => next(request) |
| 39 | ) |
| 40 | |
| 41 | // `HttpApiTest.groups` builds a typed client wired directly to the handlers of |
| 42 | // the selected groups, using the same request encoding, routing, and response |
| 43 | // decoding as a real server. |
| 44 | const makeClient = HttpApiTest.groups(Api, ["users"]) |
| 45 | |
| 46 | // `HttpServer.layerServices` provides the platform services the HTTP pipeline |
| 47 | // needs in tests. |
| 48 | layer(Layer.mergeAll(HandlersLayer, HttpServer.layerServices))("UsersApi", (it) => { |
| 49 | it.effect("lists, fetches, and creates users", () => |
| 50 | Effect.gen(function*() { |
| 51 | const client = yield* makeClient |
| 52 | |
| 53 | const created = yield* client.users.create({ |
| 54 | payload: { name: "Alice", email: "alice@acme.dev" } |
| 55 | }) |
| 56 | assert.strictEqual(created.name, "Alice") |
| 57 | |
| 58 | const fetched = yield* client.users.getById({ |
| 59 | params: { id: created.id } |
| 60 | }) |
| 61 | assert.deepStrictEqual(fetched, created) |
| 62 | |
| 63 | const all = yield* client.users.list({ query: {} }) |
| 64 | assert.isTrue(all.some((user) => user.id === created.id)) |
| 65 | }).pipe(Effect.provide(AuthorizationMiddlewareGood))) |
| 66 | |
| 67 | it.effect("returns a 404 for a missing user", () => |
| 68 | Effect.gen(function*() { |
| 69 | const client = yield* makeClient |
| 70 | |
| 71 | // Use Effect.flip to assert on the error channel |
| 72 | const error = yield* client.users.getById({ |
| 73 | params: { id: UserId.make("019845e1-682f-4b02-a706-3b2422d13aec") } |
| 74 | }).pipe(Effect.flip) |
| 75 | assert.strictEqual(error._tag, "UserNotFound") |
| 76 | }).pipe(Effect.provide(AuthorizationMiddlewareGood))) |
| 77 | |
| 78 | it.effect("rejects requests without a valid bearer token", () => |
| 79 | Effect.gen(function*() { |
| 80 | const client = yield* makeClient |
| 81 | |
| 82 | const error = yield* client.users.list({ query: {} }).pipe(Effect.flip) |
| 83 | assert.strictEqual(error._tag, "Unauthorized") |
| 84 | }).pipe(Effect.provide(AuthorizationMiddlewareBad))) |
| 85 | |
| 86 | it.effect("rejects requests with an invalid bearer token", () => |
| 87 | Effect.gen(function*() { |
| 88 | const client = yield* makeClient |
| 89 | |
| 90 | const error = yield* client.users.getById({ |
| 91 | params: { id: UserId.make("019845e1-682f-4b02-a706-3b2422d13aec") } |
| 92 | }).pipe(Effect.flip) |
| 93 | assert.strictEqual(error._tag, "Unauthorized") |
| 94 | }).pipe( |
| 95 | Effect.provide(HttpApiMiddleware.layerClient( |
| 96 | Authorization, |
| 97 | ({ next, request }) => next(HttpClientRequest.bearerToken(request, "wrong-token")) |
| 98 | )) |
| 99 | )) |
| 100 | }) |