1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as loadBalancers from "@distilled.cloud/cloudflare/load-balancers";
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// Load Balancing is a paid add-on subscription. The testing account does
19// not have it: monitor creation is rejected with the degenerate plan limit
20// "interval is not in range [1, 1]" (Cloudflare code 1002), surfaced as the
21// typed `MonitorIntervalOutOfRange` error. Set CLOUDFLARE_LB_ENABLED=1 once
22// the subscription is provisioned to run the full lifecycle tests.
23const lbEnabled = !!process.env.CLOUDFLARE_LB_ENABLED;
24
25// Deterministic per-test physical names — reused on every run.
26const NAME_ENTITLEMENT = "alchemy-lb-monitor-entitlement-probe";
27const NAME_LIFECYCLE = "alchemy-lb-monitor-lifecycle";
28
29// Freshly minted scoped tokens propagate eventually-consistently across
30// Cloudflare's edge — retry the typed `Forbidden` blips on out-of-band calls.
31const forbiddenRetrySchedule = Schedule.exponential("500 millis");
32
33const getMonitor = (accountId: string, monitorId: string) =>
34 loadBalancers.getMonitor({ accountId, monitorId }).pipe(
35 Effect.retry({
36 while: (e) => e._tag === "Forbidden",
37 schedule: forbiddenRetrySchedule,
38 times: 8,
39 }),
40 );
41
42const expectGone = (accountId: string, monitorId: string) =>
43 getMonitor(accountId, monitorId).pipe(
44 Effect.flatMap(() => Effect.fail({ _tag: "MonitorNotDeleted" } as const)),
45 // A missing monitor surfaces as the typed `MonitorNotFound` (Cloudflare
46 // error code 1001) — that's the success condition here.
47 Effect.catchTag("MonitorNotFound", () => Effect.void),
48 Effect.retry({
49 while: (e) => e._tag === "MonitorNotDeleted",
50 schedule: Schedule.max([
51 Schedule.exponential("500 millis"),
52 Schedule.recurs(10),
53 ]),
54 }),
55 );
56
57// Unentitlement probe: pins the typed plan-gate rejection, so it must skip
58// on entitled accounts — there the create would succeed instead of failing.
59test.provider.skipIf(lbEnabled)(
60 "surfaces the typed MonitorIntervalOutOfRange error without the LB subscription",
61 (stack) =>
62 Effect.gen(function* () {
63 const { accountId } = yield* yield* CloudflareEnvironment;
64
65 yield* stack.destroy();
66
67 // Without the Load Balancing subscription the account's allowed
68 // monitor interval range collapses to [1, 1], so any realistic
69 // interval is rejected with the typed plan-gate tag.
70 const error = yield* loadBalancers
71 .createMonitor({
72 accountId,
73 type: "http",
74 description: NAME_ENTITLEMENT,
75 interval: 60,
76 })
77 .pipe(
78 Effect.retry({
79 while: (e) => e._tag === "Forbidden",
80 schedule: forbiddenRetrySchedule,
81 times: 8,
82 }),
83 Effect.flip,
84 );
85 expect(error._tag).toEqual("MonitorIntervalOutOfRange");
86
87 yield* stack.destroy();
88 }).pipe(logLevel),
89 { tags: ["provider:cloudflare", "provider:cloudflare:loadbalancer", "live"] },
90);
91
92test.provider.skipIf(!lbEnabled)(
93 "create, update in place, and destroy a monitor",
94 (stack) =>
95 Effect.gen(function* () {
96 const { accountId } = yield* yield* CloudflareEnvironment;
97
98 yield* stack.destroy();
99
100 const initial = yield* stack.deploy(
101 Cloudflare.LoadBalancer.Monitor("Monitor", {
102 description: NAME_LIFECYCLE,
103 type: "https",
104 path: "/health",
105 expectedCodes: "2xx",
106 allowInsecure: true,
107 }),
108 );
109
110 expect(initial.monitorId).toBeTruthy();
111 expect(initial.accountId).toEqual(accountId);
112 expect(initial.description).toEqual(NAME_LIFECYCLE);
113 expect(initial.type).toEqual("https");
114
115 const live = yield* getMonitor(accountId, initial.monitorId);
116 expect(live.description).toEqual(NAME_LIFECYCLE);
117 expect(live.path).toEqual("/health");
118 expect(live.expectedCodes).toEqual("2xx");
119
120 // Mutable props update in place — same monitorId.
121 const updated = yield* stack.deploy(
122 Cloudflare.LoadBalancer.Monitor("Monitor", {
123 description: NAME_LIFECYCLE,
124 type: "https",
125 path: "/healthz",
126 expectedCodes: "200",
127 allowInsecure: true,
128 retries: 1,
129 }),
130 );
131 expect(updated.monitorId).toEqual(initial.monitorId);
132
133 const synced = yield* getMonitor(accountId, updated.monitorId);
134 expect(synced.path).toEqual("/healthz");
135 expect(synced.expectedCodes).toEqual("200");
136 expect(synced.retries).toEqual(1);
137
138 // Redeploying identical props is a no-op (still the same monitor).
139 const noop = yield* stack.deploy(
140 Cloudflare.LoadBalancer.Monitor("Monitor", {
141 description: NAME_LIFECYCLE,
142 type: "https",
143 path: "/healthz",
144 expectedCodes: "200",
145 allowInsecure: true,
146 retries: 1,
147 }),
148 );
149 expect(noop.monitorId).toEqual(initial.monitorId);
150
151 yield* stack.destroy();
152
153 yield* expectGone(accountId, initial.monitorId);
154 }).pipe(logLevel),
155 {
156 tags: ["provider:cloudflare", "provider:cloudflare:loadbalancer", "live"],
157 timeout: 120_000,
158 },
159);
160
161// Ungated: the account-scoped listMonitors enumeration works regardless of
162// the Load Balancing subscription (it just returns an empty array on an
163// unentitled account), so this proves the list() op end-to-end live.
164test.provider(
165 "list returns an array of monitor attributes",
166 (stack) =>
167 Effect.gen(function* () {
168 yield* stack.destroy();
169
170 const provider = yield* Provider.findProvider(
171 Cloudflare.LoadBalancer.Monitor,
172 );
173 const all = yield* provider.list();
174 expect(Array.isArray(all)).toBe(true);
175 for (const monitor of all) {
176 expect(typeof monitor.monitorId).toBe("string");
177 expect(typeof monitor.accountId).toBe("string");
178 }
179
180 yield* stack.destroy();
181 }).pipe(logLevel),
182 {
183 tags: ["provider:cloudflare", "provider:cloudflare:loadbalancer", "live"],
184 timeout: 60_000,
185 },
186);
187
188// Full presence check — requires the LB subscription to deploy a real
189// monitor. Skips on unentitled accounts (create fails with the typed
190// MonitorIntervalOutOfRange plan-gate error). Set CLOUDFLARE_LB_ENABLED=1
191// once the subscription is provisioned.
192test.provider.skipIf(!lbEnabled)(
193 "list enumerates the deployed monitor",
194 (stack) =>
195 Effect.gen(function* () {
196 yield* stack.destroy();
197
198 const deployed = yield* stack.deploy(
199 Cloudflare.LoadBalancer.Monitor("ListMonitor", {
200 description: NAME_LIFECYCLE,
201 type: "https",
202 path: "/health",
203 expectedCodes: "2xx",
204 allowInsecure: true,
205 }),
206 );
207
208 const provider = yield* Provider.findProvider(
209 Cloudflare.LoadBalancer.Monitor,
210 );
211 const all = yield* provider.list();
212
213 expect(all.some((m) => m.monitorId === deployed.monitorId)).toBe(true);
214
215 yield* stack.destroy();
216 }).pipe(logLevel),
217 {
218 tags: ["provider:cloudflare", "provider:cloudflare:loadbalancer", "live"],
219 timeout: 120_000,
220 },
221);