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// Monitor groups are Enterprise-only. On the testing account creation is
19// rejected with "monitor groups not enabled; enterprise only" (Cloudflare
20// code 1002), surfaced as the typed `MonitorGroupsNotEnabled` error. Set
21// CLOUDFLARE_LB_MONITOR_GROUPS_ENABLED=1 on an Enterprise account (which
22// also implies the Load Balancing subscription) to run the lifecycle test.
23const monitorGroupsEnabled = !!process.env.CLOUDFLARE_LB_MONITOR_GROUPS_ENABLED;
24
25// Deterministic per-test physical names — reused on every run.
26const NAME_ENTITLEMENT = "alchemy-lb-monitorgroup-entitlement-probe";
27const NAME_LIFECYCLE = "alchemy-lb-monitorgroup-lifecycle";
28const NAME_MONITOR = "alchemy-lb-monitorgroup-lifecycle-monitor";
29
30// Freshly minted scoped tokens propagate eventually-consistently across
31// Cloudflare's edge — retry the typed `Forbidden` blips on out-of-band calls.
32const forbiddenRetrySchedule = Schedule.exponential("500 millis");
33
34const getMonitorGroup = (accountId: string, monitorGroupId: string) =>
35 loadBalancers.getMonitorGroup({ accountId, monitorGroupId }).pipe(
36 Effect.retry({
37 while: (e) => e._tag === "Forbidden",
38 schedule: forbiddenRetrySchedule,
39 times: 8,
40 }),
41 );
42
43const expectGone = (accountId: string, monitorGroupId: string) =>
44 getMonitorGroup(accountId, monitorGroupId).pipe(
45 Effect.flatMap(() =>
46 Effect.fail({ _tag: "MonitorGroupNotDeleted" } as const),
47 ),
48 // A missing group surfaces as the typed `MonitorGroupNotFound`
49 // (Cloudflare error code 1001) — that's the success condition here.
50 Effect.catchTag("MonitorGroupNotFound", () => Effect.void),
51 Effect.retry({
52 while: (e) => e._tag === "MonitorGroupNotDeleted",
53 schedule: Schedule.max([
54 Schedule.exponential("500 millis"),
55 Schedule.recurs(10),
56 ]),
57 }),
58 );
59
60// Unentitlement probe: pins the typed plan-gate rejection, so it must skip
61// on Enterprise accounts — there the create would succeed instead of failing.
62test.provider.skipIf(monitorGroupsEnabled)(
63 "surfaces the typed MonitorGroupsNotEnabled error on non-Enterprise accounts",
64 (stack) =>
65 Effect.gen(function* () {
66 const { accountId } = yield* yield* CloudflareEnvironment;
67
68 yield* stack.destroy();
69
70 // The entitlement gate fires before member validation, so a
71 // placeholder monitor id is fine here.
72 const error = yield* loadBalancers
73 .createMonitorGroup({
74 accountId,
75 description: NAME_ENTITLEMENT,
76 members: [
77 {
78 monitorId: "17b5962d775c646f3f9725cbc7a53df4",
79 enabled: true,
80 monitoringOnly: false,
81 mustBeHealthy: true,
82 },
83 ],
84 })
85 .pipe(
86 Effect.retry({
87 while: (e) => e._tag === "Forbidden",
88 schedule: forbiddenRetrySchedule,
89 times: 8,
90 }),
91 Effect.flip,
92 );
93 expect(error._tag).toEqual("MonitorGroupsNotEnabled");
94
95 yield* stack.destroy();
96 }).pipe(logLevel),
97 { tags: ["provider:cloudflare", "provider:cloudflare:loadbalancer", "live"] },
98);
99
100// Ungated probe: monitor group enumeration is account-scoped and works
101// regardless of the Enterprise entitlement — a non-entitled account simply
102// has no groups, so `list()` returns an array (typically empty here).
103test.provider(
104 "list returns an array of monitor groups",
105 () =>
106 Effect.gen(function* () {
107 const provider = yield* Provider.findProvider(
108 Cloudflare.LoadBalancer.MonitorGroup,
109 );
110 const all = yield* provider.list();
111 expect(Array.isArray(all)).toBe(true);
112 }).pipe(logLevel),
113 { tags: ["provider:cloudflare", "provider:cloudflare:loadbalancer", "live"] },
114);
115
116test.provider.skipIf(!monitorGroupsEnabled)(
117 "list enumerates the deployed monitor group",
118 (stack) =>
119 Effect.gen(function* () {
120 yield* stack.destroy();
121
122 const deployed = yield* stack.deploy(
123 Effect.gen(function* () {
124 const monitor = yield* Cloudflare.LoadBalancer.Monitor("Monitor", {
125 description: NAME_MONITOR,
126 type: "https",
127 path: "/health",
128 expectedCodes: "2xx",
129 });
130 const group = yield* Cloudflare.LoadBalancer.MonitorGroup("Group", {
131 description: NAME_LIFECYCLE,
132 members: [{ monitorId: monitor.monitorId }],
133 });
134 return { monitor, group };
135 }),
136 );
137
138 const provider = yield* Provider.findProvider(
139 Cloudflare.LoadBalancer.MonitorGroup,
140 );
141 const all = yield* provider.list();
142 expect(
143 all.some((g) => g.monitorGroupId === deployed.group.monitorGroupId),
144 ).toBe(true);
145
146 yield* stack.destroy();
147 }).pipe(logLevel),
148 {
149 tags: ["provider:cloudflare", "provider:cloudflare:loadbalancer", "live"],
150 timeout: 120_000,
151 },
152);
153
154test.provider.skipIf(!monitorGroupsEnabled)(
155 "create, update in place, and destroy a monitor group",
156 (stack) =>
157 Effect.gen(function* () {
158 const { accountId } = yield* yield* CloudflareEnvironment;
159
160 yield* stack.destroy();
161
162 const initial = yield* stack.deploy(
163 Effect.gen(function* () {
164 const monitor = yield* Cloudflare.LoadBalancer.Monitor("Monitor", {
165 description: NAME_MONITOR,
166 type: "https",
167 path: "/health",
168 expectedCodes: "2xx",
169 });
170 const group = yield* Cloudflare.LoadBalancer.MonitorGroup("Group", {
171 description: NAME_LIFECYCLE,
172 members: [{ monitorId: monitor.monitorId }],
173 });
174 return { monitor, group };
175 }),
176 );
177
178 expect(initial.group.monitorGroupId).toBeTruthy();
179 expect(initial.group.accountId).toEqual(accountId);
180 expect(initial.group.description).toEqual(NAME_LIFECYCLE);
181
182 const live = yield* getMonitorGroup(
183 accountId,
184 initial.group.monitorGroupId,
185 );
186 expect(live.description).toEqual(NAME_LIFECYCLE);
187 expect(live.members.map((m) => m.monitorId)).toEqual([
188 initial.monitor.monitorId,
189 ]);
190
191 // Member flags update in place — same monitorGroupId. Keep the
192 // monitor deployed across every step.
193 const updated = yield* stack.deploy(
194 Effect.gen(function* () {
195 const monitor = yield* Cloudflare.LoadBalancer.Monitor("Monitor", {
196 description: NAME_MONITOR,
197 type: "https",
198 path: "/health",
199 expectedCodes: "2xx",
200 });
201 const group = yield* Cloudflare.LoadBalancer.MonitorGroup("Group", {
202 description: NAME_LIFECYCLE,
203 members: [{ monitorId: monitor.monitorId, monitoringOnly: true }],
204 });
205 return { monitor, group };
206 }),
207 );
208 expect(updated.group.monitorGroupId).toEqual(
209 initial.group.monitorGroupId,
210 );
211
212 const synced = yield* getMonitorGroup(
213 accountId,
214 updated.group.monitorGroupId,
215 );
216 expect(synced.members[0]?.monitoringOnly).toEqual(true);
217
218 yield* stack.destroy();
219
220 yield* expectGone(accountId, initial.group.monitorGroupId);
221 }).pipe(logLevel),
222 {
223 tags: ["provider:cloudflare", "provider:cloudflare:loadbalancer", "live"],
224 timeout: 120_000,
225 },
226);