1import * as Alchemy from "alchemy";
2import * as Neon from "alchemy/Neon";
3import * as Test from "alchemy/Test/Bun";
4import { expect } from "bun:test";
5import * as Console from "effect/Console";
6import * as Effect from "effect/Effect";
7import * as HttpClient from "effect/http/HttpClient";
8import * as FetchHttpClient from "effect/http/FetchHttpClient";
9import Stack from "../alchemy.run.ts";
10
11const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
12 providers: Neon.providers(),
13 state: Alchemy.localState(),
14 profile: process.env.ALCHEMY_PROFILE,
15});
16
17const stack = beforeAll(
18 destroy(Stack).pipe(Effect.andThen(deploy(Stack)), Effect.tap(Console.log)),
19 {
20 timeout: 120_000,
21 },
22);
23afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), { timeout: 120_000 });
24
25const base = Effect.map(stack, ({ url }) => {
26 if (!url) throw new Error("Expected a deployed website URL");
27 return String(url).replace(/\/+$/, "");
28});
29
30test(
31 "streams route output and serves image and redirect paths",
32 Effect.gen(function* () {
33 const url = yield* base;
34 const stream = yield* Test.getWhenReady(`${url}/api/stream`);
35 expect(stream.status).toBe(200);
36 expect(yield* stream.text).toBe("data: first\n\ndata: second\n\n");
37 const image = yield* Test.getWhenReady(`${url}/logo.svg`);
38 expect(image.headers["content-type"]).toContain("image/svg+xml");
39 const redirect = yield* HttpClient.get(`${url}/redirect`).pipe(
40 Effect.provideService(FetchHttpClient.RequestInit, {
41 redirect: "manual",
42 }),
43 );
44 expect(redirect.status).toBe(307);
45 expect(redirect.headers.location).toBe(`${url}/?redirected=yes`);
46 const missing = yield* HttpClient.get(`${url}/not-a-real-page`);
47 expect(missing.status).toBe(404);
48 }),
49 { timeout: 120_000 },
50);
51
52test(
53 "serves the rendered home page",
54 Effect.gen(function* () {
55 const url = yield* base;
56 const response = yield* Test.getWhenReady(url);
57 expect(response.status).toBe(200);
58 expect(yield* response.text).toContain("Hello from Next.js on Neon!");
59 }),
60 { timeout: 120_000 },
61);
62
63test(
64 "serves a static asset without falling back to HTML",
65 Effect.gen(function* () {
66 const url = yield* base;
67 const response = yield* Test.getWhenReady(`${url}/example.json`);
68 expect(response.status).toBe(200);
69 expect(yield* response.json).toEqual({
70 framework: "Next.js",
71 greeting: "Hello from Next.js on Neon!",
72 });
73 }),
74 { timeout: 120_000 },
75);
76
77test(
78 "serves a request-dependent endpoint with runtime environment",
79 Effect.gen(function* () {
80 const url = yield* base;
81 for (const name of ["Neon", "Alchemy"]) {
82 const response = yield* Test.getWhenReady(
83 `${url}/api/hello?name=${name}`,
84 );
85 expect(response.status).toBe(200);
86 expect(yield* response.json).toEqual({
87 name,
88 greeting: "Hello from Next.js on Neon!",
89 });
90 }
91 }),
92 { timeout: 120_000 },
93);