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 lcc from "@distilled.cloud/cloudflare/leaked-credential-checks";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11const { test } = Test.make({ providers: Cloudflare.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const zoneName =
19 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
20
21// Custom detection locations are plan-gated — on the testing account's free
22// zone the quota is zero and every create fails with the typed
23// `DetectionQuotaExceeded` error ("exceeded the maximum number of rules:
24// 1 out of 0"). The full detection lifecycle test below is gated behind an
25// entitled zone id supplied via env.
26const detectionZoneId = process.env.CLOUDFLARE_TEST_LCC_DETECTION_ZONE_ID;
27
28const resolveZoneId = Effect.gen(function* () {
29 const { accountId } = yield* yield* CloudflareEnvironment;
30 const zone = yield* findZoneByName({ accountId, name: zoneName });
31 if (!zone) {
32 return yield* Effect.die(
33 new Error(`zone "${zoneName}" not found in account`),
34 );
35 }
36 return zone.id;
37});
38
39// The scoped API token the test harness mints propagates eventually-
40// consistently across Cloudflare's edge — a fresh token intermittently 403s.
41// Ride out the blips on the test's own out-of-band calls by retrying the
42// typed `Forbidden` error (part of each operation's error union via
43// distilled patches).
44const forbiddenRetrySchedule = Schedule.exponential("500 millis");
45
46const getCheck = (zoneId: string) =>
47 lcc.getLeakedCredentialCheck({ zoneId }).pipe(
48 Effect.retry({
49 while: (e) => e._tag === "Forbidden",
50 schedule: forbiddenRetrySchedule,
51 times: 8,
52 }),
53 );
54
55const setBaseline = (zoneId: string, enabled: boolean) =>
56 lcc.createLeakedCredentialCheck({ zoneId, enabled }).pipe(
57 Effect.retry({
58 while: (e) => e._tag === "Forbidden",
59 schedule: forbiddenRetrySchedule,
60 times: 8,
61 }),
62 );
63
64// Both cases mutate the same zone-level Leaked Credential Check singleton; run them serially so they don't corrupt each other's captured `initialEnabled` under the global concurrent test config.
65describe.sequential(
66 "LeakedCredentialCheck",
67 {
68 tags: [
69 "provider:cloudflare",
70 "provider:cloudflare:leakedcredentialcheck",
71 "live",
72 ],
73 },
74 () => {
75 test.provider(
76 "enables leaked credential checks and restores the baseline on destroy",
77 (stack) =>
78 Effect.gen(function* () {
79 const zoneId = yield* resolveZoneId;
80
81 yield* stack.destroy();
82 // Known baseline: the check defaults to disabled.
83 yield* setBaseline(zoneId, false);
84
85 const check = yield* stack.deploy(
86 Effect.gen(function* () {
87 return yield* Cloudflare.LeakedCredentialCheck.LeakedCredentialCheck(
88 "Lcc",
89 {
90 zoneId,
91 },
92 );
93 }),
94 );
95
96 expect(check.zoneId).toEqual(zoneId);
97 expect(check.enabled).toEqual(true);
98 // The pre-management value was captured for restore-on-destroy.
99 expect(check.initialEnabled).toEqual(false);
100
101 // Out-of-band verification via the distilled API.
102 const live = yield* getCheck(zoneId);
103 expect(live.enabled).toEqual(true);
104
105 // Update in place — same singleton, initialEnabled survives.
106 const updated = yield* stack.deploy(
107 Effect.gen(function* () {
108 return yield* Cloudflare.LeakedCredentialCheck.LeakedCredentialCheck(
109 "Lcc",
110 {
111 zoneId,
112 enabled: false,
113 },
114 );
115 }),
116 );
117 expect(updated.enabled).toEqual(false);
118 expect(updated.initialEnabled).toEqual(false);
119
120 const disabled = yield* getCheck(zoneId);
121 expect(disabled.enabled).toEqual(false);
122
123 // Flip back on so destroy has something to restore.
124 const reEnabled = yield* stack.deploy(
125 Effect.gen(function* () {
126 return yield* Cloudflare.LeakedCredentialCheck.LeakedCredentialCheck(
127 "Lcc",
128 {
129 zoneId,
130 enabled: true,
131 },
132 );
133 }),
134 );
135 expect(reEnabled.enabled).toEqual(true);
136 expect(reEnabled.initialEnabled).toEqual(false);
137
138 yield* stack.destroy();
139
140 // Destroy restored the value the check had before we managed it.
141 const restored = yield* getCheck(zoneId);
142 expect(restored.enabled).toEqual(false);
143 }).pipe(logLevel),
144 { tags: ["provider:cloudflare:zone"] },
145 );
146
147 test.provider(
148 "surfaces the typed DetectionQuotaExceeded error on unentitled zones",
149 (stack) =>
150 Effect.gen(function* () {
151 const zoneId = yield* resolveZoneId;
152
153 yield* stack.destroy();
154
155 // Detection operations require the zone toggle to be on.
156 yield* setBaseline(zoneId, true);
157
158 // The standard testing zone has a zero custom-detection quota — the
159 // distilled call must fail with the typed quota tag.
160 const error = yield* lcc
161 .createDetection({
162 zoneId,
163 username: 'lookup_json_string(http.request.body.raw, "user")',
164 password: 'lookup_json_string(http.request.body.raw, "secret")',
165 })
166 .pipe(
167 Effect.retry({
168 while: (e) => e._tag === "Forbidden",
169 schedule: forbiddenRetrySchedule,
170 times: 8,
171 }),
172 Effect.flip,
173 );
174 expect(error._tag).toEqual("DetectionQuotaExceeded");
175
176 // Restore the zone's baseline (toggle off).
177 yield* setBaseline(zoneId, false);
178
179 yield* stack.destroy();
180 }).pipe(logLevel),
181 { tags: ["provider:cloudflare:zone"] },
182 );
183
184 // Canonical `list()` test (zone-scoped singleton): there is no account-wide
185 // API for this per-zone setting, so `list()` enumerates every zone via
186 // `listAllZones` and reads the singleton in each. Assert the result is
187 // non-empty and contains the standing test zone.
188 test.provider(
189 "list enumerates the check across all zones",
190 (stack) =>
191 Effect.gen(function* () {
192 const zoneId = yield* resolveZoneId;
193
194 const provider = yield* Provider.findProvider(
195 Cloudflare.LeakedCredentialCheck.LeakedCredentialCheck,
196 );
197 const all = yield* provider.list();
198
199 expect(all.length).toBeGreaterThan(0);
200 expect(all.some((s) => s.zoneId === zoneId)).toBe(true);
201
202 // `stack` is unused here (the singleton always exists on every zone),
203 // but keep the destroy bookends so the harness state stays clean.
204 yield* stack.destroy();
205 }).pipe(logLevel),
206 { tags: ["provider:cloudflare:zone"] },
207 );
208
209 // Requires a zone with a non-zero custom-detection quota (plan-gated) — the standard
210 // zone fails with the typed DetectionQuotaExceeded. Unlock with CLOUDFLARE_TEST_LCC_DETECTION_ZONE_ID=<zone id>.
211 test.provider.skipIf(!detectionZoneId)(
212 "creates, updates, and destroys a custom detection",
213 (stack) =>
214 Effect.gen(function* () {
215 const zoneId = detectionZoneId!;
216
217 yield* stack.destroy();
218
219 const usernameExpr =
220 'lookup_json_string(http.request.body.raw, "user")';
221 const passwordExpr =
222 'lookup_json_string(http.request.body.raw, "pass")';
223
224 const detection = yield* stack.deploy(
225 Effect.gen(function* () {
226 const check =
227 yield* Cloudflare.LeakedCredentialCheck.LeakedCredentialCheck(
228 "Lcc",
229 {
230 zoneId,
231 enabled: true,
232 },
233 );
234 return yield* Cloudflare.LeakedCredentialCheck.LeakedCredentialDetection(
235 "Det",
236 {
237 // Depend on the check so the toggle deploys first.
238 zoneId: check.zoneId,
239 username: usernameExpr,
240 password: passwordExpr,
241 },
242 );
243 }),
244 );
245
246 expect(detection.detectionId).not.toEqual("");
247 expect(detection.zoneId).toEqual(zoneId);
248 expect(detection.username).toEqual(usernameExpr);
249 expect(detection.password).toEqual(passwordExpr);
250
251 // Out-of-band verification via the distilled API.
252 const live = yield* lcc.getDetection({
253 zoneId,
254 detectionId: detection.detectionId,
255 });
256 expect(live.username).toEqual(usernameExpr);
257
258 // In-place update — the PUT keeps the same detection id.
259 const newPasswordExpr =
260 'lookup_json_string(http.request.body.raw, "secret")';
261 const updated = yield* stack.deploy(
262 Effect.gen(function* () {
263 const check =
264 yield* Cloudflare.LeakedCredentialCheck.LeakedCredentialCheck(
265 "Lcc",
266 {
267 zoneId,
268 enabled: true,
269 },
270 );
271 return yield* Cloudflare.LeakedCredentialCheck.LeakedCredentialDetection(
272 "Det",
273 {
274 zoneId: check.zoneId,
275 username: usernameExpr,
276 password: newPasswordExpr,
277 },
278 );
279 }),
280 );
281 expect(updated.detectionId).toEqual(detection.detectionId);
282 expect(updated.password).toEqual(newPasswordExpr);
283
284 yield* stack.destroy();
285
286 // The detection is gone — the typed not-found tag proves it.
287 const error = yield* lcc
288 .getDetection({ zoneId, detectionId: detection.detectionId })
289 .pipe(Effect.flip);
290 expect([
291 "DetectionNotFound",
292 "LeakedCredentialChecksDisabled",
293 ]).toContain(error._tag);
294 }).pipe(logLevel),
295 );
296 },
297);