1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import { findZoneByName } from "@/Cloudflare/Zone/lookup";
4import * as Provider from "@/Provider";
5import * as Test from "@/Test/Alchemy";
6import * as addressing from "@distilled.cloud/cloudflare/addressing";
7import * as dns from "@distilled.cloud/cloudflare/dns";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import * as Result from "effect/Result";
11import { MinimumLogLevel } from "effect/References";
12import * as Schedule from "effect/Schedule";
13import * as Stream from "effect/Stream";
14
15const { test } = Test.make({ providers: Cloudflare.providers() });
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
25// Deterministic per-test hostname (never derive from Date.now()/random).
26const HOSTNAME = `alchemy-regional.${zoneName}`;
27
28const resolveZoneId = Effect.gen(function* () {
29 const { accountId } = yield* yield* CloudflareEnvironment;
30 const zone = yield* findZoneByName({ accountId, name: zoneName });
31 if (!zone) {
32 return yield* Effect.die(
33 new Error(`zone "${zoneName}" not found in account`),
34 );
35 }
36 return zone.id;
37});
38
39// A freshly minted scoped token propagates eventually-consistently across
40// Cloudflare's edge — retry the typed `Forbidden` blips on out-of-band calls.
41const forbiddenRetrySchedule = Schedule.exponential("500 millis");
42
43const retryForbidden = <A, E extends { _tag: string }, R>(
44 effect: Effect.Effect<A, E, R>,
45) =>
46 effect.pipe(
47 Effect.retry({
48 while: (e) => e._tag === "Forbidden",
49 schedule: forbiddenRetrySchedule,
50 times: 8,
51 }),
52 );
53
54const getRegionalHostname = (zoneId: string, hostname: string) =>
55 retryForbidden(addressing.getRegionalHostname({ zoneId, hostname })).pipe(
56 Effect.catchTag(["RegionalHostnameNotFound", "RegionalHostnameEmpty"], () =>
57 Effect.succeed(undefined),
58 ),
59 );
60
61const deleteRegionalHostname = (zoneId: string, hostname: string) =>
62 retryForbidden(addressing.deleteRegionalHostname({ zoneId, hostname })).pipe(
63 Effect.catchTag(
64 ["RegionalHostnameNotFound", "RegionalHostnameEmpty"],
65 () => Effect.void,
66 ),
67 );
68
69// Regionalization only takes effect for hostnames with a DNS record — and
70// creation rejects hostnames without one. Ensure an A record exists for the
71// test hostname (idempotently) before exercising the resource.
72const ensureDnsRecord = (zoneId: string) =>
73 Effect.gen(function* () {
74 const existing = yield* retryForbidden(
75 dns.listRecords
76 .items({ zoneId, name: { exact: HOSTNAME }, type: "A" })
77 .pipe(
78 Stream.runCollect,
79 Effect.map((c) => Array.from(c)),
80 ),
81 );
82 if (existing.length === 0) {
83 yield* retryForbidden(
84 dns.createRecord({
85 zoneId,
86 name: HOSTNAME,
87 type: "A",
88 content: "203.0.113.77",
89 ttl: 300,
90 proxied: true,
91 }),
92 );
93 }
94 });
95
96const purgeDnsRecord = (zoneId: string) =>
97 retryForbidden(
98 dns.listRecords
99 .items({ zoneId, name: { exact: HOSTNAME }, type: "A" })
100 .pipe(
101 Stream.runCollect,
102 Effect.map((c) => Array.from(c)),
103 ),
104 ).pipe(
105 Effect.flatMap(
106 Effect.forEach((r) =>
107 dns
108 .deleteRecord({ zoneId, dnsRecordId: r.id })
109 .pipe(Effect.catch(() => Effect.void)),
110 ),
111 ),
112 );
113
114// Regional Hostnames require the Data Localization Suite / Regional Services
115// entitlement on the zone. The test probes a raw create once: unentitled
116// zones assert the typed error tag; entitled zones run the full lifecycle
117// (create, patch regionKey in place, destroy, verify gone).
118test.provider(
119 "regional hostname lifecycle (typed error on unentitled zones)",
120 (stack) =>
121 Effect.gen(function* () {
122 const zoneId = yield* resolveZoneId;
123
124 yield* stack.destroy();
125 yield* ensureDnsRecord(zoneId);
126 // Purge leftovers from interrupted runs.
127 yield* deleteRegionalHostname(zoneId, HOSTNAME);
128
129 const probe = yield* retryForbidden(
130 addressing.createRegionalHostname({
131 zoneId,
132 hostname: HOSTNAME,
133 regionKey: "eu",
134 }),
135 ).pipe(Effect.result);
136
137 if (Result.isFailure(probe)) {
138 // Unentitled — must surface as a typed tag from the operation's
139 // error union, never an untyped catch-all.
140 expect(["InvalidHostname", "RegionalHostnameEmpty"]).toContain(
141 probe.failure._tag,
142 );
143 yield* purgeDnsRecord(zoneId);
144 yield* stack.destroy();
145 return;
146 }
147
148 // Entitled — remove the probe hostname and run the full lifecycle.
149 yield* deleteRegionalHostname(zoneId, HOSTNAME);
150
151 const created = yield* stack.deploy(
152 Effect.gen(function* () {
153 return yield* Cloudflare.RegionalHostname.RegionalHostname(
154 "Regional",
155 {
156 zoneId,
157 hostname: HOSTNAME,
158 regionKey: "eu",
159 },
160 );
161 }),
162 );
163 expect(created.zoneId).toEqual(zoneId);
164 expect(created.hostname).toEqual(HOSTNAME);
165 expect(created.regionKey).toEqual("eu");
166 expect(created.createdOn).toBeDefined();
167
168 // Out-of-band verification via the distilled API.
169 const live = yield* getRegionalHostname(zoneId, HOSTNAME);
170 expect(live?.regionKey).toEqual("eu");
171
172 // Move the hostname to another region in place — same identifier.
173 const updated = yield* stack.deploy(
174 Effect.gen(function* () {
175 return yield* Cloudflare.RegionalHostname.RegionalHostname(
176 "Regional",
177 {
178 zoneId,
179 hostname: HOSTNAME,
180 regionKey: "us",
181 },
182 );
183 }),
184 );
185 expect(updated.hostname).toEqual(HOSTNAME);
186 expect(updated.regionKey).toEqual("us");
187
188 const patched = yield* getRegionalHostname(zoneId, HOSTNAME);
189 expect(patched?.regionKey).toEqual("us");
190
191 // Destroy and verify the hostname is gone (typed not-found read).
192 yield* stack.destroy();
193 const gone = yield* getRegionalHostname(zoneId, HOSTNAME);
194 expect(gone).toBeUndefined();
195
196 yield* purgeDnsRecord(zoneId);
197 }).pipe(logLevel),
198 {
199 tags: [
200 "provider:cloudflare",
201 "provider:cloudflare:addressing",
202 "provider:cloudflare:dns",
203 "provider:cloudflare:regionalhostname",
204 "provider:cloudflare:zone",
205 "live",
206 ],
207 timeout: 120_000,
208 },
209);
210
211// Canonical `list()` test (zone-scoped collection): `list()` fans out over
212// every zone via `listAllZones` and exhaustively paginates each zone's
213// regional hostnames. Data Localization is entitlement-gated, so the deploy
214// is probe-gated: when the zone is entitled we assert the deployed hostname
215// appears in the result; otherwise we still assert `list()` returns a
216// well-typed (possibly empty) array.
217//
218// SKIP-GATED: `list()` fans out over EVERY zone in the account, but the
219// scoped test token can only access `alchemy-test-2.us`. Listing regional
220// hostnames on any other zone returns a 403:
221// Forbidden: forbidden (GET /zones/{zone_id}/addressing/regional_hostnames)
222// `listRegionalHostnames` types its error union as `DefaultErrors` only, so
223// `Forbidden` cannot be `catchTag`ed/skipped yet. Needed distilled patch:
224// submodules/distilled/packages/cloudflare/patches/addressing/listRegionalHostnames.json
225// -> { "errors": { "Forbidden": [{ "status": 403 }] } }
226// then regenerate addressing and add "Forbidden" to the catch in list().
227// Gate the live run behind CLOUDFLARE_TEST_REGIONAL_HOSTNAME_LIST=1 (run on
228// an account whose token can read every zone, or a single-zone account).
229test.provider.skipIf(!process.env.CLOUDFLARE_TEST_REGIONAL_HOSTNAME_LIST)(
230 "list enumerates regional hostnames across zones",
231 (stack) =>
232 Effect.gen(function* () {
233 const zoneId = yield* resolveZoneId;
234
235 yield* stack.destroy();
236 yield* ensureDnsRecord(zoneId);
237 yield* deleteRegionalHostname(zoneId, HOSTNAME);
238
239 // Probe entitlement once: unentitled zones reject create with a typed tag.
240 const probe = yield* retryForbidden(
241 addressing.createRegionalHostname({
242 zoneId,
243 hostname: HOSTNAME,
244 regionKey: "eu",
245 }),
246 ).pipe(Effect.result);
247
248 const entitled = Result.isSuccess(probe);
249 if (!entitled) {
250 expect(["InvalidHostname", "RegionalHostnameEmpty"]).toContain(
251 probe.failure._tag,
252 );
253 } else {
254 // Remove the raw probe so the resource owns the hostname.
255 yield* deleteRegionalHostname(zoneId, HOSTNAME);
256 yield* stack.deploy(
257 Effect.gen(function* () {
258 return yield* Cloudflare.RegionalHostname.RegionalHostname(
259 "Regional",
260 {
261 zoneId,
262 hostname: HOSTNAME,
263 regionKey: "eu",
264 },
265 );
266 }),
267 );
268 }
269
270 const provider = yield* Provider.findProvider(
271 Cloudflare.RegionalHostname.RegionalHostname,
272 );
273 const all = yield* provider.list();
274
275 // `list()` always returns the full Attributes shape for each item.
276 expect(Array.isArray(all)).toBe(true);
277 for (const item of all) {
278 expect(typeof item.zoneId).toBe("string");
279 expect(typeof item.hostname).toBe("string");
280 expect(typeof item.regionKey).toBe("string");
281 }
282
283 if (entitled) {
284 // The deployed hostname must appear in the exhaustively-paginated result.
285 expect(
286 all.some(
287 (item) => item.zoneId === zoneId && item.hostname === HOSTNAME,
288 ),
289 ).toBe(true);
290 }
291
292 yield* stack.destroy();
293 yield* deleteRegionalHostname(zoneId, HOSTNAME);
294 yield* purgeDnsRecord(zoneId);
295 }).pipe(logLevel),
296 {
297 tags: [
298 "provider:cloudflare",
299 "provider:cloudflare:addressing",
300 "provider:cloudflare:dns",
301 "provider:cloudflare:regionalhostname",
302 "provider:cloudflare:zone",
303 "live",
304 ],
305 timeout: 120_000,
306 },
307);