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 dns from "@distilled.cloud/cloudflare/dns";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11const { test } = Test.make({ providers: Cloudflare.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const zoneName =
19 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
20
21const resolveZoneId = Effect.gen(function* () {
22 const { accountId } = yield* yield* CloudflareEnvironment;
23 const zone = yield* findZoneByName({ accountId, name: zoneName });
24 if (!zone) {
25 return yield* Effect.die(
26 new Error(`zone "${zoneName}" not found in account`),
27 );
28 }
29 return zone.id;
30});
31
32// Ride out fresh-token 403 blips on out-of-band calls by retrying the typed
33// `Forbidden` error (added to the dns ops' unions via distilled patches).
34const forbiddenRetrySchedule = Schedule.exponential("500 millis");
35
36const retryForbidden = <A, E extends { _tag: string }, R>(
37 eff: Effect.Effect<A, E, R>,
38) =>
39 eff.pipe(
40 Effect.retry({
41 while: (e) => e._tag === "Forbidden",
42 schedule: forbiddenRetrySchedule,
43 times: 8,
44 }),
45 );
46
47const getSettings = (zoneId: string) =>
48 retryForbidden(dns.getSettingZone({ zoneId }));
49
50// Baselines for the (entitlement-free) fields these tests manage.
51// NOTE: `nsTtl` and custom SOA records are entitlement-gated on the testing
52// account ("Custom nameserver TTLs / Custom SOA records are not available to
53// this account or zone"), so the tests exercise `flattenAllCnames` and
54// `multiProvider` instead.
55const BASELINE_FLATTEN_ALL_CNAMES = false;
56const BASELINE_MULTI_PROVIDER = false;
57
58// Normalize the singleton to a known baseline so each run starts from the
59// same cloud state regardless of what a previous (possibly interrupted) run
60// left behind. Patches only when something actually drifted.
61const normalizeBaseline = (zoneId: string) =>
62 Effect.gen(function* () {
63 const observed = yield* getSettings(zoneId);
64 if (
65 observed.flattenAllCnames === BASELINE_FLATTEN_ALL_CNAMES &&
66 observed.multiProvider === BASELINE_MULTI_PROVIDER
67 ) {
68 return;
69 }
70 yield* retryForbidden(
71 dns.patchSettingZone({
72 zoneId,
73 flattenAllCnames: BASELINE_FLATTEN_ALL_CNAMES,
74 multiProvider: BASELINE_MULTI_PROVIDER,
75 }),
76 );
77 });
78
79describe.sequential(
80 "ZoneSettings",
81 {
82 tags: [
83 "provider:cloudflare",
84 "provider:cloudflare:dns",
85 "provider:cloudflare:zone",
86 "live",
87 ],
88 },
89 () => {
90 test.provider(
91 "pins flattenAllCnames and restores the pre-management value on destroy",
92 (stack) =>
93 Effect.gen(function* () {
94 const zoneId = yield* resolveZoneId;
95
96 yield* stack.destroy();
97 yield* normalizeBaseline(zoneId);
98
99 const settings = yield* stack.deploy(
100 Effect.gen(function* () {
101 return yield* Cloudflare.DNS.ZoneDnsSettings("DnsSettings", {
102 zoneId,
103 flattenAllCnames: true,
104 });
105 }),
106 );
107
108 expect(settings.zoneId).toEqual(zoneId);
109 expect(settings.flattenAllCnames).toEqual(true);
110 // The pre-management snapshot was captured for restore-on-destroy.
111 expect(settings.initialSettings.flattenAllCnames).toEqual(
112 BASELINE_FLATTEN_ALL_CNAMES,
113 );
114 expect(settings.managedKeys).toContain("flattenAllCnames");
115
116 // Out-of-band verify via the SDK.
117 const live = yield* getSettings(zoneId);
118 expect(live.flattenAllCnames).toEqual(true);
119
120 yield* stack.destroy();
121
122 // Destroy restored the managed field to its pre-management value.
123 const restored = yield* getSettings(zoneId);
124 expect(restored.flattenAllCnames).toEqual(
125 BASELINE_FLATTEN_ALL_CNAMES,
126 );
127
128 // Re-running destroy is idempotent (nothing left to restore).
129 yield* stack.destroy();
130 const still = yield* getSettings(zoneId);
131 expect(still.flattenAllCnames).toEqual(BASELINE_FLATTEN_ALL_CNAMES);
132 }).pipe(logLevel),
133 { timeout: 300_000 },
134 );
135
136 test.provider(
137 "updates in place, unions managedKeys, restores all managed fields",
138 (stack) =>
139 Effect.gen(function* () {
140 const zoneId = yield* resolveZoneId;
141
142 yield* stack.destroy();
143 yield* normalizeBaseline(zoneId);
144
145 const initial = yield* stack.deploy(
146 Effect.gen(function* () {
147 return yield* Cloudflare.DNS.ZoneDnsSettings("DnsSettings", {
148 zoneId,
149 multiProvider: true,
150 });
151 }),
152 );
153 expect(initial.multiProvider).toEqual(true);
154 expect(initial.initialSettings.multiProvider).toEqual(
155 BASELINE_MULTI_PROVIDER,
156 );
157 expect(initial.managedKeys).toContain("multiProvider");
158
159 // Same singleton patched in place — new value plus a second managed
160 // field; the original snapshot survives the update.
161 const updated = yield* stack.deploy(
162 Effect.gen(function* () {
163 return yield* Cloudflare.DNS.ZoneDnsSettings("DnsSettings", {
164 zoneId,
165 multiProvider: true,
166 flattenAllCnames: true,
167 });
168 }),
169 );
170 expect(updated.zoneId).toEqual(zoneId);
171 expect(updated.multiProvider).toEqual(true);
172 expect(updated.flattenAllCnames).toEqual(true);
173 expect(updated.initialSettings.multiProvider).toEqual(
174 BASELINE_MULTI_PROVIDER,
175 );
176 expect(updated.initialSettings.flattenAllCnames).toEqual(
177 BASELINE_FLATTEN_ALL_CNAMES,
178 );
179 expect(updated.managedKeys).toContain("multiProvider");
180 expect(updated.managedKeys).toContain("flattenAllCnames");
181
182 const live = yield* getSettings(zoneId);
183 expect(live.multiProvider).toEqual(true);
184 expect(live.flattenAllCnames).toEqual(true);
185
186 // Drop `multiProvider` from props — the key stays managed (union
187 // across all reconciles) so destroy still restores it.
188 const dropped = yield* stack.deploy(
189 Effect.gen(function* () {
190 return yield* Cloudflare.DNS.ZoneDnsSettings("DnsSettings", {
191 zoneId,
192 flattenAllCnames: true,
193 });
194 }),
195 );
196 expect(dropped.managedKeys).toContain("multiProvider");
197 expect(dropped.managedKeys).toContain("flattenAllCnames");
198
199 yield* stack.destroy();
200
201 // Both managed fields were restored to their pre-management values.
202 const restored = yield* getSettings(zoneId);
203 expect(restored.multiProvider).toEqual(BASELINE_MULTI_PROVIDER);
204 expect(restored.flattenAllCnames).toEqual(
205 BASELINE_FLATTEN_ALL_CNAMES,
206 );
207 }).pipe(logLevel),
208 { timeout: 300_000 },
209 );
210
211 // Canonical `list()` test (zone-scoped singleton): there is no account-wide
212 // API for this per-zone settings object, so `list()` enumerates every zone
213 // via `listAllZones` and reads the singleton in each. Assert the result is
214 // non-empty and contains the standing test zone.
215 test.provider("list enumerates DNS settings across all zones", (stack) =>
216 Effect.gen(function* () {
217 const zoneId = yield* resolveZoneId;
218
219 const provider = yield* Provider.findProvider(
220 Cloudflare.DNS.ZoneDnsSettings,
221 );
222 const all = yield* provider.list();
223
224 expect(all.length).toBeGreaterThan(0);
225 expect(all.some((s) => s.zoneId === zoneId)).toBe(true);
226
227 // `stack` is unused here (the singleton always exists on every zone),
228 // but keep the destroy bookend so the harness state stays clean.
229 yield* stack.destroy();
230 }).pipe(logLevel),
231 );
232 },
233);