1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as mcn from "@distilled.cloud/cloudflare/magic-cloud-networking";
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// Magic Cloud Networking is an entitlement-gated add-on (Magic WAN family).
19// On the standard testing account every MCN call fails with the typed
20// `FeatureNotEnabled` error (HTTP 403, Cloudflare code 1012 "feature not
21// enabled"). The full lifecycle tests below are gated behind an explicit
22// opt-in env flag for entitled accounts; the probe test always runs and
23// pins the typed tag.
24const entitled = !!process.env.CLOUDFLARE_TEST_MAGIC_CLOUD_NETWORKING;
25
26// The scoped API token the test harness mints propagates eventually-
27// consistently across Cloudflare's edge — ride out 403 blips (`Forbidden`,
28// declared in the distilled error union) on out-of-band calls. The retry
29// is bounded so an unentitled account fails fast with the typed tag.
30const getSync = (accountId: string, syncId: string) =>
31 mcn.getCatalogSync({ accountId, syncId }).pipe(
32 Effect.retry({
33 while: (e) => e._tag === "Forbidden",
34 schedule: Schedule.exponential("500 millis"),
35 times: 5,
36 }),
37 );
38
39// Poll until the sync is gone after destroy. Cloudflare answers GET for a
40// missing sync with the typed `CatalogSyncNotFound` (404).
41const expectGone = (accountId: string, syncId: string) =>
42 getSync(accountId, syncId).pipe(
43 Effect.flatMap(() => Effect.fail({ _tag: "SyncNotDeleted" } as const)),
44 Effect.catchTag("CatalogSyncNotFound", () => Effect.void),
45 Effect.retry({
46 while: (e) => e._tag === "SyncNotDeleted",
47 schedule: Schedule.max([
48 Schedule.exponential("500 millis"),
49 Schedule.recurs(10),
50 ]),
51 }),
52 );
53
54test.provider(
55 "unentitled accounts surface the typed FeatureNotEnabled error",
56 (stack) =>
57 Effect.gen(function* () {
58 const { accountId } = yield* yield* CloudflareEnvironment;
59
60 yield* stack.destroy();
61
62 const canList = yield* mcn.listCatalogSyncs({ accountId }).pipe(
63 Effect.as(true),
64 Effect.catchTag("FeatureNotEnabled", () => Effect.succeed(false)),
65 );
66 if (canList) {
67 // Entitled account — the gated lifecycle test covers real behavior.
68 yield* Effect.logInfo("account is MCN-entitled; probe test is a no-op");
69 return;
70 }
71
72 // The typed tag — not UnknownCloudflareError, not a status check.
73 const error = yield* mcn
74 .listCatalogSyncs({ accountId })
75 .pipe(Effect.flip);
76 expect(error._tag).toEqual("FeatureNotEnabled");
77
78 const createError = yield* mcn
79 .createCatalogSync({
80 accountId,
81 name: "alchemy-mcn-probe",
82 destinationType: "NONE",
83 updateMode: "MANUAL",
84 })
85 .pipe(Effect.flip);
86 expect(createError._tag).toEqual("FeatureNotEnabled");
87
88 yield* stack.destroy();
89 }).pipe(logLevel),
90 {
91 tags: [
92 "provider:cloudflare",
93 "provider:cloudflare:magiccloudnetworking",
94 "live",
95 ],
96 },
97);
98
99test.provider.skipIf(!entitled)(
100 "creates a sync, updates mutable props in place, and destroys it",
101 (stack) =>
102 Effect.gen(function* () {
103 const { accountId } = yield* yield* CloudflareEnvironment;
104
105 yield* stack.destroy();
106
107 const sync = yield* stack.deploy(
108 Cloudflare.MagicCloudNetworking.CatalogSync("Sync", {
109 name: "alchemy-mcn-catalog-sync",
110 destinationType: "NONE",
111 updateMode: "MANUAL",
112 description: "alchemy catalog sync test",
113 }),
114 );
115
116 expect(sync.syncId).toBeDefined();
117 expect(sync.accountId).toEqual(accountId);
118 expect(sync.name).toEqual("alchemy-mcn-catalog-sync");
119 expect(sync.destinationType).toEqual("NONE");
120 expect(sync.updateMode).toEqual("MANUAL");
121 expect(sync.description).toEqual("alchemy catalog sync test");
122
123 // Out-of-band verification via the distilled API.
124 const live = yield* getSync(accountId, sync.syncId);
125 expect(live.name).toEqual("alchemy-mcn-catalog-sync");
126 expect(live.updateMode).toEqual("MANUAL");
127
128 // Update mutable props in place — same syncId.
129 const updated = yield* stack.deploy(
130 Cloudflare.MagicCloudNetworking.CatalogSync("Sync", {
131 name: "alchemy-mcn-catalog-sync-v2",
132 destinationType: "NONE",
133 updateMode: "AUTO",
134 description: "alchemy catalog sync test v2",
135 }),
136 );
137
138 expect(updated.syncId).toEqual(sync.syncId);
139 expect(updated.name).toEqual("alchemy-mcn-catalog-sync-v2");
140 expect(updated.updateMode).toEqual("AUTO");
141 expect(updated.description).toEqual("alchemy catalog sync test v2");
142
143 const liveUpdated = yield* getSync(accountId, sync.syncId);
144 expect(liveUpdated.name).toEqual("alchemy-mcn-catalog-sync-v2");
145 expect(liveUpdated.updateMode).toEqual("AUTO");
146
147 yield* stack.destroy();
148
149 yield* expectGone(accountId, sync.syncId);
150 }).pipe(logLevel),
151 {
152 tags: [
153 "provider:cloudflare",
154 "provider:cloudflare:magiccloudnetworking",
155 "live",
156 ],
157 timeout: 120_000,
158 },
159);
160
161// On unentitled accounts `list()` swallows the typed `FeatureNotEnabled`
162// error and returns `[]`, so the read-only assertion runs everywhere. On an
163// entitled account we additionally deploy a sync and assert it shows up in
164// the exhaustively-paginated result.
165test.provider(
166 "list enumerates account catalog syncs",
167 (stack) =>
168 Effect.gen(function* () {
169 yield* stack.destroy();
170
171 const provider = yield* Provider.findProvider(
172 Cloudflare.MagicCloudNetworking.CatalogSync,
173 );
174
175 const before = yield* provider.list();
176 expect(Array.isArray(before)).toBe(true);
177
178 if (!entitled) {
179 // Unentitled — FeatureNotEnabled is caught and mapped to [].
180 expect(before).toEqual([]);
181 yield* stack.destroy();
182 return;
183 }
184
185 const deployed = yield* stack.deploy(
186 Cloudflare.MagicCloudNetworking.CatalogSync("ListSync", {
187 name: "alchemy-mcn-catalog-sync-list",
188 destinationType: "NONE",
189 updateMode: "MANUAL",
190 }),
191 );
192
193 const all = yield* provider.list();
194 expect(all.some((s) => s.syncId === deployed.syncId)).toBe(true);
195
196 yield* stack.destroy();
197
198 yield* expectGone(deployed.accountId, deployed.syncId);
199 }).pipe(logLevel),
200 {
201 tags: [
202 "provider:cloudflare",
203 "provider:cloudflare:magiccloudnetworking",
204 "live",
205 ],
206 timeout: 120_000,
207 },
208);
209
210test.provider.skipIf(!entitled)(
211 "replaces the sync when destinationType changes",
212 (stack) =>
213 Effect.gen(function* () {
214 const { accountId } = yield* yield* CloudflareEnvironment;
215
216 yield* stack.destroy();
217
218 const initial = yield* stack.deploy(
219 Cloudflare.MagicCloudNetworking.CatalogSync("ReplaceSync", {
220 name: "alchemy-mcn-catalog-sync-replace",
221 destinationType: "NONE",
222 updateMode: "MANUAL",
223 }),
224 );
225
226 // destinationType is provisioned at create time — changing it must
227 // produce a brand-new sync (new syncId).
228 const replaced = yield* stack.deploy(
229 Cloudflare.MagicCloudNetworking.CatalogSync("ReplaceSync", {
230 name: "alchemy-mcn-catalog-sync-replace",
231 destinationType: "ZERO_TRUST_LIST",
232 updateMode: "MANUAL",
233 }),
234 );
235
236 expect(replaced.syncId).not.toEqual(initial.syncId);
237 expect(replaced.destinationType).toEqual("ZERO_TRUST_LIST");
238 expect(replaced.destinationId).toBeTruthy();
239
240 yield* expectGone(accountId, initial.syncId);
241
242 yield* stack.destroy();
243
244 yield* expectGone(accountId, replaced.syncId);
245 }).pipe(logLevel),
246 {
247 tags: [
248 "provider:cloudflare",
249 "provider:cloudflare:magiccloudnetworking",
250 "live",
251 ],
252 timeout: 120_000,
253 },
254);