1import * as Alchemy from "alchemy";
2import * as Railway from "alchemy/Railway";
3import * as Test from "alchemy/Test/Bun";
4import { expect } from "bun:test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as HttpClient from "effect/http/HttpClient";
8import Stack from "../alchemy.run.ts";
9import { SECRET_NAME } from "../src/shared.ts";
10
11const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
12 providers: Railway.providers(),
13 state: Alchemy.localState(),
14 profile: process.env.ALCHEMY_PROFILE,
15});
16
17const fetchOk = (url: string) =>
18 Effect.gen(function* () {
19 const client = yield* HttpClient.HttpClient;
20 return yield* client.get(url).pipe(
21 Effect.flatMap((res) =>
22 res.status === 200
23 ? Effect.succeed(res)
24 : Effect.fail(new Error(`HTTP ${res.status}`)),
25 ),
26 Effect.retry({
27 schedule: Schedule.exponential("500 millis"),
28 times: 10,
29 }),
30 );
31 });
32
33const stack = beforeAll(deploy(Stack), { timeout: 480_000 });
34
35afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), {
36 timeout: 180_000,
37});
38
39test(
40 "deploys the full graph and serves /health",
41 Effect.gen(function* () {
42 const out = yield* stack;
43 expect(out.projectId).toBeString();
44 expect(out.environmentId).toBeString();
45 expect(out.stagingId).toBeString();
46 expect(out.stagingName).toBe("staging");
47 expect(out.secretName).toBeString();
48 expect(out.volumeId).toBeString();
49 expect(out.postgresServiceId).toBeString();
50 expect(out.postgresPublic).toBeString();
51 expect(out.mysqlServiceId).toBeString();
52 expect(out.mysqlName).toBeString();
53 expect(out.databaseUrlName).toBe("APP_DATABASE_URL");
54 expect(out.pingUrl).toMatch(/^https:\/\/.+\.up\.railway\.app$/);
55 expect(out.groupId).toBeString();
56 expect(out.redisServiceId).toBeString();
57 expect(out.redisProxy).toContain(":");
58 expect(out.bucketId).toBeString();
59 expect(out.echoUrl).toMatch(/^https:\/\/.+\.up\.railway\.app$/);
60 expect(out.apiUrl).toMatch(/^https:\/\/.+\.up\.railway\.app$/);
61 expect(out.workerServiceId).toBeString();
62 expect(out.workspaceId).toBeString();
63
64 const health = yield* fetchOk(`${out.apiUrl}/health`);
65 expect(health.status).toBe(200);
66 const body = (yield* health.json) as { rows: unknown };
67 expect(body.rows).toBeDefined();
68
69 expect(out.pingUrl).toEqual(expect.any(String));
70 const ping = yield* fetchOk(out.pingUrl!);
71 expect(ping.status).toBe(200);
72 const pingBody = (yield* ping.json) as { rows: unknown };
73 expect(pingBody.rows).toBeDefined();
74 }),
75 { timeout: 180_000 },
76);
77
78test(
79 "Variable is injected as env on /secret",
80 Effect.gen(function* () {
81 const out = yield* stack;
82 const response = yield* fetchOk(`${out.apiUrl}/secret`);
83 expect(response.status).toBe(200);
84 const body = (yield* response.json) as { ok: boolean; name: string };
85 expect(body.ok).toBe(true);
86 expect(body.name).toBe(SECRET_NAME);
87 }),
88 { timeout: 60_000 },
89);
90
91test(
92 "ReadWriteRedis round-trips a key on /redis",
93 Effect.gen(function* () {
94 const out = yield* stack;
95 const response = yield* fetchOk(`${out.apiUrl}/redis`);
96 expect(response.status).toBe(200);
97 const body = (yield* response.json) as { ok: boolean };
98 expect(body.ok).toBe(true);
99 }),
100 { timeout: 60_000 },
101);
102
103test(
104 "Bucket Put/Get/Head/List/Delete on /bucket",
105 Effect.gen(function* () {
106 const out = yield* stack;
107 const response = yield* fetchOk(`${out.apiUrl}/bucket`);
108 expect(response.status).toBe(200);
109 const body = (yield* response.json) as { ok: boolean };
110 expect(body.ok).toBe(true);
111 }),
112 { timeout: 60_000 },
113);