| 1 | import * as Cloudflare from "@/Cloudflare"; |
| 2 | import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment"; |
| 3 | import * as Provider from "@/Provider"; |
| 4 | import * as Test from "@/Test/Alchemy"; |
| 5 | import * as addressing from "@distilled.cloud/cloudflare/addressing"; |
| 6 | import { expect } from "alchemy-test"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import { MinimumLogLevel } from "effect/References"; |
| 9 | import * as Schedule from "effect/Schedule"; |
| 10 | import * as Stream from "effect/Stream"; |
| 11 | |
| 12 | const { test } = Test.make({ providers: Cloudflare.providers() }); |
| 13 | |
| 14 | const 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. |
| 21 | const forbiddenRetrySchedule = Schedule.exponential("500 millis"); |
| 22 | |
| 23 | const 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 | |
| 34 | const 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 |
| 46 | const byoipCidr = process.env.CLOUDFLARE_TEST_BYOIP_CIDR; |
| 47 | const byoipAsn = process.env.CLOUDFLARE_TEST_BYOIP_ASN; |
| 48 | const byoipPrefixId = process.env.CLOUDFLARE_TEST_BYOIP_PREFIX_ID; |
| 49 | const 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. |
| 53 | test.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. |
| 86 | test.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 | |
| 110 | test.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 | |
| 171 | test.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 | |
| 231 | test.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 | |
| 282 | test.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 | ); |