1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as cloudforceOne from "@distilled.cloud/cloudflare/cloudforce-one";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: Cloudflare.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18// Cloudforce One attack-surface scans are entitlement-gated: on the standard
19// testing account every `/cloudforce-one/scans/config` call fails with the
20// typed `Unauthorized` error ("needs cfone.port_scan entitlement"). The full
21// lifecycle test below is gated behind an explicit opt-in env flag for
22// entitled accounts; the probe test always runs and pins the typed tag.
23const entitled = !!process.env.CLOUDFLARE_TEST_CLOUDFORCE_ONE;
24
25// Probe once per suite: either the account can list scan configs (entitled)
26// or the call fails with the typed `Unauthorized` tag.
27const probeEntitlement = (accountId: string) =>
28 cloudforceOne.listScanConfigs({ accountId }).pipe(
29 Effect.as(true),
30 Effect.catchTag("Unauthorized", () => Effect.succeed(false)),
31 );
32
33// Unentitlement probe — pins the typed Unauthorized rejection ("needs cfone.port_scan
34// entitlement") and skips on entitled accounts, where the API would accept the calls.
35test.provider.skipIf(entitled)(
36 "unentitled accounts surface the typed Unauthorized error",
37 (stack) =>
38 Effect.gen(function* () {
39 const { accountId } = yield* yield* CloudflareEnvironment;
40
41 yield* stack.destroy();
42
43 const canList = yield* probeEntitlement(accountId);
44 if (canList) {
45 // Entitled account — nothing to assert here; the lifecycle test
46 // covers the real behavior.
47 yield* Effect.logInfo(
48 "account is cloudforce-one entitled; probe test is a no-op",
49 );
50 return;
51 }
52
53 // The typed tag — not UnknownCloudflareError, not a status check.
54 const listError = yield* cloudforceOne
55 .listScanConfigs({ accountId })
56 .pipe(Effect.flip);
57 expect(listError._tag).toEqual("Unauthorized");
58 expect(listError.message).toContain("cfone.port_scan");
59
60 const createError = yield* cloudforceOne
61 .createScanConfig({ accountId, ips: ["1.1.1.1/32"], frequency: 0 })
62 .pipe(Effect.flip);
63 expect(createError._tag).toEqual("Unauthorized");
64
65 yield* stack.destroy();
66 }).pipe(logLevel),
67 {
68 tags: ["provider:cloudflare", "provider:cloudflare:cloudforceone", "live"],
69 },
70);
71
72// Poll the account list until the config id disappears — there is no
73// get-by-id endpoint, so "gone" means absent from the list.
74const expectGone = (accountId: string, configId: string) =>
75 cloudforceOne.listScanConfigs({ accountId }).pipe(
76 Effect.flatMap((page) =>
77 page.result.some((c) => c.id === configId)
78 ? Effect.fail({ _tag: "ScanConfigNotDeleted" } as const)
79 : Effect.void,
80 ),
81 Effect.retry({
82 while: (e) => e._tag === "ScanConfigNotDeleted",
83 schedule: Schedule.max([
84 Schedule.exponential("500 millis"),
85 Schedule.recurs(10),
86 ]),
87 }),
88 );
89
90// Requires the Cloudforce One attack-surface scan (cfone.port_scan) entitlement —
91// unentitled accounts fail with the typed Unauthorized. Unlock with CLOUDFLARE_TEST_CLOUDFORCE_ONE=1.
92test.provider.skipIf(!entitled)(
93 "create a scan config, update ports in place, delete",
94 (stack) =>
95 Effect.gen(function* () {
96 const { accountId } = yield* yield* CloudflareEnvironment;
97
98 yield* stack.destroy();
99
100 // Create — one-off scan of a single documentation address.
101 const created = yield* stack.deploy(
102 Cloudflare.CloudforceOne.ScanConfig("TestScanConfig", {
103 ips: ["1.1.1.1/32"],
104 frequency: 0,
105 }),
106 );
107 expect(created.configId).toBeTruthy();
108 expect(created.accountId).toEqual(accountId);
109 expect(created.ips).toEqual(["1.1.1.1/32"]);
110 expect(created.frequency).toEqual(0);
111
112 // Out-of-band verification — the config shows up in the account list.
113 const listed = yield* cloudforceOne.listScanConfigs({ accountId });
114 expect(listed.result.map((c) => c.id)).toContain(created.configId);
115
116 // In-place update — change the port list; the config id is stable.
117 const updated = yield* stack.deploy(
118 Cloudflare.CloudforceOne.ScanConfig("TestScanConfig", {
119 ips: ["1.1.1.1/32"],
120 frequency: 0,
121 ports: ["1-80", "443"],
122 }),
123 );
124 expect(updated.configId).toEqual(created.configId);
125 expect([...updated.ports].sort()).toEqual(["1-80", "443"]);
126
127 // No-op redeploy — same props, same config id.
128 const noop = yield* stack.deploy(
129 Cloudflare.CloudforceOne.ScanConfig("TestScanConfig", {
130 ips: ["1.1.1.1/32"],
131 frequency: 0,
132 ports: ["1-80", "443"],
133 }),
134 );
135 expect(noop.configId).toEqual(created.configId);
136
137 yield* stack.destroy();
138
139 yield* expectGone(accountId, created.configId);
140
141 // Destroy again — delete is idempotent (typed ScanConfigNotFound).
142 yield* stack.destroy();
143 }).pipe(logLevel),
144 {
145 tags: ["provider:cloudflare", "provider:cloudflare:cloudforceone", "live"],
146 },
147);
148
149// Read-only list assertion — always runs. On unentitled accounts the provider's
150// list() catches the typed `Unauthorized` (needs cfone.port_scan) and returns
151// [], so this stays green everywhere.
152test.provider(
153 "list returns an array of scan configs",
154 (stack) =>
155 Effect.gen(function* () {
156 yield* stack.destroy();
157
158 const provider = yield* Provider.findProvider(
159 Cloudflare.CloudforceOne.ScanConfig,
160 );
161 const all = yield* provider.list();
162 expect(Array.isArray(all)).toBe(true);
163
164 yield* stack.destroy();
165 }).pipe(logLevel),
166 {
167 tags: ["provider:cloudflare", "provider:cloudflare:cloudforceone", "live"],
168 },
169);
170
171// Full enumeration — gated on the cfone.port_scan entitlement. Unlock with
172// CLOUDFLARE_TEST_CLOUDFORCE_ONE=1. Unentitled accounts surface the typed
173// Unauthorized on the deploy (createScanConfig), so the mutation is gated.
174test.provider.skipIf(!entitled)(
175 "list enumerates the deployed scan config",
176 (stack) =>
177 Effect.gen(function* () {
178 yield* stack.destroy();
179
180 const deployed = yield* stack.deploy(
181 Cloudflare.CloudforceOne.ScanConfig("ListScanConfig", {
182 ips: ["1.1.1.1/32"],
183 frequency: 0,
184 }),
185 );
186
187 const provider = yield* Provider.findProvider(
188 Cloudflare.CloudforceOne.ScanConfig,
189 );
190 const all = yield* provider.list();
191 expect(all.some((c) => c.configId === deployed.configId)).toBe(true);
192
193 yield* stack.destroy();
194 }).pipe(logLevel),
195 {
196 tags: ["provider:cloudflare", "provider:cloudflare:cloudforceone", "live"],
197 },
198);