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