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"). On-ramps additionally provision real cloud-side infrastructure
22// (VPN gateways / Transit Gateways) inside a wired CloudIntegration, so the
23// lifecycle test needs an entitled account AND a discovered VPC, supplied
24// via env. The probe test always runs and pins the typed tag.
25const entitled = !!process.env.CLOUDFLARE_TEST_MAGIC_CLOUD_NETWORKING;
26// Discovered VPC resource id + region to connect (entitled accounts only).
27const vpcId = process.env.CLOUDFLARE_TEST_MCN_VPC_ID;
28const vpcRegion = process.env.CLOUDFLARE_TEST_MCN_VPC_REGION;
29
30// The scoped API token the test harness mints propagates eventually-
31// consistently across Cloudflare's edge — ride out 403 blips (`Forbidden`,
32// declared in the distilled error union) on out-of-band calls. The retry
33// is bounded so an unentitled account fails fast with the typed tag.
34const getOnRamp = (accountId: string, onrampId: string) =>
35 mcn.getOnRamp({ accountId, onrampId }).pipe(
36 Effect.retry({
37 while: (e) => e._tag === "Forbidden",
38 schedule: Schedule.exponential("500 millis"),
39 times: 5,
40 }),
41 );
42
43// Poll until the on-ramp is gone after destroy. Cloudflare answers GET for
44// a missing on-ramp with the typed `OnRampNotFound` (404). Deletion tears
45// down cloud-side infra, so the window is generous but bounded.
46const expectGone = (accountId: string, onrampId: string) =>
47 getOnRamp(accountId, onrampId).pipe(
48 Effect.flatMap(() => Effect.fail({ _tag: "OnRampNotDeleted" } as const)),
49 Effect.catchTag("OnRampNotFound", () => Effect.void),
50 Effect.retry({
51 while: (e) => e._tag === "OnRampNotDeleted",
52 schedule: Schedule.spaced("5 seconds"),
53 times: 10,
54 }),
55 );
56
57test.provider(
58 "unentitled accounts surface the typed FeatureNotEnabled error",
59 (stack) =>
60 Effect.gen(function* () {
61 const { accountId } = yield* yield* CloudflareEnvironment;
62
63 yield* stack.destroy();
64
65 const canList = yield* mcn.listOnRamps({ accountId }).pipe(
66 Effect.as(true),
67 Effect.catchTag("FeatureNotEnabled", () => Effect.succeed(false)),
68 );
69 if (canList) {
70 // Entitled account — the gated lifecycle test covers real behavior.
71 yield* Effect.logInfo("account is MCN-entitled; probe test is a no-op");
72 return;
73 }
74
75 // The typed tag — not UnknownCloudflareError, not a status check.
76 const error = yield* mcn.listOnRamps({ accountId }).pipe(Effect.flip);
77 expect(error._tag).toEqual("FeatureNotEnabled");
78
79 const createError = yield* mcn
80 .createOnRamp({
81 accountId,
82 name: "alchemy-mcn-probe",
83 cloudType: "AWS",
84 type: "OnrampTypeSingle",
85 dynamicRouting: false,
86 installRoutesInCloud: false,
87 installRoutesInMagicWan: false,
88 })
89 .pipe(Effect.flip);
90 expect(createError._tag).toEqual("FeatureNotEnabled");
91
92 yield* stack.destroy();
93 }).pipe(logLevel),
94 {
95 tags: [
96 "provider:cloudflare",
97 "provider:cloudflare:magiccloudnetworking",
98 "live",
99 ],
100 },
101);
102
103test.provider.skipIf(!entitled || !vpcId || !vpcRegion)(
104 "creates an on-ramp, updates mutable props in place, and destroys it",
105 (stack) =>
106 Effect.gen(function* () {
107 const { accountId } = yield* yield* CloudflareEnvironment;
108
109 yield* stack.destroy();
110
111 const onramp = yield* stack.deploy(
112 Cloudflare.MagicCloudNetworking.OnRamp("Ramp", {
113 name: "alchemy-mcn-onramp",
114 cloudType: "AWS",
115 type: "OnrampTypeSingle",
116 region: vpcRegion!,
117 vpc: vpcId!,
118 dynamicRouting: false,
119 installRoutesInCloud: false,
120 installRoutesInMagicWan: false,
121 description: "alchemy on-ramp test",
122 }),
123 );
124
125 expect(onramp.onRampId).toBeDefined();
126 expect(onramp.accountId).toEqual(accountId);
127 expect(onramp.name).toEqual("alchemy-mcn-onramp");
128 expect(onramp.cloudType).toEqual("AWS");
129 expect(onramp.type).toEqual("OnrampTypeSingle");
130
131 // Out-of-band verification via the distilled API.
132 const live = yield* getOnRamp(accountId, onramp.onRampId);
133 expect(live.name).toEqual("alchemy-mcn-onramp");
134
135 // Update mutable props in place — same onRampId.
136 const updated = yield* stack.deploy(
137 Cloudflare.MagicCloudNetworking.OnRamp("Ramp", {
138 name: "alchemy-mcn-onramp-v2",
139 cloudType: "AWS",
140 type: "OnrampTypeSingle",
141 region: vpcRegion!,
142 vpc: vpcId!,
143 dynamicRouting: false,
144 installRoutesInCloud: false,
145 installRoutesInMagicWan: true,
146 description: "alchemy on-ramp test v2",
147 }),
148 );
149
150 expect(updated.onRampId).toEqual(onramp.onRampId);
151 expect(updated.name).toEqual("alchemy-mcn-onramp-v2");
152 expect(updated.installRoutesInMagicWan).toBe(true);
153 expect(updated.description).toEqual("alchemy on-ramp test v2");
154
155 yield* stack.destroy();
156
157 yield* expectGone(accountId, onramp.onRampId);
158 }).pipe(logLevel),
159 {
160 tags: [
161 "provider:cloudflare",
162 "provider:cloudflare:magiccloudnetworking",
163 "live",
164 ],
165 timeout: 120_000,
166 },
167);
168
169// Read-only list assertion that runs on every account. On an unentitled
170// account `list()` catches the typed `FeatureNotEnabled` and returns a
171// well-typed `[]`; on an entitled account it returns the account's on-ramps
172// as the exact `read` Attributes shape (an array, possibly empty).
173test.provider(
174 "list returns on-ramps or a typed [] when unentitled",
175 (stack) =>
176 Effect.gen(function* () {
177 const { accountId } = yield* yield* CloudflareEnvironment;
178
179 yield* stack.destroy();
180
181 const canList = yield* mcn.listOnRamps({ accountId }).pipe(
182 Effect.as(true),
183 Effect.catchTag("FeatureNotEnabled", () => Effect.succeed(false)),
184 );
185
186 const provider = yield* Provider.findProvider(
187 Cloudflare.MagicCloudNetworking.OnRamp,
188 );
189 const all = yield* provider.list();
190
191 if (!canList) {
192 // Unentitled — FeatureNotEnabled makes the account non-listable.
193 expect(all).toEqual([]);
194 } else {
195 expect(Array.isArray(all)).toBe(true);
196 }
197
198 yield* stack.destroy();
199 }).pipe(logLevel),
200 {
201 tags: [
202 "provider:cloudflare",
203 "provider:cloudflare:magiccloudnetworking",
204 "live",
205 ],
206 },
207);
208
209test.provider.skipIf(!entitled || !vpcId || !vpcRegion)(
210 "list enumerates the deployed on-ramp",
211 (stack) =>
212 Effect.gen(function* () {
213 const { accountId } = yield* yield* CloudflareEnvironment;
214
215 yield* stack.destroy();
216
217 const onramp = yield* stack.deploy(
218 Cloudflare.MagicCloudNetworking.OnRamp("ListRamp", {
219 name: "alchemy-mcn-list-onramp",
220 cloudType: "AWS",
221 type: "OnrampTypeSingle",
222 region: vpcRegion!,
223 vpc: vpcId!,
224 dynamicRouting: false,
225 installRoutesInCloud: false,
226 installRoutesInMagicWan: false,
227 }),
228 );
229
230 const provider = yield* Provider.findProvider(
231 Cloudflare.MagicCloudNetworking.OnRamp,
232 );
233 const all = yield* provider.list();
234
235 expect(all.some((x) => x.onRampId === onramp.onRampId)).toBe(true);
236 expect(all.some((x) => x.name === "alchemy-mcn-list-onramp")).toBe(true);
237
238 yield* stack.destroy();
239
240 yield* expectGone(accountId, onramp.onRampId);
241 }).pipe(logLevel),
242 {
243 tags: [
244 "provider:cloudflare",
245 "provider:cloudflare:magiccloudnetworking",
246 "live",
247 ],
248 timeout: 120_000,
249 },
250);