1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as addressing from "@distilled.cloud/cloudflare/addressing";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10import * as Stream from "effect/Stream";
11
12const { test } = Test.make({ providers: Cloudflare.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19// A freshly minted scoped token propagates eventually-consistently across
20// Cloudflare's edge — retry the typed `Forbidden` blips on out-of-band calls.
21const forbiddenRetrySchedule = Schedule.exponential("500 millis");
22
23const retryForbidden = <A, E extends { _tag: string }, R>(
24 effect: Effect.Effect<A, E, R>,
25) =>
26 effect.pipe(
27 Effect.retry({
28 while: (e) => e._tag === "Forbidden",
29 schedule: forbiddenRetrySchedule,
30 times: 8,
31 }),
32 );
33
34const resolveAccountId = Effect.gen(function* () {
35 const { accountId } = yield* yield* CloudflareEnvironment;
36 return accountId;
37});
38
39// BYOIP prefixes can only be onboarded through a Cloudflare contract process
40// (LOA upload, IRR/RPKI validation, manual approval), so the full lifecycle
41// tests are gated behind env vars pointing at a BYOIP-entitled account:
42// CLOUDFLARE_TEST_BYOIP_CIDR — a CIDR onboardable by the account
43// CLOUDFLARE_TEST_BYOIP_ASN — the ASN to advertise under
44// CLOUDFLARE_TEST_BYOIP_PREFIX_ID — an existing approved prefix id
45// CLOUDFLARE_TEST_BYOIP_DELEGATE_ACCOUNT_ID — second account for delegations
46const byoipCidr = process.env.CLOUDFLARE_TEST_BYOIP_CIDR;
47const byoipAsn = process.env.CLOUDFLARE_TEST_BYOIP_ASN;
48const byoipPrefixId = process.env.CLOUDFLARE_TEST_BYOIP_PREFIX_ID;
49const delegateAccountId = process.env.CLOUDFLARE_TEST_BYOIP_DELEGATE_ACCOUNT_ID;
50
51// The read-only catalog endpoints are available regardless of the BYOIP
52// entitlement — exercise the distilled wiring live on every run.
53test.provider(
54 "lists the services catalog and prefixes (read-only)",
55 (stack) =>
56 Effect.gen(function* () {
57 const accountId = yield* resolveAccountId;
58
59 yield* stack.destroy();
60
61 const services = yield* retryForbidden(
62 addressing.listServices.items({ accountId }).pipe(
63 Stream.runCollect,
64 Effect.map((c) => Array.from(c)),
65 ),
66 );
67 expect(Array.isArray(services)).toBe(true);
68
69 const prefixes = yield* retryForbidden(
70 addressing.listPrefixes.items({ accountId }).pipe(
71 Stream.runCollect,
72 Effect.map((c) => Array.from(c)),
73 ),
74 );
75 expect(Array.isArray(prefixes)).toBe(true);
76
77 yield* stack.destroy();
78 }).pipe(logLevel),
79 { tags: ["provider:cloudflare", "provider:cloudflare:addressing", "live"] },
80);
81
82// `list()` enumerates account-scoped BYOIP prefixes via the catalog endpoint,
83// which is available regardless of the BYOIP entitlement (it returns an empty
84// array on accounts with no onboarded prefixes). The result is a well-typed
85// `PrefixAttributes[]` — the exact shape `read` produces.
86test.provider(
87 "list enumerates account prefixes (read-only)",
88 (stack) =>
89 Effect.gen(function* () {
90 yield* stack.destroy();
91
92 const provider = yield* Provider.findProvider(
93 Cloudflare.Addressing.Prefix,
94 );
95 const all = yield* retryForbidden(provider.list());
96
97 expect(Array.isArray(all)).toBe(true);
98 for (const p of all) {
99 expect(typeof p.prefixId).toBe("string");
100 expect(typeof p.accountId).toBe("string");
101 expect(typeof p.cidr).toBe("string");
102 expect(typeof p.asn).toBe("number");
103 }
104
105 yield* stack.destroy();
106 }).pipe(logLevel),
107 { tags: ["provider:cloudflare", "provider:cloudflare:addressing", "live"] },
108);
109
110test.provider.skipIf(!byoipCidr || !byoipAsn)(
111 "prefix: create, patch description in place, destroy",
112 (stack) =>
113 Effect.gen(function* () {
114 const accountId = yield* resolveAccountId;
115 const cidr = byoipCidr!;
116 const asn = Number(byoipAsn!);
117
118 yield* stack.destroy();
119
120 const created = yield* stack.deploy(
121 Effect.gen(function* () {
122 return yield* Cloudflare.Addressing.Prefix("Byoip", {
123 cidr,
124 asn,
125 description: "alchemy v1",
126 delegateLoaCreation: true,
127 });
128 }),
129 );
130 expect(created.prefixId).toBeDefined();
131 expect(created.cidr).toEqual(cidr);
132 expect(created.asn).toEqual(asn);
133 expect(created.description).toEqual("alchemy v1");
134
135 // Out-of-band verification.
136 const live = yield* retryForbidden(
137 addressing.getPrefix({ accountId, prefixId: created.prefixId }),
138 );
139 expect(live.cidr).toEqual(cidr);
140 expect(live.description).toEqual("alchemy v1");
141
142 // Update the description in place — same physical prefix.
143 const updated = yield* stack.deploy(
144 Effect.gen(function* () {
145 return yield* Cloudflare.Addressing.Prefix("Byoip", {
146 cidr,
147 asn,
148 description: "alchemy v2",
149 delegateLoaCreation: true,
150 });
151 }),
152 );
153 expect(updated.prefixId).toEqual(created.prefixId);
154 expect(updated.description).toEqual("alchemy v2");
155
156 // Destroy and verify gone via the typed not-found read.
157 yield* stack.destroy();
158 const gone = yield* retryForbidden(
159 addressing.getPrefix({ accountId, prefixId: created.prefixId }),
160 ).pipe(
161 Effect.catchTag("PrefixNotFound", () => Effect.succeed(undefined)),
162 );
163 expect(gone).toBeUndefined();
164 }).pipe(logLevel),
165 {
166 tags: ["provider:cloudflare", "provider:cloudflare:addressing", "live"],
167 timeout: 120_000,
168 },
169);
170
171test.provider.skipIf(!byoipPrefixId)(
172 "bgp prefix: adopt-or-create by cidr, toggle advertisement, withdraw on destroy",
173 (stack) =>
174 Effect.gen(function* () {
175 const accountId = yield* resolveAccountId;
176 const prefixId = byoipPrefixId!;
177
178 const prefix = yield* retryForbidden(
179 addressing.getPrefix({ accountId, prefixId }),
180 );
181 const cidr = prefix.cidr!;
182
183 yield* stack.destroy();
184
185 // Cloudflare auto-creates BGP prefixes during onboarding — the
186 // resource adopts the one matching the CIDR rather than duplicating.
187 const created = yield* stack.deploy(
188 Effect.gen(function* () {
189 return yield* Cloudflare.Addressing.BgpPrefix("Bgp", {
190 prefixId,
191 cidr,
192 advertised: false,
193 });
194 }),
195 );
196 expect(created.bgpPrefixId).toBeDefined();
197 expect(created.cidr).toEqual(cidr);
198 expect(created.onDemand.advertised).toEqual(false);
199
200 // Advertise in place — same BGP prefix; propagation is eventually
201 // consistent so only the API acknowledgement is asserted.
202 const advertised = yield* stack.deploy(
203 Effect.gen(function* () {
204 return yield* Cloudflare.Addressing.BgpPrefix("Bgp", {
205 prefixId,
206 cidr,
207 advertised: true,
208 });
209 }),
210 );
211 expect(advertised.bgpPrefixId).toEqual(created.bgpPrefixId);
212 expect(advertised.onDemand.advertised).toEqual(true);
213
214 // Destroy withdraws the advertisement (no delete API exists).
215 yield* stack.destroy();
216 const after = yield* retryForbidden(
217 addressing.getPrefixBgpPrefix({
218 accountId,
219 prefixId,
220 bgpPrefixId: created.bgpPrefixId,
221 }),
222 );
223 expect(after.onDemand?.advertised ?? false).toEqual(false);
224 }).pipe(logLevel),
225 {
226 tags: ["provider:cloudflare", "provider:cloudflare:addressing", "live"],
227 timeout: 120_000,
228 },
229);
230
231test.provider.skipIf(!byoipPrefixId || !delegateAccountId)(
232 "prefix delegation: create and destroy (replace-only resource)",
233 (stack) =>
234 Effect.gen(function* () {
235 const accountId = yield* resolveAccountId;
236 const prefixId = byoipPrefixId!;
237
238 const prefix = yield* retryForbidden(
239 addressing.getPrefix({ accountId, prefixId }),
240 );
241 const cidr = prefix.cidr!;
242
243 yield* stack.destroy();
244
245 const created = yield* stack.deploy(
246 Effect.gen(function* () {
247 return yield* Cloudflare.Addressing.PrefixDelegation("Share", {
248 prefixId,
249 cidr,
250 delegatedAccountId: delegateAccountId!,
251 });
252 }),
253 );
254 expect(created.delegationId).toBeDefined();
255 expect(created.cidr).toEqual(cidr);
256 expect(created.delegatedAccountId).toEqual(delegateAccountId);
257
258 // Out-of-band verification via the list endpoint (no get op).
259 const delegations = yield* retryForbidden(
260 addressing.listPrefixDelegations.items({ accountId, prefixId }).pipe(
261 Stream.runCollect,
262 Effect.map((c) => Array.from(c)),
263 ),
264 );
265 expect(delegations.some((d) => d.id === created.delegationId)).toBe(true);
266
267 yield* stack.destroy();
268 const after = yield* retryForbidden(
269 addressing.listPrefixDelegations.items({ accountId, prefixId }).pipe(
270 Stream.runCollect,
271 Effect.map((c) => Array.from(c)),
272 ),
273 );
274 expect(after.some((d) => d.id === created.delegationId)).toBe(false);
275 }).pipe(logLevel),
276 {
277 tags: ["provider:cloudflare", "provider:cloudflare:addressing", "live"],
278 timeout: 120_000,
279 },
280);
281
282test.provider.skipIf(!byoipPrefixId)(
283 "service binding: create against the CDN service and destroy",
284 (stack) =>
285 Effect.gen(function* () {
286 const accountId = yield* resolveAccountId;
287 const prefixId = byoipPrefixId!;
288
289 const prefix = yield* retryForbidden(
290 addressing.getPrefix({ accountId, prefixId }),
291 );
292 const cidr = prefix.cidr!;
293
294 const services = yield* retryForbidden(
295 addressing.listServices.items({ accountId }).pipe(
296 Stream.runCollect,
297 Effect.map((c) => Array.from(c)),
298 ),
299 );
300 const cdn = services.find((s) => s.name === "CDN") ?? services[0];
301 expect(cdn?.id).toBeDefined();
302
303 yield* stack.destroy();
304
305 const created = yield* stack.deploy(
306 Effect.gen(function* () {
307 return yield* Cloudflare.Addressing.ServiceBinding("Cdn", {
308 prefixId,
309 cidr,
310 serviceId: cdn!.id!,
311 });
312 }),
313 );
314 expect(created.bindingId).toBeDefined();
315 expect(created.serviceId).toEqual(cdn!.id);
316 // Provisioning to the edge is asynchronous.
317 expect(["provisioning", "active"]).toContain(
318 created.provisioning.state ?? "provisioning",
319 );
320
321 // Out-of-band verification.
322 const live = yield* retryForbidden(
323 addressing.getPrefixServiceBinding({
324 accountId,
325 prefixId,
326 bindingId: created.bindingId,
327 }),
328 );
329 expect(live.cidr).toEqual(cidr);
330
331 yield* stack.destroy();
332 const gone = yield* retryForbidden(
333 addressing.getPrefixServiceBinding({
334 accountId,
335 prefixId,
336 bindingId: created.bindingId,
337 }),
338 ).pipe(
339 Effect.catchTag(["BindingNotFound", "PrefixNotFound"], () =>
340 Effect.succeed(undefined),
341 ),
342 );
343 expect(gone).toBeUndefined();
344 }).pipe(logLevel),
345 {
346 tags: ["provider:cloudflare", "provider:cloudflare:addressing", "live"],
347 timeout: 120_000,
348 },
349);