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 hostnames from "@distilled.cloud/cloudflare/hostnames";
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// Per-hostname TLS settings are gated behind Advanced Certificate Manager
23// (or Cloudflare for SaaS) — on the standard testing zone every PUT/DELETE
24// fails with Cloudflare code 1450, surfaced as the typed
25// `AdvancedCertificateManagerRequired` error. The full lifecycle test below
26// is gated behind an entitled zone + hostname supplied via env.
27const acmZoneId = process.env.CLOUDFLARE_TEST_ACM_ZONE_ID;
28const acmHostname = process.env.CLOUDFLARE_TEST_ACM_HOSTNAME;
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
41// Freshly-minted scoped tokens propagate eventually-consistently across
42// Cloudflare's edge — ride out intermittent 403 blips on the test's
43// out-of-band calls by retrying the typed `Forbidden` error.
44const forbiddenRetrySchedule = Schedule.exponential("500 millis");
45
46test.provider(
47 "lists overrides and surfaces the typed AdvancedCertificateManagerRequired error on unentitled zones",
48 (stack) =>
49 Effect.gen(function* () {
50 const zoneId = yield* resolveZoneId;
51
52 yield* stack.destroy();
53
54 // Reading a hostname's override works on any zone; the standing test
55 // zone has no overrides, so the probe resolves to "none".
56 const observed = yield* hostnames
57 .listSettingsTls({
58 zoneId,
59 settingId: "min_tls_version",
60 })
61 .pipe(
62 Effect.map((settings) =>
63 settings.find(
64 (setting) => setting.hostname === `alchemy-htls-gate.${zoneName}`,
65 ),
66 ),
67 );
68 expect(observed).toBeUndefined();
69
70 // The standard testing zone lacks the ACM entitlement — a write must
71 // fail with the typed entitlement tag (Cloudflare code 1450).
72 const error = yield* hostnames
73 .putSettingTls({
74 zoneId,
75 settingId: "min_tls_version",
76 hostname: `alchemy-htls-gate.${zoneName}`,
77 value: "1.2",
78 })
79 .pipe(
80 Effect.retry({
81 while: (e) => e._tag === "Forbidden",
82 schedule: forbiddenRetrySchedule,
83 times: 8,
84 }),
85 Effect.flip,
86 );
87 expect(error._tag).toEqual("AdvancedCertificateManagerRequired");
88
89 yield* stack.destroy();
90 }).pipe(logLevel),
91 {
92 tags: [
93 "provider:cloudflare",
94 "provider:cloudflare:hostnametlssetting",
95 "provider:cloudflare:zone",
96 "live",
97 ],
98 },
99);
100
101// Canonical `list()` test (zone-scoped collection): there is no account-wide
102// API for per-hostname overrides, so `list()` enumerates every zone via
103// `listAllZones` and lists the overrides for each of the three TLS settings.
104// The standing test zone has no ACM entitlement and therefore
105// no overrides, so the well-typed result is normally empty; when an entitled
106// zone + hostname is supplied via env we deploy one and assert its presence.
107test.provider(
108 "list enumerates per-hostname TLS overrides",
109 (stack) =>
110 Effect.gen(function* () {
111 yield* stack.destroy();
112
113 if (acmZoneId && acmHostname) {
114 yield* stack.deploy(
115 Effect.gen(function* () {
116 return yield* Cloudflare.HostnameTlsSetting.HostnameTlsSetting(
117 "ListMinTls",
118 {
119 zoneId: acmZoneId,
120 settingId: "min_tls_version",
121 hostname: acmHostname,
122 value: "1.2",
123 },
124 );
125 }),
126 );
127 }
128
129 const provider = yield* Provider.findProvider(
130 Cloudflare.HostnameTlsSetting.HostnameTlsSetting,
131 );
132 const all = yield* provider.list();
133
134 // Always a well-typed array (possibly empty on an unentitled account).
135 expect(Array.isArray(all)).toBe(true);
136
137 if (acmZoneId && acmHostname) {
138 expect(
139 all.some(
140 (s) =>
141 s.zoneId === acmZoneId &&
142 s.settingId === "min_tls_version" &&
143 s.hostname === acmHostname,
144 ),
145 ).toBe(true);
146 }
147
148 yield* stack.destroy();
149 }).pipe(logLevel),
150 {
151 tags: [
152 "provider:cloudflare",
153 "provider:cloudflare:hostnametlssetting",
154 "live",
155 ],
156 timeout: 120_000,
157 },
158);
159
160test.provider.skipIf(!acmZoneId || !acmHostname)(
161 "create, update in place, and destroy a min_tls_version override",
162 (stack) =>
163 Effect.gen(function* () {
164 const zoneId = acmZoneId!;
165 const hostname = acmHostname!;
166
167 yield* stack.destroy();
168
169 const created = yield* stack.deploy(
170 Effect.gen(function* () {
171 return yield* Cloudflare.HostnameTlsSetting.HostnameTlsSetting(
172 "MinTls",
173 {
174 zoneId,
175 settingId: "min_tls_version",
176 hostname,
177 value: "1.2",
178 },
179 );
180 }),
181 );
182
183 expect(created.zoneId).toEqual(zoneId);
184 expect(created.settingId).toEqual("min_tls_version");
185 expect(created.hostname).toEqual(hostname);
186 expect(created.value).toEqual("1.2");
187
188 // Out-of-band verification via the distilled API.
189 const live = yield* hostnames
190 .listSettingsTls({
191 zoneId,
192 settingId: "min_tls_version",
193 })
194 .pipe(
195 Effect.map((settings) =>
196 settings.find((setting) => setting.hostname === hostname),
197 ),
198 );
199 expect(live).toBeDefined();
200 expect(live!.value).toEqual("1.2");
201
202 // Update in place — PUT upserts the same (settingId, hostname) pair.
203 const updated = yield* stack.deploy(
204 Effect.gen(function* () {
205 return yield* Cloudflare.HostnameTlsSetting.HostnameTlsSetting(
206 "MinTls",
207 {
208 zoneId,
209 settingId: "min_tls_version",
210 hostname,
211 value: "1.3",
212 },
213 );
214 }),
215 );
216 expect(updated.hostname).toEqual(hostname);
217 expect(updated.value).toEqual("1.3");
218
219 const liveUpdated = yield* hostnames
220 .listSettingsTls({
221 zoneId,
222 settingId: "min_tls_version",
223 })
224 .pipe(
225 Effect.map((settings) =>
226 settings.find((setting) => setting.hostname === hostname),
227 ),
228 );
229 expect(liveUpdated!.value).toEqual("1.3");
230
231 yield* stack.destroy();
232
233 // Removal is eventually consistent — poll the GET (bounded) until
234 // the override disappears and the hostname reverts to zone defaults.
235 const gone = yield* hostnames
236 .listSettingsTls({ zoneId, settingId: "min_tls_version" })
237 .pipe(
238 Effect.map((settings) =>
239 settings.find((setting) => setting.hostname === hostname),
240 ),
241 Effect.repeat({
242 schedule: Schedule.spaced("3 seconds"),
243 until: (entry) => entry === undefined,
244 times: 10,
245 }),
246 );
247 expect(gone).toBeUndefined();
248 }).pipe(logLevel),
249 {
250 tags: [
251 "provider:cloudflare",
252 "provider:cloudflare:hostnametlssetting",
253 "live",
254 ],
255 timeout: 120_000,
256 },
257);