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 test below is 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 getIntegration = (accountId: string, providerId: string) =>
31 mcn.getCloudIntegration({ accountId, providerId }).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 integration is gone after destroy. Cloudflare answers GET
40// for a missing integration with the typed `CloudIntegrationNotFound` (404).
41const expectGone = (accountId: string, providerId: string) =>
42 getIntegration(accountId, providerId).pipe(
43 Effect.flatMap(() =>
44 Effect.fail({ _tag: "IntegrationNotDeleted" } as const),
45 ),
46 Effect.catchTag("CloudIntegrationNotFound", () => Effect.void),
47 Effect.retry({
48 while: (e) => e._tag === "IntegrationNotDeleted",
49 schedule: Schedule.max([
50 Schedule.exponential("500 millis"),
51 Schedule.recurs(10),
52 ]),
53 }),
54 );
55
56test.provider(
57 "unentitled accounts surface the typed FeatureNotEnabled error",
58 (stack) =>
59 Effect.gen(function* () {
60 const { accountId } = yield* yield* CloudflareEnvironment;
61
62 yield* stack.destroy();
63
64 const canList = yield* mcn.listCloudIntegrations({ accountId }).pipe(
65 Effect.as(true),
66 Effect.catchTag("FeatureNotEnabled", () => Effect.succeed(false)),
67 );
68 if (canList) {
69 // Entitled account — the gated lifecycle test covers real behavior.
70 yield* Effect.logInfo("account is MCN-entitled; probe test is a no-op");
71 return;
72 }
73
74 // The typed tag — not UnknownCloudflareError, not a status check.
75 const error = yield* mcn
76 .listCloudIntegrations({ accountId })
77 .pipe(Effect.flip);
78 expect(error._tag).toEqual("FeatureNotEnabled");
79
80 const createError = yield* mcn
81 .createCloudIntegration({
82 accountId,
83 cloudType: "AWS",
84 friendlyName: "alchemy-mcn-probe",
85 })
86 .pipe(Effect.flip);
87 expect(createError._tag).toEqual("FeatureNotEnabled");
88
89 yield* stack.destroy();
90 }).pipe(logLevel),
91 {
92 tags: [
93 "provider:cloudflare",
94 "provider:cloudflare:magiccloudnetworking",
95 "live",
96 ],
97 },
98);
99
100test.provider(
101 "list returns a well-typed array of integrations",
102 (stack) =>
103 Effect.gen(function* () {
104 yield* stack.destroy();
105
106 const provider = yield* Provider.findProvider(
107 Cloudflare.MagicCloudNetworking.CloudIntegration,
108 );
109 const all = yield* provider.list();
110
111 // On an unentitled account `list()` catches the typed `FeatureNotEnabled`
112 // tag and yields `[]`; on an entitled account it enumerates every
113 // integration. Either way the result is the full Attributes array.
114 expect(Array.isArray(all)).toBe(true);
115 for (const item of all) {
116 expect(typeof item.integrationId).toBe("string");
117 expect(typeof item.accountId).toBe("string");
118 }
119
120 yield* stack.destroy();
121 }).pipe(logLevel),
122 {
123 tags: [
124 "provider:cloudflare",
125 "provider:cloudflare:magiccloudnetworking",
126 "live",
127 ],
128 },
129);
130
131test.provider.skipIf(!entitled)(
132 "list enumerates the deployed integration",
133 (stack) =>
134 Effect.gen(function* () {
135 yield* stack.destroy();
136
137 const deployed = yield* stack.deploy(
138 Cloudflare.MagicCloudNetworking.CloudIntegration("ListIntegration", {
139 cloudType: "AWS",
140 friendlyName: "alchemy-mcn-cloud-integration-list",
141 }),
142 );
143
144 const provider = yield* Provider.findProvider(
145 Cloudflare.MagicCloudNetworking.CloudIntegration,
146 );
147 const all = yield* provider.list();
148
149 expect(all.some((x) => x.integrationId === deployed.integrationId)).toBe(
150 true,
151 );
152
153 yield* stack.destroy();
154 }).pipe(logLevel),
155 {
156 tags: [
157 "provider:cloudflare",
158 "provider:cloudflare:magiccloudnetworking",
159 "live",
160 ],
161 timeout: 120_000,
162 },
163);
164
165test.provider.skipIf(!entitled)(
166 "registers an AWS integration, updates it in place, and destroys it",
167 (stack) =>
168 Effect.gen(function* () {
169 const { accountId } = yield* yield* CloudflareEnvironment;
170
171 yield* stack.destroy();
172
173 const integration = yield* stack.deploy(
174 Cloudflare.MagicCloudNetworking.CloudIntegration("Aws", {
175 cloudType: "AWS",
176 friendlyName: "alchemy-mcn-cloud-integration",
177 description: "alchemy cloud integration test",
178 }),
179 );
180
181 expect(integration.integrationId).toBeDefined();
182 expect(integration.accountId).toEqual(accountId);
183 expect(integration.cloudType).toEqual("AWS");
184 expect(integration.friendlyName).toEqual("alchemy-mcn-cloud-integration");
185 // No credentials are wired yet — the integration is pending setup.
186 expect(integration.lifecycleState).toEqual("PENDING_SETUP");
187
188 // Out-of-band verification via the distilled API.
189 const live = yield* getIntegration(accountId, integration.integrationId);
190 expect(live.friendlyName).toEqual("alchemy-mcn-cloud-integration");
191 expect(live.cloudType).toEqual("AWS");
192
193 // Update mutable props in place — same integrationId.
194 const updated = yield* stack.deploy(
195 Cloudflare.MagicCloudNetworking.CloudIntegration("Aws", {
196 cloudType: "AWS",
197 friendlyName: "alchemy-mcn-cloud-integration-v2",
198 description: "alchemy cloud integration test v2",
199 }),
200 );
201
202 expect(updated.integrationId).toEqual(integration.integrationId);
203 expect(updated.friendlyName).toEqual("alchemy-mcn-cloud-integration-v2");
204 expect(updated.description).toEqual("alchemy cloud integration test v2");
205
206 const liveUpdated = yield* getIntegration(
207 accountId,
208 integration.integrationId,
209 );
210 expect(liveUpdated.friendlyName).toEqual(
211 "alchemy-mcn-cloud-integration-v2",
212 );
213
214 yield* stack.destroy();
215
216 yield* expectGone(accountId, integration.integrationId);
217 }).pipe(logLevel),
218 {
219 tags: [
220 "provider:cloudflare",
221 "provider:cloudflare:magiccloudnetworking",
222 "live",
223 ],
224 timeout: 120_000,
225 },
226);
227
228test.provider.skipIf(!entitled)(
229 "replaces the integration when cloudType changes",
230 (stack) =>
231 Effect.gen(function* () {
232 const { accountId } = yield* yield* CloudflareEnvironment;
233
234 yield* stack.destroy();
235
236 const initial = yield* stack.deploy(
237 Cloudflare.MagicCloudNetworking.CloudIntegration("ReplaceIntegration", {
238 cloudType: "AWS",
239 friendlyName: "alchemy-mcn-cloud-integration-replace",
240 }),
241 );
242
243 // cloudType is the provider identity — changing it must produce a
244 // brand-new integration (new integrationId).
245 const replaced = yield* stack.deploy(
246 Cloudflare.MagicCloudNetworking.CloudIntegration("ReplaceIntegration", {
247 cloudType: "GOOGLE",
248 friendlyName: "alchemy-mcn-cloud-integration-replace",
249 }),
250 );
251
252 expect(replaced.integrationId).not.toEqual(initial.integrationId);
253 expect(replaced.cloudType).toEqual("GOOGLE");
254
255 yield* expectGone(accountId, initial.integrationId);
256
257 yield* stack.destroy();
258
259 yield* expectGone(accountId, replaced.integrationId);
260 }).pipe(logLevel),
261 {
262 tags: [
263 "provider:cloudflare",
264 "provider:cloudflare:magiccloudnetworking",
265 "live",
266 ],
267 timeout: 120_000,
268 },
269);