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