1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import { findZoneByName } from "@/Cloudflare/Zone/lookup";
4import * as Core from "@/Test/Core.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 Stack from "./fixtures/stack.ts";
12
13const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
14 providers: Cloudflare.providers(),
15});
16
17const logLevel = Effect.provideService(
18 MinimumLogLevel,
19 process.env.DEBUG ? "Debug" : "Info",
20);
21
22const zoneName =
23 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
24
25const stack = beforeAll(deploy(Stack));
26afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
27
28// Resolve the test zone id via the SDK. `withProviders` supplies the Cloudflare
29// credentials + account id the lookup needs inside a plain test body.
30const resolveZoneId = Core.withProviders(
31 Effect.gen(function* () {
32 const { accountId } = yield* yield* CloudflareEnvironment;
33 const zone = yield* findZoneByName({ accountId, name: zoneName });
34 return zone?.id;
35 }),
36 { providers: Cloudflare.providers() },
37 "DnsTestStack",
38);
39
40test(
41 "deployed worker drives the full DNS record CRUD surface via DnsReadWrite",
42 Effect.gen(function* () {
43 const { effectUrl } = yield* stack;
44 expect(effectUrl).toBeTypeOf("string");
45
46 const zoneId = yield* resolveZoneId;
47 expect(zoneId, `zone "${zoneName}" not found in account`).toBeTypeOf(
48 "string",
49 );
50
51 // Deterministic record name — the same on every run (never
52 // Date.now()/random). The fixture's /dns route deletes any leftover
53 // record with this name before creating, so a crashed run self-heals
54 // instead of leaking records into the zone.
55 const name = `alchemy-dns-test-crud.${zoneName}`;
56
57 const client = yield* HttpClient.HttpClient;
58 const res = yield* client
59 .get(`${effectUrl}/dns?name=${encodeURIComponent(name)}`)
60 .pipe(
61 // A cold-starting or briefly-unhealthy edge returns 5xx — often a
62 // Cloudflare HTML error page, NOT the worker's structured JSON, so we
63 // must never try to parse it. Treat any non-200 as transient and ride
64 // it out: this covers both cold start and eventual-consistency blips
65 // in the scoped API-token propagation the worker depends on.
66 Effect.flatMap((res) =>
67 res.status === 200
68 ? Effect.succeed(res)
69 : Effect.fail(new Error(`Worker not ready: ${res.status}`)),
70 ),
71 // Cap exponential backoff at 3s so retries stay bounded.
72 Effect.retry({
73 schedule: Schedule.min([
74 Schedule.exponential("500 millis"),
75 Schedule.spaced("3 seconds"),
76 ]),
77 times: 20,
78 }),
79 );
80
81 const body = (yield* res.json) as {
82 id: string;
83 getName: string;
84 count: number;
85 updatedId: string;
86 deleted: boolean;
87 };
88 expect(body.id.length).toBeGreaterThan(0);
89 expect(body.getName).toBe(name);
90 expect(body.count).toBeGreaterThan(0);
91 expect(body.updatedId).toBe(body.id);
92 expect(body.deleted).toBe(true);
93 }).pipe(logLevel),
94 {
95 tags: [
96 "provider:cloudflare",
97 "provider:cloudflare:dns",
98 "provider:cloudflare:worker",
99 "provider:cloudflare:zone",
100 "live",
101 ],
102 timeout: 180_000,
103 },
104);