1import * as Cloudflare from "@/Cloudflare";
2import { adopt } from "@/AdoptPolicy";
3import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
4import { findZoneByName } from "@/Cloudflare/Zone/lookup";
5import * as Provider from "@/Provider";
6import * as Test from "@/Test/Alchemy";
7import * as vulnScanner from "@distilled.cloud/cloudflare/vulnerability-scanner";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import { MinimumLogLevel } from "effect/References";
11import * as Schedule from "effect/Schedule";
12
13const { test } = Test.make({ providers: Cloudflare.providers() });
14
15const logLevel = Effect.provideService(
16 MinimumLogLevel,
17 process.env.DEBUG ? "Debug" : "Info",
18);
19
20const zoneName =
21 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
22
23// Deterministic per-test names — same on every run, disjoint between tests.
24const NAME_DEFAULT = "alchemy-vulnscanner-target-default";
25const NAME_UPDATE = "alchemy-vulnscanner-target-update";
26const NAME_LIST = "alchemy-vulnscanner-target-list";
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
39const accountIdOf = Effect.gen(function* () {
40 const { accountId } = yield* yield* CloudflareEnvironment;
41 return accountId;
42});
43
44// Freshly minted scoped tokens propagate eventually-consistently across
45// Cloudflare's edge; ride out 403 blips on out-of-band verification calls.
46const forbiddenRetrySchedule = Schedule.exponential("500 millis");
47
48const getTargetEnvironment = (accountId: string, targetEnvironmentId: string) =>
49 vulnScanner.getTargetEnvironment({ accountId, targetEnvironmentId }).pipe(
50 Effect.retry({
51 while: (e) => e._tag === "Forbidden",
52 schedule: forbiddenRetrySchedule,
53 times: 8,
54 }),
55 );
56
57// Typed "is it gone?" probe — `TargetEnvironmentNotFound` (code 11001) maps
58// to undefined; anything else propagates.
59const findTargetEnvironment = (
60 accountId: string,
61 targetEnvironmentId: string,
62) =>
63 getTargetEnvironment(accountId, targetEnvironmentId).pipe(
64 Effect.catchTag("TargetEnvironmentNotFound", () =>
65 Effect.succeed(undefined),
66 ),
67 );
68
69test.provider(
70 "create, verify, and destroy a target environment",
71 (stack) =>
72 Effect.gen(function* () {
73 const zoneId = yield* resolveZoneId;
74 const accountId = yield* accountIdOf;
75
76 yield* stack.destroy();
77
78 const env = yield* stack.deploy(
79 Effect.gen(function* () {
80 return yield* Cloudflare.VulnerabilityScanner.VulnScannerTargetEnvironment(
81 "DefaultEnv",
82 {
83 name: NAME_DEFAULT,
84 zoneId,
85 description: "alchemy test target environment",
86 },
87 ).pipe(adopt(true));
88 }),
89 );
90
91 expect(env.targetEnvironmentId).toBeDefined();
92 expect(env.accountId).toEqual(accountId);
93 expect(env.name).toEqual(NAME_DEFAULT);
94 expect(env.zoneId).toEqual(zoneId);
95 expect(env.description).toEqual("alchemy test target environment");
96
97 // Out-of-band verification against the live API.
98 const live = yield* getTargetEnvironment(
99 accountId,
100 env.targetEnvironmentId,
101 );
102 expect(live.id).toEqual(env.targetEnvironmentId);
103 expect(live.name).toEqual(NAME_DEFAULT);
104 expect(live.target.zoneTag).toEqual(zoneId);
105 expect(live.description).toEqual("alchemy test target environment");
106
107 yield* stack.destroy();
108
109 const gone = yield* findTargetEnvironment(
110 accountId,
111 env.targetEnvironmentId,
112 );
113 expect(gone).toBeUndefined();
114 }).pipe(logLevel),
115 {
116 tags: [
117 "provider:cloudflare",
118 "provider:cloudflare:vulnerabilityscanner",
119 "provider:cloudflare:zone",
120 "live",
121 ],
122 },
123);
124
125test.provider(
126 "updating name and description patches in place; clearing description",
127 (stack) =>
128 Effect.gen(function* () {
129 const zoneId = yield* resolveZoneId;
130 const accountId = yield* accountIdOf;
131
132 yield* stack.destroy();
133
134 const initial = yield* stack.deploy(
135 Effect.gen(function* () {
136 return yield* Cloudflare.VulnerabilityScanner.VulnScannerTargetEnvironment(
137 "UpdateEnv",
138 {
139 name: NAME_UPDATE,
140 zoneId,
141 description: "v1",
142 },
143 ).pipe(adopt(true));
144 }),
145 );
146
147 expect(initial.description).toEqual("v1");
148
149 const updated = yield* stack.deploy(
150 Effect.gen(function* () {
151 return yield* Cloudflare.VulnerabilityScanner.VulnScannerTargetEnvironment(
152 "UpdateEnv",
153 {
154 name: `${NAME_UPDATE}-renamed`,
155 zoneId,
156 description: "v2",
157 },
158 ).pipe(adopt(true));
159 }),
160 );
161
162 // Same environment updated in place — not a replacement.
163 expect(updated.targetEnvironmentId).toEqual(initial.targetEnvironmentId);
164 expect(updated.name).toEqual(`${NAME_UPDATE}-renamed`);
165 expect(updated.description).toEqual("v2");
166
167 const live = yield* getTargetEnvironment(
168 accountId,
169 updated.targetEnvironmentId,
170 );
171 expect(live.name).toEqual(`${NAME_UPDATE}-renamed`);
172 expect(live.description).toEqual("v2");
173
174 // Omitting the description clears it (PUT with explicit null).
175 const cleared = yield* stack.deploy(
176 Effect.gen(function* () {
177 return yield* Cloudflare.VulnerabilityScanner.VulnScannerTargetEnvironment(
178 "UpdateEnv",
179 {
180 name: `${NAME_UPDATE}-renamed`,
181 zoneId,
182 },
183 ).pipe(adopt(true));
184 }),
185 );
186
187 expect(cleared.targetEnvironmentId).toEqual(initial.targetEnvironmentId);
188 expect(cleared.description).toBeUndefined();
189
190 const liveCleared = yield* getTargetEnvironment(
191 accountId,
192 cleared.targetEnvironmentId,
193 );
194 expect(liveCleared.description ?? undefined).toBeUndefined();
195
196 yield* stack.destroy();
197
198 const gone = yield* findTargetEnvironment(
199 accountId,
200 initial.targetEnvironmentId,
201 );
202 expect(gone).toBeUndefined();
203 }).pipe(logLevel),
204 {
205 tags: [
206 "provider:cloudflare",
207 "provider:cloudflare:vulnerabilityscanner",
208 "provider:cloudflare:zone",
209 "live",
210 ],
211 },
212);
213
214// Canonical `list()` test (account collection): deploy a target environment,
215// then enumerate the account-scoped collection and assert the deployed item
216// appears in the exhaustively-paginated result.
217test.provider(
218 "list enumerates the deployed target environment",
219 (stack) =>
220 Effect.gen(function* () {
221 const zoneId = yield* resolveZoneId;
222
223 yield* stack.destroy();
224
225 const env = yield* stack.deploy(
226 Effect.gen(function* () {
227 return yield* Cloudflare.VulnerabilityScanner.VulnScannerTargetEnvironment(
228 "ListEnv",
229 {
230 name: NAME_LIST,
231 zoneId,
232 },
233 ).pipe(adopt(true));
234 }),
235 );
236
237 const provider = yield* Provider.findProvider(
238 Cloudflare.VulnerabilityScanner.VulnScannerTargetEnvironment,
239 );
240 const all = yield* provider.list();
241
242 expect(
243 all.some((e) => e.targetEnvironmentId === env.targetEnvironmentId),
244 ).toBe(true);
245 const found = all.find(
246 (e) => e.targetEnvironmentId === env.targetEnvironmentId,
247 );
248 expect(found?.name).toEqual(NAME_LIST);
249 expect(found?.zoneId).toEqual(zoneId);
250
251 yield* stack.destroy();
252 }).pipe(logLevel),
253 {
254 tags: [
255 "provider:cloudflare",
256 "provider:cloudflare:vulnerabilityscanner",
257 "provider:cloudflare:zone",
258 "live",
259 ],
260 },
261);