1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as dns from "@distilled.cloud/cloudflare/dns";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: Cloudflare.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18// Ride out fresh-token 403 blips on out-of-band calls.
19const retryForbidden = <A, E extends { _tag: string }, R>(
20 eff: Effect.Effect<A, E, R>,
21) =>
22 eff.pipe(
23 Effect.retry({
24 while: (e) => e._tag === "Forbidden",
25 schedule: Schedule.exponential("500 millis"),
26 times: 8,
27 }),
28 );
29
30const getAcl = (accountId: string, aclId: string) =>
31 retryForbidden(dns.getZoneTransferAcl({ accountId, aclId }));
32
33// A deleted ACL surfaces as the typed `AclNotFound` (HTTP 404) — poll
34// until the GET reports it gone.
35const expectGone = (accountId: string, aclId: string) =>
36 getAcl(accountId, aclId).pipe(
37 Effect.flatMap(() => Effect.fail({ _tag: "AclNotDeleted" } as const)),
38 Effect.catchTag("AclNotFound", () => Effect.void),
39 Effect.retry({
40 while: (e) => e._tag === "AclNotDeleted",
41 schedule: Schedule.max([
42 Schedule.exponential("500 millis"),
43 Schedule.recurs(10),
44 ]),
45 }),
46 );
47
48test.provider(
49 "create, update in place, and delete a zone transfer ACL",
50 (stack) =>
51 Effect.gen(function* () {
52 const { accountId } = yield* yield* CloudflareEnvironment;
53
54 yield* stack.destroy();
55
56 const created = yield* stack.deploy(
57 Cloudflare.DNS.ZoneTransferAcl("TestAcl", {
58 name: "alchemy-dnszt-acl-test",
59 // Cloudflare normalizes the range to its network address —
60 // use one that is already normalized for stable diffs.
61 ipRange: "192.0.2.48/28",
62 }),
63 );
64 expect(created.aclId).toBeDefined();
65 expect(created.accountId).toEqual(accountId);
66 expect(created.name).toEqual("alchemy-dnszt-acl-test");
67 expect(created.ipRange).toEqual("192.0.2.48/28");
68
69 // Out-of-band verify via the SDK.
70 const live = yield* getAcl(accountId, created.aclId);
71 expect(live.name).toEqual("alchemy-dnszt-acl-test");
72 expect(live.ipRange).toEqual("192.0.2.48/28");
73
74 // Update both mutable fields in place — same physical ACL.
75 const updated = yield* stack.deploy(
76 Cloudflare.DNS.ZoneTransferAcl("TestAcl", {
77 name: "alchemy-dnszt-acl-test-renamed",
78 ipRange: "198.51.100.0/28",
79 }),
80 );
81 expect(updated.aclId).toEqual(created.aclId);
82 expect(updated.name).toEqual("alchemy-dnszt-acl-test-renamed");
83 expect(updated.ipRange).toEqual("198.51.100.0/28");
84
85 const liveUpdated = yield* getAcl(accountId, created.aclId);
86 expect(liveUpdated.name).toEqual("alchemy-dnszt-acl-test-renamed");
87 expect(liveUpdated.ipRange).toEqual("198.51.100.0/28");
88
89 yield* stack.destroy();
90 yield* expectGone(accountId, created.aclId);
91
92 // Re-running destroy is idempotent.
93 yield* stack.destroy();
94 }).pipe(logLevel),
95 {
96 tags: ["provider:cloudflare", "provider:cloudflare:dns", "live"],
97 timeout: 120_000,
98 },
99);
100
101test.provider(
102 "generates a deterministic name when none is provided",
103 (stack) =>
104 Effect.gen(function* () {
105 const { accountId } = yield* yield* CloudflareEnvironment;
106
107 yield* stack.destroy();
108
109 const created = yield* stack.deploy(
110 Cloudflare.DNS.ZoneTransferAcl("DefaultNameAcl", {
111 ipRange: "203.0.113.0/28",
112 }),
113 );
114 expect(created.name).toBeDefined();
115 expect(created.name.length).toBeGreaterThan(0);
116 expect(created.ipRange).toEqual("203.0.113.0/28");
117
118 yield* stack.destroy();
119 yield* expectGone(accountId, created.aclId);
120 }).pipe(logLevel),
121 {
122 tags: ["provider:cloudflare", "provider:cloudflare:dns", "live"],
123 timeout: 120_000,
124 },
125);
126
127test.provider(
128 "list enumerates the deployed zone transfer ACLs",
129 (stack) =>
130 Effect.gen(function* () {
131 yield* stack.destroy();
132
133 const deployed = yield* stack.deploy(
134 Cloudflare.DNS.ZoneTransferAcl("ListAcl", {
135 name: "alchemy-dnszt-acl-list",
136 ipRange: "192.0.2.64/28",
137 }),
138 );
139
140 const provider = yield* Provider.findProvider(
141 Cloudflare.DNS.ZoneTransferAcl,
142 );
143 const all = yield* provider.list();
144
145 // Exhaustively-paginated result contains the deployed ACL, in the
146 // exact `read` Attributes shape.
147 const found = all.find((a) => a.aclId === deployed.aclId);
148 expect(found).toBeDefined();
149 expect(found?.accountId).toEqual(deployed.accountId);
150 expect(found?.name).toEqual("alchemy-dnszt-acl-list");
151 expect(found?.ipRange).toEqual("192.0.2.64/28");
152
153 yield* stack.destroy();
154 }).pipe(logLevel),
155 {
156 tags: ["provider:cloudflare", "provider:cloudflare:dns", "live"],
157 timeout: 120_000,
158 },
159);