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 { 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// Outgoing zone transfers are an Enterprise feature. On the testing
23// account, `POST /zones/{id}/secondary_dns/outgoing` fails with HTTP
24// 401 — result "Not authorized to setup outgoing zone transfers" —
25// surfaced as the typed `OutgoingZoneTransfersNotAllowed` error. The
26// full lifecycle test below is gated behind an entitled zone supplied
27// via env.
28const outgoingEntitled = !!process.env.CLOUDFLARE_TEST_OUTGOING_ZONE_TRANSFER;
29
30const resolveZoneId = Effect.gen(function* () {
31 const { accountId } = yield* yield* CloudflareEnvironment;
32 const zone = yield* findZoneByName({ accountId, name: zoneName });
33 if (!zone) {
34 return yield* Effect.die(
35 new Error(`zone "${zoneName}" not found in account`),
36 );
37 }
38 return zone.id;
39});
40
41const retryForbidden = <A, E extends { _tag: string }, R>(
42 eff: Effect.Effect<A, E, R>,
43) =>
44 eff.pipe(
45 Effect.retry({
46 while: (e) => e._tag === "Forbidden",
47 schedule: Schedule.exponential("500 millis"),
48 times: 8,
49 }),
50 );
51
52test.provider.skipIf(outgoingEntitled)(
53 "surfaces the typed OutgoingZoneTransfersNotAllowed error on unentitled zones",
54 (stack) =>
55 Effect.gen(function* () {
56 const { accountId } = yield* yield* CloudflareEnvironment;
57 const zoneId = yield* resolveZoneId;
58
59 yield* stack.destroy();
60
61 // Need a peer to attempt the link with — peers themselves are
62 // available on this account.
63 const peer = yield* retryForbidden(
64 dns.createZoneTransferPeer({
65 accountId,
66 name: "alchemy-dnszt-outgoing-probe-peer",
67 }),
68 );
69
70 const error = yield* retryForbidden(
71 dns.createZoneTransferOutgoing({
72 zoneId,
73 name: `${zoneName}.`,
74 peers: [peer.id],
75 }),
76 ).pipe(
77 Effect.flip,
78 Effect.ensuring(
79 dns
80 .deleteZoneTransferPeer({ accountId, peerId: peer.id })
81 .pipe(Effect.ignore),
82 ),
83 );
84 expect(error._tag).toEqual("OutgoingZoneTransfersNotAllowed");
85 }).pipe(logLevel),
86 {
87 tags: [
88 "provider:cloudflare",
89 "provider:cloudflare:dns",
90 "provider:cloudflare:zone",
91 "live",
92 ],
93 timeout: 120_000,
94 },
95);
96
97test.provider.skipIf(!outgoingEntitled)(
98 "create, sync peers and enabled state, and delete the outgoing config",
99 (stack) =>
100 Effect.gen(function* () {
101 const zoneId = yield* resolveZoneId;
102
103 yield* stack.destroy();
104
105 const { outgoing, peer } = yield* stack.deploy(
106 Effect.gen(function* () {
107 const peer = yield* Cloudflare.DNS.ZoneTransferPeer("OutgoingPeer", {
108 name: "alchemy-dnszt-outgoing-peer",
109 ip: "192.0.2.53",
110 port: 53,
111 });
112 const outgoing = yield* Cloudflare.DNS.ZoneTransferOutgoing(
113 "Outgoing",
114 {
115 zoneId,
116 name: `${zoneName}.`,
117 peers: [peer.peerId],
118 },
119 );
120 return { outgoing, peer };
121 }),
122 );
123 expect(outgoing.zoneId).toEqual(zoneId);
124 expect(outgoing.peers).toEqual([peer.peerId]);
125 expect(outgoing.enabled).toEqual(true);
126
127 // Disable in place — toggled via the dedicated endpoint. Keep the
128 // peer deployed across the update (engine constraint: never drop a
129 // dependency in the same deploy that changes its dependent).
130 const disabled = yield* stack.deploy(
131 Effect.gen(function* () {
132 const peer = yield* Cloudflare.DNS.ZoneTransferPeer("OutgoingPeer", {
133 name: "alchemy-dnszt-outgoing-peer",
134 ip: "192.0.2.53",
135 port: 53,
136 });
137 const outgoing = yield* Cloudflare.DNS.ZoneTransferOutgoing(
138 "Outgoing",
139 {
140 zoneId,
141 name: `${zoneName}.`,
142 peers: [peer.peerId],
143 enabled: false,
144 },
145 );
146 return outgoing;
147 }),
148 );
149 expect(disabled.enabled).toEqual(false);
150
151 yield* stack.destroy();
152
153 // Once deleted, the GET reports the typed not-found tag.
154 const gone = yield* retryForbidden(
155 dns.getZoneTransferOutgoing({ zoneId }),
156 ).pipe(Effect.flip);
157 expect(gone._tag).toEqual("OutgoingZoneTransferNotFound");
158
159 // Re-running destroy is idempotent.
160 yield* stack.destroy();
161 }).pipe(logLevel),
162 {
163 tags: [
164 "provider:cloudflare",
165 "provider:cloudflare:dns",
166 "provider:cloudflare:zone",
167 "live",
168 ],
169 timeout: 300_000,
170 },
171);
172
173// Canonical `list()` test (zone-scoped singleton): there is no account-wide
174// API for the outgoing transfer config, so `list()` enumerates every zone
175// via `listAllZones` and reads the singleton in each, skipping zones that
176// have no config or lack the Secondary DNS entitlement (typed
177// OutgoingZoneTransferNotFound / OutgoingZoneTransfersNotAllowed tags). The
178// read-only assertion (well-typed Attributes[]) always runs; on the
179// unentitled testing account no zone returns a config, so the deployed-
180// presence assertion is gated behind an entitled zone.
181test.provider(
182 "list enumerates the outgoing transfer config per zone",
183 (stack) =>
184 Effect.gen(function* () {
185 const zoneId = yield* resolveZoneId;
186
187 const provider = yield* Provider.findProvider(
188 Cloudflare.DNS.ZoneTransferOutgoing,
189 );
190 const all = yield* provider.list();
191
192 // Always-on assertion: list() returns a well-typed Attributes[] and
193 // never throws even when every zone is unconfigured/unentitled.
194 expect(Array.isArray(all)).toBe(true);
195 for (const item of all) {
196 expect(typeof item.zoneId).toBe("string");
197 expect(Array.isArray(item.peers)).toBe(true);
198 expect(typeof item.enabled).toBe("boolean");
199 }
200
201 // Only an entitled+configured zone appears in the enumeration.
202 if (outgoingEntitled) {
203 expect(all.some((o) => o.zoneId === zoneId)).toBe(true);
204 }
205
206 yield* stack.destroy();
207 }).pipe(logLevel),
208 {
209 tags: [
210 "provider:cloudflare",
211 "provider:cloudflare:dns",
212 "provider:cloudflare:zone",
213 "live",
214 ],
215 timeout: 120_000,
216 },
217);