1import * as Alchemy from "alchemy";
2import * as Cloudflare from "alchemy/Cloudflare";
3import * as Test from "alchemy/Test/Alchemy";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import AuthWorker from "./fixtures/auth-worker.ts";
7import {
8 AuthHttpError,
9 edgeRetry,
10 getJson,
11 postJson,
12 toCookieHeader,
13} from "../http.ts";
14
15const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
16 providers: Cloudflare.providers(),
17});
18
19const Stack = Alchemy.Stack(
20 "BetterAuthD1TestStack",
21 { providers: Cloudflare.providers(), state: Cloudflare.state() },
22 Effect.gen(function* () {
23 const worker = yield* AuthWorker;
24 return { url: worker.url.as<string>() };
25 }),
26);
27
28const stack = beforeAll(deploy(Stack));
29afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
30
31test(
32 "auto-migrated D1 worker: sign-up, sign-in, session",
33 Effect.gen(function* () {
34 const { url } = yield* stack;
35 const email = "d1-user@example.com";
36 const password = "password1234";
37
38 // Deploy already ran the schema migration Action — sign-up proves the
39 // tables exist. A leftover user from an interrupted prior run (with
40 // NO_DESTROY) is fine: fall through to sign-in. The retry also rides
41 // out workers.dev subdomain propagation (Cloudflare's 404 HTML page).
42 yield* postJson(`${url}/auth/sign-up/email`, {
43 email,
44 password,
45 name: "D1 User",
46 }).pipe(
47 Effect.filterOrFail(
48 (response) =>
49 response.status === 200 ||
50 response.body.includes("USER_ALREADY_EXISTS"),
51 (response) => new AuthHttpError({ url, ...response }),
52 ),
53 edgeRetry,
54 );
55
56 const signIn = yield* postJson(`${url}/auth/sign-in/email`, {
57 email,
58 password,
59 }).pipe(
60 Effect.filterOrFail(
61 (response) => response.status === 200,
62 (response) => new AuthHttpError({ url, ...response }),
63 ),
64 );
65 expect(signIn.setCookies.length).toBeGreaterThan(0);
66 const cookie = toCookieHeader(signIn.setCookies);
67
68 // session round-trip through the worker's getSession route
69 const me = yield* getJson<{ email: string | null }>(`${url}/me`, {
70 cookie,
71 });
72 expect(me.email).toBe(email);
73
74 // anonymous request resolves a null session, not an error
75 const anonymous = yield* getJson<{ email: string | null }>(`${url}/me`);
76 expect(anonymous.email).toBeNull();
77 }),
78 {
79 tags: [
80 "provider:cloudflare",
81 "provider:cloudflare:d1",
82 "provider:cloudflare:worker",
83 "live",
84 ],
85 timeout: 120_000,
86 },
87);
88
89test(
90 "second deploy is a no-op for the migration Action",
91 Effect.gen(function* () {
92 // beforeAll deployed once; deploying again must succeed quickly with
93 // the migration Action's input hash unchanged (no re-migration, no
94 // failure on existing tables).
95 const { url } = yield* deploy(Stack);
96 const me = yield* getJson<{ email: string | null }>(`${url}/me`).pipe(
97 edgeRetry,
98 );
99 expect(me.email).toBeNull();
100 }),
101 {
102 tags: [
103 "provider:cloudflare",
104 "provider:cloudflare:d1",
105 "provider:cloudflare:worker",
106 "live",
107 ],
108 timeout: 120_000,
109 },
110);