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/nuxt-app",
24);
25const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
26const fixtureEntries = [
27 ".gitignore",
28 "package.json",
29 "nuxt.config.ts",
30 "app",
31 "server",
32 "public",
33];
34
35const waitUntilGone = (id: number) =>
36 servers.getServer({ id }).pipe(
37 Effect.as("found" as const),
38 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
39 Effect.repeat({
40 schedule: Schedule.spaced("1 second"),
41 until: (status) => status === "gone",
42 times: 10,
43 }),
44 );
45
46test.provider.skipIf(!hasHetznerCreds)(
47 "Nuxt: deploy, GET /, destroy, gone",
48 (stack) =>
49 Effect.gen(function* () {
50 yield* stack.destroy();
51
52 const rootDir = yield* cloneFixture(fixtureDir, {
53 prefix: "alchemy-nuxt-hetzner-",
54 tempRoot,
55 entries: fixtureEntries,
56 });
57
58 const deployed = yield* stack.deploy(
59 Effect.gen(function* () {
60 const site = yield* Hetzner.Website.Nuxt("Web", {
61 rootDir,
62 memo: {
63 include: [
64 "app/**",
65 "server/**",
66 "public/**",
67 "package.json",
68 "nuxt.config.ts",
69 ],
70 },
71 });
72 return { site };
73 }),
74 );
75
76 const url = deployed.site.url;
77 expect(url).toBeDefined();
78 expect(url).toMatch(/^http:\/\//);
79 expect(deployed.site.service).toBeDefined();
80 expect(deployed.site.server).toBeDefined();
81
82 yield* expectUrlContains(`${url!}/health`, "ok", {
83 timeout: "30 seconds",
84 label: "health",
85 });
86 yield* expectUrlContains(`${url!}/robots.txt`, "User-agent", {
87 timeout: "30 seconds",
88 label: "static robots",
89 });
90 yield* expectUrlContains(
91 `${url!}/prerendered`,
92 "NUXT_AWS_PRERENDERED_MARKER",
93 {
94 timeout: "30 seconds",
95 label: "extra route",
96 },
97 );
98 yield* expectUrlContains(`${url!}/`, "NUXT_AWS_PAGE_MARKER", {
99 timeout: "90 seconds",
100 label: "home page",
101 });
102 yield* expectUrlContains(
103 `${url!}/api/hello?echo=roundtrip`,
104 "NUXT_AWS_API_MARKER",
105 {
106 timeout: "30 seconds",
107 label: "api route",
108 },
109 );
110
111 const serverId = deployed.site.server!.serverId;
112 yield* stack.destroy();
113 const gone = yield* waitUntilGone(serverId);
114 expect(gone).toEqual("gone");
115 }).pipe(
116 logLevel,
117 Effect.ensuring(stack.destroy().pipe(Effect.orDie)),
118 Effect.catchTag(["PreconditionFailed", "Forbidden"], (error) =>
119 Effect.logWarning(`skipping: Hetzner quota (${error._tag})`),
120 ),
121 ),
122 {
123 tags: [
124 "provider:hetzner",
125 "provider:hetzner:service",
126 "provider:hetzner:website",
127 "live",
128 ],
129 timeout: 240000,
130 },
131);