1import * as Alchemy from "alchemy";
2import * as Cloudflare from "alchemy/Cloudflare";
3import * as Test from "alchemy/Test/Bun";
4import { expect } from "bun:test";
5import * as Effect from "effect/Effect";
6import * as HttpClient from "effect/http/HttpClient";
7import * as HttpClientRequest from "effect/http/HttpClientRequest";
8import Stack from "../alchemy.run.ts";
9import type { User } from "../src/Api.ts";
10
11const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
12 providers: Cloudflare.providers(),
13 state: Alchemy.localState(),
14});
15
16const stack = beforeAll(deploy(Stack));
17
18afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
19
20// Fresh `workers.dev` URLs transiently 404 while the route propagates.
21// `Test.getWhenReady` retries until the worker answers.
22const { getWhenReady } = Test;
23
24test(
25 "worker exposes a URL and database name",
26 Effect.gen(function* () {
27 const { url, databaseName } = yield* stack;
28
29 expect(url).toBeString();
30 expect(databaseName).toBeString();
31 }),
32);
33
34test(
35 "worker exposes user CRUD through @effect/sql-d1",
36 Effect.gen(function* () {
37 const { url } = yield* stack;
38 const baseUrl = url.replace(/\/+$/, "");
39
40 const initialResponse = yield* getWhenReady(baseUrl);
41 expect(initialResponse.status).toBe(200);
42
43 const initialBody = (yield* initialResponse.json) as unknown as {
44 users: User[];
45 };
46 expect(Array.isArray(initialBody.users)).toBe(true);
47
48 const createResponse = yield* HttpClient.execute(
49 HttpClientRequest.post(baseUrl),
50 );
51 expect(createResponse.status).toBe(200);
52
53 const { user: createdUser } = (yield* createResponse.json) as unknown as {
54 user: User;
55 };
56 expect(createdUser.id).toBeNumber();
57 expect(createdUser.email).toBeString();
58 expect(createdUser.name).toBeString();
59 expect(createdUser.created_at).toBeNumber();
60
61 const readResponse = yield* HttpClient.get(`${baseUrl}/${createdUser.id}`);
62 expect(readResponse.status).toBe(200);
63 const readBody = (yield* readResponse.json) as unknown as { user: User };
64 expect(readBody.user).toMatchObject({
65 id: createdUser.id,
66 email: createdUser.email,
67 name: createdUser.name,
68 });
69
70 const invalidReadResponse = yield* HttpClient.get(`${baseUrl}/not-a-user`);
71 expect(invalidReadResponse.status).toBe(400);
72 expect(yield* invalidReadResponse.json).toEqual({
73 error: "Invalid user ID",
74 });
75
76 const methodResponse = yield* HttpClient.execute(
77 HttpClientRequest.patch(baseUrl),
78 );
79 expect(methodResponse.status).toBe(405);
80 expect(yield* methodResponse.json).toEqual({
81 error: "Method not allowed",
82 });
83
84 const deleteResponse = yield* HttpClient.execute(
85 HttpClientRequest.delete(`${baseUrl}/${createdUser.id}`),
86 );
87 expect(deleteResponse.status).toBe(200);
88 const deleteBody = (yield* deleteResponse.json) as unknown as {
89 user: User;
90 };
91 expect(deleteBody.user).toMatchObject({ id: createdUser.id });
92
93 const finalResponse = yield* HttpClient.get(baseUrl);
94 expect(finalResponse.status).toBe(200);
95 const finalBody = (yield* finalResponse.json) as unknown as {
96 users: User[];
97 };
98 expect(finalBody.users.some((user) => user.id === createdUser.id)).toBe(
99 false,
100 );
101 }),
102 { timeout: 120_000 },
103);