1import * as AWS from "alchemy/AWS";
2import * as Neon from "alchemy/Neon";
3import * as Core from "alchemy/Test/Core";
4import * as Test from "alchemy/Test/Alchemy";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Schedule from "effect/Schedule";
9import AuthFunctionLive, { AuthFunction } from "./fixtures/auth-handler.ts";
10import { AuthHttpError, getJson, postJson, toCookieHeader } from "../http.ts";
11
12const testOptions = {
13 providers: Layer.mergeAll(AWS.providers(), Neon.providers()),
14};
15const { test, beforeAll, afterAll } = Test.make(testOptions);
16const sharedStack = Core.scratchStack(testOptions, "BetterAuthLambda");
17
18let baseUrl: string;
19
20// Lambda function URL cold-start can take well over 60s on a fresh
21// deploy — poll readiness before the actual assertions.
22const readinessRetry = Effect.retry({
23 schedule: Schedule.exponential("2 seconds", 1.5),
24 times: 10,
25});
26
27beforeAll(
28 Effect.gen(function* () {
29 yield* sharedStack.destroy();
30 const { functionUrl } = yield* sharedStack.deploy(
31 Effect.gen(function* () {
32 return yield* AuthFunction;
33 }).pipe(Effect.provide(AuthFunctionLive)),
34 );
35 expect(functionUrl).toBeTruthy();
36 baseUrl = functionUrl!.replace(/\/+$/, "");
37
38 yield* getJson<{ email: string | null }>(`${baseUrl}/me`).pipe(
39 Effect.tapError((error) =>
40 Effect.logWarning(`Lambda not ready yet: ${error.message}`),
41 ),
42 readinessRetry,
43 );
44 }),
45 { timeout: 240_000 },
46);
47
48afterAll.skipIf(!!process.env.NO_DESTROY)(sharedStack.destroy(), {
49 timeout: 120_000,
50});
51
52test(
53 "Lambda host: sign-up, sign-in, session against Neon Postgres",
54 Effect.gen(function* () {
55 const email = "lambda-user@example.com";
56 const password = "password1234";
57
58 yield* postJson(`${baseUrl}/auth/sign-up/email`, {
59 email,
60 password,
61 name: "Lambda User",
62 }).pipe(
63 Effect.filterOrFail(
64 (response) =>
65 response.status === 200 ||
66 response.body.includes("USER_ALREADY_EXISTS"),
67 (response) => new AuthHttpError({ url: baseUrl, ...response }),
68 ),
69 );
70
71 const signIn = yield* postJson(`${baseUrl}/auth/sign-in/email`, {
72 email,
73 password,
74 }).pipe(
75 Effect.filterOrFail(
76 (response) => response.status === 200,
77 (response) => new AuthHttpError({ url: baseUrl, ...response }),
78 ),
79 );
80 expect(signIn.setCookies.length).toBeGreaterThan(0);
81 const cookie = toCookieHeader(signIn.setCookies);
82
83 const me = yield* getJson<{ email: string | null }>(`${baseUrl}/me`, {
84 cookie,
85 });
86 expect(me.email).toBe(email);
87
88 const anonymous = yield* getJson<{ email: string | null }>(`${baseUrl}/me`);
89 expect(anonymous.email).toBeNull();
90 }),
91 {
92 tags: [
93 "provider:aws",
94 "provider:aws:lambda",
95 "provider:neon",
96 "provider:neon:project",
97 "live",
98 ],
99 timeout: 120_000,
100 },
101);