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 waitingRooms from "@distilled.cloud/cloudflare/waiting-rooms";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11import * as Stream from "effect/Stream";
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// Waiting Rooms require a Business or Enterprise zone plan. On the testing
24// account's zone every write fails with "Zone not entitled to this
25// functionality" (Cloudflare code 1034), surfaced as the typed
26// `ZoneNotEntitled` error. The full lifecycle test below is gated behind an
27// entitled zone id supplied via env.
28const entitledZoneId = process.env.CLOUDFLARE_TEST_WAITING_ROOM_ZONE_ID;
29const entitledZoneHost =
30 process.env.CLOUDFLARE_TEST_WAITING_ROOM_HOST ?? zoneName;
31
32// Deterministic per-test room names — reused on every run.
33const NAME_LIFECYCLE = "alchemy-waitingroom-lifecycle";
34
35const resolveZoneId = Effect.gen(function* () {
36 const { accountId } = yield* yield* CloudflareEnvironment;
37 const zone = yield* findZoneByName({ accountId, name: zoneName });
38 if (!zone) {
39 return yield* Effect.die(
40 new Error(`zone "${zoneName}" not found in account`),
41 );
42 }
43 return zone.id;
44});
45
46// Freshly minted scoped tokens propagate eventually-consistently across
47// Cloudflare's edge — retry the typed `Forbidden` blips on out-of-band calls.
48const forbiddenRetrySchedule = Schedule.exponential("500 millis");
49
50const getRoom = (zoneId: string, waitingRoomId: string) =>
51 waitingRooms.getWaitingRoom({ zoneId, waitingRoomId }).pipe(
52 Effect.retry({
53 while: (e) => e._tag === "Forbidden",
54 schedule: forbiddenRetrySchedule,
55 times: 8,
56 }),
57 );
58
59const findRoomByName = (zoneId: string, name: string) =>
60 waitingRooms.listWaitingRoomsForZone.items({ zoneId }).pipe(
61 Stream.filter((room) => room.name === name),
62 Stream.runCollect,
63 Effect.map((chunk) => Array.from(chunk)[0]),
64 Effect.retry({
65 while: (e) => e._tag === "Forbidden",
66 schedule: forbiddenRetrySchedule,
67 times: 8,
68 }),
69 );
70
71test.provider(
72 "surfaces the typed ZoneNotEntitled error on unentitled zones",
73 (stack) =>
74 Effect.gen(function* () {
75 const zoneId = yield* resolveZoneId;
76
77 yield* stack.destroy();
78
79 // The standard testing zone lacks the Waiting Rooms entitlement —
80 // the distilled create must fail with the typed plan-gate tag.
81 const error = yield* waitingRooms
82 .createWaitingRoom({
83 zoneId,
84 name: "alchemy-waitingroom-entitlement-probe",
85 host: zoneName,
86 totalActiveUsers: 200,
87 newUsersPerMinute: 200,
88 })
89 .pipe(
90 Effect.retry({
91 while: (e) => e._tag === "Forbidden",
92 schedule: forbiddenRetrySchedule,
93 times: 8,
94 }),
95 Effect.flip,
96 );
97 expect(error._tag).toEqual("ZoneNotEntitled");
98
99 yield* stack.destroy();
100 }).pipe(logLevel),
101 {
102 tags: [
103 "provider:cloudflare",
104 "provider:cloudflare:waitingroom",
105 "provider:cloudflare:zone",
106 "live",
107 ],
108 },
109);
110
111test.provider.skipIf(!entitledZoneId)(
112 "create, update in place, and destroy a waiting room",
113 (stack) =>
114 Effect.gen(function* () {
115 const zoneId = entitledZoneId!;
116
117 yield* stack.destroy();
118
119 const initial = yield* stack.deploy(
120 Effect.gen(function* () {
121 return yield* Cloudflare.WaitingRoom.WaitingRoom("Room", {
122 zoneId,
123 name: NAME_LIFECYCLE,
124 host: entitledZoneHost,
125 path: "/alchemy-test",
126 totalActiveUsers: 200,
127 newUsersPerMinute: 200,
128 description: "v1",
129 });
130 }),
131 );
132
133 expect(initial.waitingRoomId).toBeDefined();
134 expect(initial.zoneId).toEqual(zoneId);
135 expect(initial.name).toEqual(NAME_LIFECYCLE);
136 expect(initial.host).toEqual(entitledZoneHost);
137 expect(initial.path).toEqual("/alchemy-test");
138 expect(initial.totalActiveUsers).toEqual(200);
139 expect(initial.newUsersPerMinute).toEqual(200);
140 expect(initial.description).toEqual("v1");
141 // Cloudflare defaults.
142 expect(initial.sessionDuration).toEqual(5);
143 expect(initial.queueAll).toEqual(false);
144 expect(initial.queueingMethod).toEqual("fifo");
145 expect(initial.suspended).toEqual(false);
146
147 // Out-of-band verification via the distilled API.
148 const live = yield* getRoom(zoneId, initial.waitingRoomId);
149 expect(live.id).toEqual(initial.waitingRoomId);
150 expect(live.name).toEqual(NAME_LIFECYCLE);
151 expect(live.host).toEqual(entitledZoneHost);
152
153 // Update mutable fields in place — same physical room.
154 const updated = yield* stack.deploy(
155 Effect.gen(function* () {
156 return yield* Cloudflare.WaitingRoom.WaitingRoom("Room", {
157 zoneId,
158 name: NAME_LIFECYCLE,
159 host: entitledZoneHost,
160 path: "/alchemy-test",
161 totalActiveUsers: 250,
162 newUsersPerMinute: 200,
163 description: "v2",
164 sessionDuration: 10,
165 queueAll: true,
166 });
167 }),
168 );
169
170 expect(updated.waitingRoomId).toEqual(initial.waitingRoomId);
171 expect(updated.totalActiveUsers).toEqual(250);
172 expect(updated.description).toEqual("v2");
173 expect(updated.sessionDuration).toEqual(10);
174 expect(updated.queueAll).toEqual(true);
175
176 const liveUpdated = yield* getRoom(zoneId, updated.waitingRoomId);
177 expect(liveUpdated.totalActiveUsers).toEqual(250);
178
179 yield* stack.destroy();
180
181 const gone = yield* findRoomByName(zoneId, NAME_LIFECYCLE);
182 expect(gone).toBeUndefined();
183
184 // Destroy again — deletion is idempotent.
185 yield* stack.destroy();
186 }).pipe(logLevel),
187 {
188 tags: ["provider:cloudflare", "provider:cloudflare:waitingroom", "live"],
189 timeout: 120_000,
190 },
191);
192
193// Canonical `list()` test (zone-scoped collection): waiting rooms have no
194// account-wide enumeration API, so `list()` fans out across every zone via
195// `listAllZones`, exhaustively paginates `listWaitingRoomsForZone` per zone,
196// and hydrates each room into the `read` Attributes shape. Plan-gated /
197// unentitled zones reject the route (typed `Forbidden`) and are skipped to
198// `[]`. The standing test zone lacks the Waiting Rooms entitlement, so the
199// read-only assertion only requires a well-typed array; the deployed-presence
200// assertion is gated behind an entitled zone id supplied via env.
201test.provider(
202 "list enumerates waiting rooms across zones",
203 (stack) =>
204 Effect.gen(function* () {
205 yield* stack.destroy();
206
207 const provider = yield* Provider.findProvider(
208 Cloudflare.WaitingRoom.WaitingRoom,
209 );
210
211 if (entitledZoneId) {
212 const deployed = yield* stack.deploy(
213 Effect.gen(function* () {
214 return yield* Cloudflare.WaitingRoom.WaitingRoom("ListRoom", {
215 zoneId: entitledZoneId,
216 name: "alchemy-waitingroom-list",
217 host: entitledZoneHost,
218 path: "/alchemy-list",
219 totalActiveUsers: 200,
220 newUsersPerMinute: 200,
221 });
222 }),
223 );
224
225 const all = yield* provider.list();
226 expect(
227 all.some((r) => r.waitingRoomId === deployed.waitingRoomId),
228 ).toBe(true);
229 } else {
230 // Unentitled standing zone: no rooms exist, but `list()` must still
231 // return a well-typed array (unentitled zones skip to `[]`).
232 const all = yield* provider.list();
233 expect(Array.isArray(all)).toBe(true);
234 }
235
236 yield* stack.destroy();
237 }).pipe(logLevel),
238 {
239 tags: ["provider:cloudflare", "provider:cloudflare:waitingroom", "live"],
240 timeout: 120_000,
241 },
242);