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 zones from "@distilled.cloud/cloudflare/zones";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({ providers: Cloudflare.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19const zoneName =
20 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
21
22// Enabling account-level custom nameservers (ACNS) on a zone requires an
23// account nameserver set to exist first — a Business/Enterprise feature the
24// testing account does not have. The enable/disable lifecycle below is
25// gated behind a zone id (in an ACNS-entitled account) supplied via env.
26const acnsZoneId = process.env.CLOUDFLARE_TEST_ACNS_ZONE_ID;
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// The scoped API token the test harness mints propagates eventually-
40// consistently across Cloudflare's edge — a fresh token intermittently 403s.
41// Ride out the blips on the test's own out-of-band calls by retrying the
42// typed `Forbidden` error.
43const forbiddenRetrySchedule = Schedule.exponential("500 millis");
44
45const getCustomNs = (zoneId: string) =>
46 zones.getCustomNameserver({ zoneId }).pipe(
47 Effect.retry({
48 while: (e) => e._tag === "Forbidden",
49 schedule: forbiddenRetrySchedule,
50 times: 8,
51 }),
52 );
53
54test.provider(
55 "pins the custom nameserver toggle and leaves the zone at its baseline on destroy",
56 (stack) =>
57 Effect.gen(function* () {
58 const zoneId = yield* resolveZoneId;
59
60 yield* stack.destroy();
61
62 // Baseline: the testing zone never has ACNS enabled (the account has
63 // no custom nameserver set), so `enabled: false` is the observed
64 // pre-management state the resource must capture and converge on.
65 const baseline = yield* getCustomNs(zoneId);
66 expect(baseline.enabled ?? false).toEqual(false);
67
68 const customNs = yield* stack.deploy(
69 Effect.gen(function* () {
70 return yield* Cloudflare.Zone.CustomNameservers("CustomNs", {
71 zoneId,
72 enabled: false,
73 });
74 }),
75 );
76
77 expect(customNs.zoneId).toEqual(zoneId);
78 expect(customNs.enabled).toEqual(false);
79 // The pre-management state was captured for restore-on-destroy.
80 expect(customNs.initialEnabled).toEqual(false);
81 expect(customNs.initialNsSet).toBeUndefined();
82
83 const live = yield* getCustomNs(zoneId);
84 expect(live.enabled ?? false).toEqual(false);
85
86 // Re-deploy with no changes — reconcile observes the in-sync state
87 // and applies nothing.
88 const steady = yield* stack.deploy(
89 Effect.gen(function* () {
90 return yield* Cloudflare.Zone.CustomNameservers("CustomNs", {
91 zoneId,
92 enabled: false,
93 });
94 }),
95 );
96 expect(steady.enabled).toEqual(false);
97 expect(steady.initialEnabled).toEqual(false);
98
99 yield* stack.destroy();
100
101 // Destroy restored (kept) the zone's pre-management state.
102 const restored = yield* getCustomNs(zoneId);
103 expect(restored.enabled ?? false).toEqual(false);
104 }).pipe(logLevel),
105 { tags: ["provider:cloudflare", "provider:cloudflare:zone", "live"] },
106);
107
108test.provider(
109 "surfaces the typed CustomNameserverSetNotFound error when enabling without an account nameserver set",
110 (stack) =>
111 Effect.gen(function* () {
112 const zoneId = yield* resolveZoneId;
113
114 yield* stack.destroy();
115
116 // The testing account has no account-level custom nameserver set, so
117 // enabling ACNS on the zone must fail with the typed gate error.
118 const error = yield* zones
119 .putCustomNameserver({ zoneId, enabled: true })
120 .pipe(
121 Effect.retry({
122 while: (e) => e._tag === "Forbidden",
123 schedule: forbiddenRetrySchedule,
124 times: 8,
125 }),
126 Effect.flip,
127 );
128 expect(error._tag).toEqual("CustomNameserverSetNotFound");
129
130 yield* stack.destroy();
131 }).pipe(logLevel),
132 { tags: ["provider:cloudflare", "provider:cloudflare:zone", "live"] },
133);
134
135// Canonical `list()` test (zone-scoped singleton). There is no account-wide
136// API for the per-zone ACNS toggle, so `list()` enumerates every zone via
137// `listAllZones` and reads the singleton in each — emitting one Attributes
138// only for zones that actively use account custom nameservers (skipping the
139// default-disabled ones, mirroring `read`). The read-only shape assertion
140// always runs; the testing account has no ACNS entitlement, so the array is
141// typically empty.
142test.provider(
143 "list enumerates zones using account custom nameservers",
144 (stack) =>
145 Effect.gen(function* () {
146 yield* stack.destroy();
147
148 const provider = yield* Provider.findProvider(
149 Cloudflare.Zone.CustomNameservers,
150 );
151 const all = yield* provider.list();
152
153 expect(Array.isArray(all)).toBe(true);
154 for (const item of all) {
155 expect(typeof item.zoneId).toBe("string");
156 // `list()` only emits zones with ACNS actively enabled.
157 expect(item.enabled).toBe(true);
158 expect(typeof item.initialEnabled).toBe("boolean");
159 expect(item.nsSet === undefined || typeof item.nsSet === "number").toBe(
160 true,
161 );
162 expect(
163 item.initialNsSet === undefined ||
164 typeof item.initialNsSet === "number",
165 ).toBe(true);
166 }
167
168 yield* stack.destroy();
169 }).pipe(logLevel),
170 { tags: ["provider:cloudflare", "provider:cloudflare:zone", "live"] },
171);
172
173// In an ACNS-entitled account, `list()` must surface the enabled zone.
174test.provider.skipIf(!acnsZoneId)(
175 "list includes a zone with account custom nameservers enabled",
176 (stack) =>
177 Effect.gen(function* () {
178 const zoneId = acnsZoneId!;
179
180 yield* stack.destroy();
181
182 const enabled = yield* stack.deploy(
183 Effect.gen(function* () {
184 return yield* Cloudflare.Zone.CustomNameservers("AcnsList", {
185 zoneId,
186 enabled: true,
187 });
188 }),
189 );
190 expect(enabled.enabled).toEqual(true);
191
192 const provider = yield* Provider.findProvider(
193 Cloudflare.Zone.CustomNameservers,
194 );
195 const all = yield* provider.list();
196 expect(all.some((x) => x.zoneId === zoneId && x.enabled)).toBe(true);
197
198 yield* stack.destroy();
199 }).pipe(logLevel),
200 { tags: ["provider:cloudflare", "provider:cloudflare:zone", "live"] },
201);
202
203test.provider.skipIf(!acnsZoneId)(
204 "enables account custom nameservers and restores the baseline on destroy",
205 (stack) =>
206 Effect.gen(function* () {
207 const zoneId = acnsZoneId!;
208
209 yield* stack.destroy();
210
211 const enabled = yield* stack.deploy(
212 Effect.gen(function* () {
213 return yield* Cloudflare.Zone.CustomNameservers("AcnsToggle", {
214 zoneId,
215 enabled: true,
216 });
217 }),
218 );
219 expect(enabled.enabled).toEqual(true);
220 expect(enabled.initialEnabled).toEqual(false);
221
222 const live = yield* getCustomNs(zoneId);
223 expect(live.enabled).toEqual(true);
224
225 yield* stack.destroy();
226
227 // Destroy restored the zone to Cloudflare-assigned nameservers.
228 const restored = yield* getCustomNs(zoneId);
229 expect(restored.enabled ?? false).toEqual(false);
230 }).pipe(logLevel),
231 { tags: ["provider:cloudflare", "provider:cloudflare:zone", "live"] },
232);