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 cache from "@distilled.cloud/cloudflare/cache";
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
21const resolveZoneId = Effect.gen(function* () {
22 const { accountId } = yield* yield* CloudflareEnvironment;
23 const zone = yield* findZoneByName({ accountId, name: zoneName });
24 if (!zone) {
25 return yield* Effect.die(
26 new Error(`zone "${zoneName}" not found in account`),
27 );
28 }
29 return zone.id;
30});
31
32// The scoped API token the test harness mints propagates eventually-
33// consistently across Cloudflare's edge — a fresh token intermittently rejects
34// requests with `403 Forbidden` OR `401 Unauthorized` until it has propagated.
35// Ride out the blips on the test's own out-of-band calls by retrying both typed
36// auth errors (part of each cache operation's error union via distilled patches).
37const tokenPropagationRetrySchedule = Schedule.exponential("500 millis");
38const isTokenPropagationError = (e: { _tag: string }) =>
39 e._tag === "Forbidden" || e._tag === "Unauthorized";
40
41/**
42 * Observe the live setting: the configured value, or `undefined` when the
43 * setting does not exist (typed `VariantsNotConfigured`).
44 */
45const getVariants = (zoneId: string) =>
46 cache.getVariant({ zoneId }).pipe(
47 Effect.catchTag("VariantsNotConfigured", () => Effect.succeed(undefined)),
48 Effect.retry({
49 while: isTokenPropagationError,
50 schedule: tokenPropagationRetrySchedule,
51 times: 8,
52 }),
53 );
54
55// Normalize to a known baseline: the setting deleted (zone default).
56const resetBaseline = (zoneId: string) =>
57 cache.deleteVariant({ zoneId }).pipe(
58 Effect.catchTag("VariantsNotConfigured", () => Effect.succeed(undefined)),
59 Effect.retry({
60 while: isTokenPropagationError,
61 schedule: tokenPropagationRetrySchedule,
62 times: 8,
63 }),
64 );
65
66describe.sequential(
67 "Variants",
68 {
69 tags: [
70 "provider:cloudflare",
71 "provider:cloudflare:cache",
72 "provider:cloudflare:zone",
73 "live",
74 ],
75 },
76 () => {
77 test.provider(
78 "creates, updates in place, and deletes the variants setting",
79 (stack) =>
80 Effect.gen(function* () {
81 const zoneId = yield* resolveZoneId;
82
83 yield* stack.destroy();
84 // Known baseline: the setting does not exist.
85 yield* resetBaseline(zoneId);
86
87 const created = yield* stack.deploy(
88 Effect.gen(function* () {
89 return yield* Cloudflare.Cache.Variants("ImageVariants", {
90 zoneId,
91 jpeg: ["image/webp"],
92 });
93 }),
94 );
95
96 expect(created.zoneId).toEqual(zoneId);
97 expect(created.value).toEqual({ jpeg: ["image/webp"] });
98 expect(created.editable).toEqual(true);
99
100 // Out-of-band verification via the distilled API.
101 const live = yield* getVariants(zoneId);
102 expect(live).toBeDefined();
103 expect(live!.value.jpeg).toEqual(["image/webp"]);
104
105 // Update in place — PATCH replaces the full value object.
106 const updated = yield* stack.deploy(
107 Effect.gen(function* () {
108 return yield* Cloudflare.Cache.Variants("ImageVariants", {
109 zoneId,
110 jpeg: ["image/webp", "image/avif"],
111 png: ["image/webp"],
112 });
113 }),
114 );
115
116 expect(updated.value).toEqual({
117 jpeg: ["image/webp", "image/avif"],
118 png: ["image/webp"],
119 });
120
121 const liveUpdated = yield* getVariants(zoneId);
122 expect(liveUpdated!.value.jpeg).toEqual(["image/webp", "image/avif"]);
123 expect(liveUpdated!.value.png).toEqual(["image/webp"]);
124
125 // Removing an extension from props unsets it (full replace).
126 const narrowed = yield* stack.deploy(
127 Effect.gen(function* () {
128 return yield* Cloudflare.Cache.Variants("ImageVariants", {
129 zoneId,
130 png: ["image/avif"],
131 });
132 }),
133 );
134
135 expect(narrowed.value).toEqual({ png: ["image/avif"] });
136 const liveNarrowed = yield* getVariants(zoneId);
137 expect(liveNarrowed!.value.jpeg ?? undefined).toBeUndefined();
138 expect(liveNarrowed!.value.png).toEqual(["image/avif"]);
139
140 yield* stack.destroy();
141
142 // Destroy removed the setting entirely — observed via the typed
143 // `VariantsNotConfigured` error (mapped to `undefined`).
144 const gone = yield* getVariants(zoneId);
145 expect(gone).toBeUndefined();
146 }).pipe(logLevel),
147 );
148
149 test.provider(
150 "deploy is idempotent and converges out-of-band drift",
151 (stack) =>
152 Effect.gen(function* () {
153 const zoneId = yield* resolveZoneId;
154
155 yield* stack.destroy();
156 yield* resetBaseline(zoneId);
157
158 const created = yield* stack.deploy(
159 Effect.gen(function* () {
160 return yield* Cloudflare.Cache.Variants("ImageVariants", {
161 zoneId,
162 webp: ["image/avif"],
163 });
164 }),
165 );
166 expect(created.value).toEqual({ webp: ["image/avif"] });
167
168 // Drift the setting out-of-band. The next reconcile (triggered by a
169 // prop change) observes the live cloud state and replaces the full
170 // value — the drifted `gif` entry must not survive.
171 yield* cache
172 .patchVariant({ zoneId, value: { gif: ["image/webp"] } })
173 .pipe(
174 Effect.retry({
175 while: isTokenPropagationError,
176 schedule: tokenPropagationRetrySchedule,
177 times: 8,
178 }),
179 );
180
181 const converged = yield* stack.deploy(
182 Effect.gen(function* () {
183 return yield* Cloudflare.Cache.Variants("ImageVariants", {
184 zoneId,
185 webp: ["image/avif"],
186 tiff: ["image/webp"],
187 });
188 }),
189 );
190 expect(converged.value).toEqual({
191 tiff: ["image/webp"],
192 webp: ["image/avif"],
193 });
194
195 const live = yield* getVariants(zoneId);
196 expect(live!.value.webp).toEqual(["image/avif"]);
197 expect(live!.value.tiff).toEqual(["image/webp"]);
198 expect(live!.value.gif ?? undefined).toBeUndefined();
199
200 yield* stack.destroy();
201
202 const gone = yield* getVariants(zoneId);
203 expect(gone).toBeUndefined();
204 }).pipe(logLevel),
205 );
206
207 // Canonical `list()` test (zone-scoped singleton with create/delete
208 // semantics): there is no account-wide API, so `list()` enumerates every
209 // zone via `listAllZones` and reads the setting in each, skipping zones
210 // where it was never configured. Deploy the setting on the standing test
211 // zone first so it appears in the enumeration, then assert it is present.
212 test.provider("list enumerates the configured variants settings", (stack) =>
213 Effect.gen(function* () {
214 const zoneId = yield* resolveZoneId;
215
216 yield* stack.destroy();
217 yield* resetBaseline(zoneId);
218
219 yield* stack.deploy(
220 Effect.gen(function* () {
221 return yield* Cloudflare.Cache.Variants("ImageVariants", {
222 zoneId,
223 jpeg: ["image/webp"],
224 });
225 }),
226 );
227
228 const provider = yield* Provider.findProvider(
229 Cloudflare.Cache.Variants,
230 );
231 // `list()` enumerates every zone with the freshly-minted scoped token,
232 // which propagates eventually-consistently and intermittently returns
233 // `401 Unauthorized` / `403 Forbidden` — ride those blips out too.
234 const all = yield* provider.list().pipe(
235 Effect.retry({
236 while: isTokenPropagationError,
237 schedule: tokenPropagationRetrySchedule,
238 times: 8,
239 }),
240 );
241
242 expect(all.length).toBeGreaterThan(0);
243 const entry = all.find((v) => v.zoneId === zoneId);
244 expect(entry).toBeDefined();
245 expect(entry!.value.jpeg).toEqual(["image/webp"]);
246
247 yield* stack.destroy();
248 }).pipe(logLevel),
249 );
250 },
251);