1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as registrar from "@distilled.cloud/cloudflare/registrar";
6import { describe, 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";
11const { test } = Test.make({ providers: Cloudflare.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18// A domain registered through Cloudflare Registrar on the testing account.
19// Registrations cannot be created via the API, so the test adopts whatever
20// is already there and always restores it on the way out.
21const domainName =
22 process.env.CLOUDFLARE_TEST_REGISTRAR_DOMAIN ?? "alchemy-test-3.us";
23
24// The scoped API token the test harness mints propagates eventually-
25// consistently across Cloudflare's edge — ride out 403 blips on the test's
26// own out-of-band calls by retrying the typed `Forbidden` error.
27const forbiddenRetrySchedule = Schedule.exponential("500 millis");
28
29const findDomain = (accountId: string) =>
30 registrar.listDomains.items({ accountId }).pipe(
31 Stream.runCollect,
32 Effect.map((chunk) =>
33 Array.from(chunk).find((domain) => domain.name === domainName),
34 ),
35 Effect.retry({
36 while: (e) => e._tag === "Forbidden",
37 schedule: forbiddenRetrySchedule,
38 times: 8,
39 }),
40 );
41
42// Both cases mutate the same registered domain's settings; run them serially so they don't corrupt each other's captured `initialSettings` under the global concurrent test config.
43describe.sequential(
44 "Domain",
45 { tags: ["provider:cloudflare", "provider:cloudflare:registrar", "live"] },
46 () => {
47 test.provider(
48 "adopts a registered domain, no-op syncs, and never releases it on destroy",
49 (stack) =>
50 Effect.gen(function* () {
51 const { accountId } = yield* yield* CloudflareEnvironment;
52
53 const baseline = yield* findDomain(accountId);
54 if (!baseline) {
55 yield* Effect.log(
56 `skipping: no Cloudflare Registrar domain "${domainName}" on account ${accountId}`,
57 );
58 return;
59 }
60
61 yield* stack.destroy();
62
63 // Declare the settings the domain already has — a pure adoption with
64 // a no-op sync, so no registrar write permission is needed.
65 const props = {
66 domainName,
67 autoRenew: baseline.autoRenew ?? undefined,
68 locked: baseline.locked ?? undefined,
69 privacy: baseline.privacy ?? undefined,
70 };
71
72 const domain = yield* stack.deploy(
73 Effect.gen(function* () {
74 return yield* Cloudflare.Registrar.Domain("Domain", props);
75 }),
76 );
77
78 expect(domain.domainName).toEqual(domainName);
79 expect(domain.accountId).toEqual(accountId);
80 expect(domain.autoRenew).toEqual(baseline.autoRenew ?? undefined);
81 expect(domain.locked).toEqual(baseline.locked ?? undefined);
82 expect(domain.privacy).toEqual(baseline.privacy ?? undefined);
83 expect(domain.currentRegistrar).toEqual("Cloudflare");
84 expect(domain.supportedTld).toEqual(true);
85 expect(domain.expiresAt).toBeDefined();
86 // The pre-management settings were captured for restore-on-destroy.
87 expect(domain.initialSettings).toEqual({
88 autoRenew: baseline.autoRenew ?? undefined,
89 locked: baseline.locked ?? undefined,
90 privacy: baseline.privacy ?? undefined,
91 });
92
93 // Idempotent redeploy — still a no-op sync, initialSettings survive.
94 const again = yield* stack.deploy(
95 Effect.gen(function* () {
96 return yield* Cloudflare.Registrar.Domain("Domain", props);
97 }),
98 );
99 expect(again.domainName).toEqual(domainName);
100 expect(again.initialSettings).toEqual(domain.initialSettings);
101
102 yield* stack.destroy();
103
104 // Destroy must never release the registration — the domain is still
105 // there with its settings intact.
106 const after = yield* findDomain(accountId);
107 expect(after).toBeDefined();
108 expect(after?.autoRenew).toEqual(baseline.autoRenew);
109 expect(after?.locked).toEqual(baseline.locked);
110 expect(after?.privacy).toEqual(baseline.privacy);
111 }).pipe(logLevel),
112 { timeout: 120_000 },
113 );
114
115 test.provider(
116 "updates settings in place and restores the baseline on destroy",
117 (stack) =>
118 Effect.gen(function* () {
119 const { accountId } = yield* yield* CloudflareEnvironment;
120
121 const baseline = yield* findDomain(accountId);
122 if (!baseline) {
123 yield* Effect.log(
124 `skipping: no Cloudflare Registrar domain "${domainName}" on account ${accountId}`,
125 );
126 return;
127 }
128
129 // Probe write access with a same-value PUT (no actual change). The
130 // testing token lacks registrar write permission, which surfaces as
131 // the typed `RegistrarUpdateNotAllowed` (HTTP 422, "You are not
132 // allowed to perform this action") — skip the mutation flow then.
133 const probe = yield* registrar
134 .putDomain({
135 accountId,
136 domainName,
137 autoRenew: baseline.autoRenew ?? true,
138 })
139 .pipe(
140 Effect.as("allowed" as const),
141 Effect.catchTag("RegistrarUpdateNotAllowed", () =>
142 Effect.succeed("blocked" as const),
143 ),
144 Effect.retry({
145 while: (e) => e._tag === "Forbidden",
146 schedule: forbiddenRetrySchedule,
147 times: 8,
148 }),
149 );
150 if (probe === "blocked") {
151 yield* Effect.log(
152 "skipping: API token lacks Cloudflare Registrar write permission " +
153 "(putDomain failed with RegistrarUpdateNotAllowed)",
154 );
155 return;
156 }
157
158 yield* stack.destroy();
159
160 const flipped = !(baseline.autoRenew ?? true);
161
162 // In-place update: flip autoRenew away from the baseline.
163 const domain = yield* stack.deploy(
164 Effect.gen(function* () {
165 return yield* Cloudflare.Registrar.Domain("Domain", {
166 domainName,
167 autoRenew: flipped,
168 });
169 }),
170 );
171 expect(domain.autoRenew).toEqual(flipped);
172 expect(domain.initialSettings.autoRenew).toEqual(
173 baseline.autoRenew ?? undefined,
174 );
175
176 // Flip it back via an in-place update.
177 const updated = yield* stack.deploy(
178 Effect.gen(function* () {
179 return yield* Cloudflare.Registrar.Domain("Domain", {
180 domainName,
181 autoRenew: baseline.autoRenew ?? true,
182 });
183 }),
184 );
185 expect(updated.autoRenew).toEqual(baseline.autoRenew ?? true);
186 // The captured pre-management settings survive updates.
187 expect(updated.initialSettings.autoRenew).toEqual(
188 baseline.autoRenew ?? undefined,
189 );
190
191 yield* stack.destroy();
192
193 // Destroy restored the baseline settings and kept the registration.
194 const restored = yield* findDomain(accountId);
195 expect(restored).toBeDefined();
196 expect(restored?.autoRenew).toEqual(baseline.autoRenew);
197 }).pipe(logLevel),
198 { timeout: 120_000 },
199 );
200
201 // Registrar domains are real, pre-existing registrations that cannot be
202 // created via the API, so this is read-only: `list()` enumerates whatever
203 // is already on the account and we assert a well-typed Attributes[] (which
204 // may legitimately be empty if the account has no registered domains).
205 test.provider("list enumerates registrar domains on the account", (stack) =>
206 Effect.gen(function* () {
207 yield* stack.destroy();
208
209 const provider = yield* Provider.findProvider(
210 Cloudflare.Registrar.Domain,
211 );
212 const all = yield* provider.list();
213
214 expect(Array.isArray(all)).toBe(true);
215 for (const domain of all) {
216 expect(typeof domain.domainName).toBe("string");
217 expect(typeof domain.accountId).toBe("string");
218 expect(domain.initialSettings).toBeDefined();
219 }
220
221 yield* stack.destroy();
222 }).pipe(logLevel),
223 );
224 },
225);