1import { Query } from "@distilled.cloud/core/query";
2import { Railway as RailwayApi } from "@distilled.cloud/railway";
3import * as Railway from "@/Railway";
4import { suitePartition } from "../suiteProject.ts";
5import * as Test from "@/Test/Alchemy";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import * as HttpClient from "effect/http/HttpClient";
11import * as pathe from "pathe";
12import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
13import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
14
15const { test } = Test.make({ providers: Railway.providers() });
16
17const logLevel = Effect.provideService(
18 MinimumLogLevel,
19 process.env.DEBUG ? "Debug" : "Info",
20);
21
22const fixtureDir = pathe.resolve(
23 import.meta.dirname,
24 "../../AWS/Website/fixtures/astro-app",
25);
26const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
27const fixtureEntries = [
28 ".gitignore",
29 "package.json",
30 "astro.config.mjs",
31 "src",
32 "public",
33];
34
35const readService = Query.fn((id: string) => ({
36 deletedAt: RailwayApi.service({ id }).deletedAt,
37}));
38
39const waitUntilGone = (serviceId: string) =>
40 readService(serviceId).pipe(
41 Effect.map((service) =>
42 service.deletedAt != null ? ("gone" as const) : ("found" as const),
43 ),
44 Effect.catchTag("RailwayNotFound", () => Effect.succeed("gone" as const)),
45 Effect.repeat({
46 schedule: Schedule.spaced("1 second"),
47 until: (status) => status === "gone",
48 times: 10,
49 }),
50 );
51
52test.provider(
53 "Astro SSR: GET / and a dynamic API route",
54 (stack) =>
55 Effect.gen(function* () {
56 yield* stack.destroy();
57
58 const rootDir = yield* cloneFixture(fixtureDir, {
59 prefix: "alchemy-astro-railway-",
60 tempRoot,
61 entries: fixtureEntries,
62 });
63
64 const deployed = yield* stack.deploy(
65 Effect.gen(function* () {
66 const { project, environment } = yield* suitePartition;
67 const site = yield* Railway.Website.Astro("Web", {
68 project,
69 environment,
70 rootDir,
71 memo: {
72 include: [
73 "src/**",
74 "public/**",
75 "package.json",
76 "astro.config.mjs",
77 ],
78 },
79 });
80 return { site };
81 }),
82 );
83
84 const url = deployed.site.url;
85 expect(url).toBeDefined();
86 expect(url).toMatch(/^https:\/\//);
87 expect(deployed.site.service).toBeDefined();
88
89 yield* expectUrlContains(`${url!}/`, "ASTRO_AWS_PAGE_MARKER", {
90 timeout: "90 seconds",
91 label: "astro ssr home",
92 });
93 yield* expectUrlContains(
94 `${url!}/api/hello?echo=roundtrip`,
95 "ASTRO_AWS_API_MARKER",
96 {
97 timeout: "30 seconds",
98 label: "astro api route",
99 },
100 );
101
102 const client = yield* HttpClient.HttpClient;
103 const health = yield* client.get(`${url!}/health`).pipe(
104 Effect.flatMap((res) =>
105 res.status === 200
106 ? res.text
107 : Effect.fail(new Error(`health returned ${res.status}`)),
108 ),
109 Effect.retry({
110 schedule: Schedule.exponential("500 millis"),
111 times: 6,
112 }),
113 );
114 expect(health).toContain("ok");
115
116 const serviceId = deployed.site.service!.serviceId;
117 yield* stack.destroy();
118 const gone = yield* waitUntilGone(serviceId);
119 expect(gone).toEqual("gone");
120 }).pipe(logLevel),
121 {
122 tags: [
123 "provider:railway",
124 "provider:railway:project",
125 "provider:railway:projectenvironment",
126 "provider:railway:service",
127 "provider:railway:website",
128 "live",
129 ],
130 timeout: 120_000,
131 },
132);