1import * as Hetzner from "@/Hetzner";
2import * as Test from "@/Test/Alchemy";
3import * as servers from "@distilled.cloud/hetzner/servers";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import * as pathe from "pathe";
9import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
10import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
11
12const { test } = Test.make({ providers: Hetzner.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19const hasHetznerCreds = !!process.env.HCLOUD_TOKEN;
20
21const fixtureDir = pathe.resolve(
22 import.meta.dirname,
23 "../../AWS/Website/fixtures/solidstart-app",
24);
25const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
26const fixtureEntries = [
27 ".gitignore",
28 "package.json",
29 "vite.config.ts",
30 "src",
31 "public",
32];
33
34const waitUntilGone = (id: number) =>
35 servers.getServer({ id }).pipe(
36 Effect.as("found" as const),
37 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
38 Effect.repeat({
39 schedule: Schedule.spaced("1 second"),
40 until: (status) => status === "gone",
41 times: 10,
42 }),
43 );
44
45test.provider.skipIf(!hasHetznerCreds)(
46 "SolidStart: deploy, GET /, destroy, gone",
47 (stack) =>
48 Effect.gen(function* () {
49 yield* stack.destroy();
50
51 const rootDir = yield* cloneFixture(fixtureDir, {
52 prefix: "alchemy-solidstart-hetzner-",
53 tempRoot,
54 entries: fixtureEntries,
55 });
56
57 const deployed = yield* stack.deploy(
58 Effect.gen(function* () {
59 const site = yield* Hetzner.Website.SolidStart("Web", {
60 rootDir,
61 memo: {
62 include: [
63 "src/**",
64 "public/**",
65 "package.json",
66 "vite.config.ts",
67 ],
68 },
69 });
70 return { site };
71 }),
72 );
73
74 const url = deployed.site.url;
75 expect(url).toBeDefined();
76 expect(url).toMatch(/^http:\/\//);
77 expect(deployed.site.service).toBeDefined();
78 expect(deployed.site.server).toBeDefined();
79
80 yield* expectUrlContains(`${url!}/health`, "ok", {
81 timeout: "60 seconds",
82 label: "health",
83 });
84 yield* expectUrlContains(`${url!}/`, "SOLIDSTART_AWS_PAGE_MARKER", {
85 timeout: "180 seconds",
86 label: "home page",
87 });
88 yield* expectUrlContains(
89 `${url!}/api/hello?echo=roundtrip`,
90 "SOLIDSTART_AWS_API_MARKER",
91 {
92 timeout: "30 seconds",
93 label: "api route",
94 },
95 );
96
97 const serverId = deployed.site.server!.serverId;
98 yield* stack.destroy();
99 const gone = yield* waitUntilGone(serverId);
100 expect(gone).toEqual("gone");
101 }).pipe(
102 logLevel,
103 Effect.ensuring(stack.destroy().pipe(Effect.orDie)),
104 Effect.catchTag(["PreconditionFailed", "Forbidden"], (error) =>
105 Effect.logWarning(`skipping: Hetzner quota (${error._tag})`),
106 ),
107 ),
108 {
109 tags: [
110 "provider:hetzner",
111 "provider:hetzner:service",
112 "provider:hetzner:website",
113 "live",
114 ],
115 timeout: 420000,
116 },
117);