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 */
7import { assert, layer } from "@effect/vitest"
8import { Effect, Layer } from "effect"
9import { HttpClientRequest, HttpServer } from "effect/http"
10import { HttpApiMiddleware, HttpApiTest } from "effect/http-api"
11import { Api } from "./fixtures/api/Api.ts"
12import { Authorization } from "./fixtures/api/Authorization.ts"
13import { UserId } from "./fixtures/domain/User.ts"
14import { AuthorizationLayer } from "./fixtures/server/Authorization.ts"
15import { Users } from "./fixtures/server/Users.ts"
16import { 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.
22const 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.
30const AuthorizationMiddlewareGood = HttpApiMiddleware.layerClient(
31 Authorization,
32 ({ next, request }) => next(HttpClientRequest.bearerToken(request, "dev-token"))
33)
34
35const 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.
44const makeClient = HttpApiTest.groups(Api, ["users"])
45
46// `HttpServer.layerServices` provides the platform services the HTTP pipeline
47// needs in tests.
48layer(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})