1import * as Alchemy from "alchemy";
2import * as Hetzner from "alchemy/Hetzner";
3import * as Neon from "alchemy/Neon";
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 Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientRequest from "effect/http/HttpClientRequest";
11import Stack from "../alchemy.run.ts";
12
13const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
14 providers: Layer.mergeAll(Hetzner.providers(), Neon.providers()),
15 state: Alchemy.localState(),
16 profile: process.env.ALCHEMY_PROFILE,
17});
18
19const executeOk = (request: HttpClientRequest.HttpClientRequest) =>
20 Effect.gen(function* () {
21 const client = yield* HttpClient.HttpClient;
22 return yield* client.execute(request).pipe(
23 Effect.flatMap((res) =>
24 res.status === 200 || res.status === 201
25 ? Effect.succeed(res)
26 : Effect.fail(new Error(`HTTP ${res.status}`)),
27 ),
28 Effect.retry({
29 schedule: Schedule.spaced("2 seconds"),
30 times: 30,
31 }),
32 );
33 });
34
35const stack = beforeAll(deploy(Stack), { timeout: 240_000 });
36
37afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), {
38 timeout: 180_000,
39});
40
41test(
42 "SPA renders the Notes heading",
43 Effect.gen(function* () {
44 const { url } = yield* stack;
45 if (!url) throw new Error("expected the site to expose a url");
46 const base = String(url).replace(/\/+$/, "");
47 const response = yield* executeOk(HttpClientRequest.get(`${base}/`));
48 const body = yield* response.text;
49 expect(body).toContain("Notes — Hetzner");
50 }),
51 { timeout: 180_000 },
52);
53
54test(
55 "API persists a note in Postgres",
56 Effect.gen(function* () {
57 const { apiUrl } = yield* stack;
58 if (!apiUrl) throw new Error("expected the API url");
59 const base = String(apiUrl).replace(/\/+$/, "");
60 const marker = `hetzner-note-${crypto.randomUUID()}`;
61 yield* executeOk(
62 HttpClientRequest.post(`${base}/notes`).pipe(
63 HttpClientRequest.bodyJsonUnsafe({ body: marker }),
64 ),
65 );
66 const listed = yield* executeOk(HttpClientRequest.get(`${base}/notes`));
67 const payload = (yield* listed.json) as {
68 notes: Array<{ body: string }>;
69 };
70 expect(payload.notes.some((note) => note.body === marker)).toBe(true);
71 }),
72 { timeout: 180_000 },
73);