1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as magicTransit from "@distilled.cloud/cloudflare/magic-transit";
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 Transit is an entitlement-gated product. On the standard testing
19// account every Magic tunnel/route call fails with the typed
20// `MagicTransitNotOnboarded` error (Cloudflare code 1012, "Magic Transit
21// is not onboarded for this account") or `Forbidden` (403) depending on
22// token scope. The lifecycle tests below are gated behind an explicit
23// opt-in env flag for entitled accounts; the probe test always runs and
24// pins the typed tag.
25const entitled = !!process.env.CLOUDFLARE_TEST_MAGIC_TRANSIT;
26
27// Entitled accounts must supply a real Cloudflare anycast endpoint that is
28// allocated to the account — there is no way to derive one.
29const cfEndpoint = process.env.CLOUDFLARE_TEST_MT_CF_ENDPOINT ?? "203.0.113.1";
30
31const getTunnel = (accountId: string, greTunnelId: string) =>
32 magicTransit.getGreTunnel({
33 accountId,
34 greTunnelId,
35 xMagicNewHcTarget: true,
36 });
37
38// Poll until the tunnel is gone after destroy. Cloudflare answers GET for
39// a missing tunnel with the typed `GreTunnelNotFound` (code 1029).
40const expectGone = (accountId: string, greTunnelId: string) =>
41 getTunnel(accountId, greTunnelId).pipe(
42 Effect.flatMap(() => Effect.fail({ _tag: "TunnelNotDeleted" } as const)),
43 Effect.catchTag("GreTunnelNotFound", () => Effect.void),
44 Effect.retry({
45 while: (e) => e._tag === "TunnelNotDeleted",
46 schedule: Schedule.max([
47 Schedule.exponential("500 millis"),
48 Schedule.recurs(10),
49 ]),
50 }),
51 );
52
53test.provider(
54 "unentitled accounts surface the typed MagicTransitNotOnboarded error",
55 (stack) =>
56 Effect.gen(function* () {
57 const { accountId } = yield* yield* CloudflareEnvironment;
58
59 yield* stack.destroy();
60
61 const canList = yield* magicTransit
62 .listGreTunnels({ accountId, xMagicNewHcTarget: true })
63 .pipe(
64 Effect.as(true),
65 Effect.catchTag(["MagicTransitNotOnboarded", "Forbidden"], () =>
66 Effect.succeed(false),
67 ),
68 );
69 if (canList) {
70 // Entitled account — the gated lifecycle test covers real behavior.
71 yield* Effect.logInfo(
72 "account is Magic Transit-entitled; probe test is a no-op",
73 );
74 return;
75 }
76
77 // The typed tag — not UnknownCloudflareError, not a status check.
78 const error = yield* magicTransit
79 .listGreTunnels({ accountId, xMagicNewHcTarget: true })
80 .pipe(Effect.flip);
81 expect(["MagicTransitNotOnboarded", "Forbidden"]).toContain(error._tag);
82
83 const createError = yield* magicTransit
84 .createGreTunnel({
85 accountId,
86 xMagicNewHcTarget: true,
87 name: "alch-gre-probe",
88 cloudflareGreEndpoint: cfEndpoint,
89 customerGreEndpoint: "198.51.100.10",
90 interfaceAddress: "10.213.10.10/31",
91 })
92 .pipe(Effect.flip);
93 expect(["MagicTransitNotOnboarded", "Forbidden"]).toContain(
94 createError._tag,
95 );
96
97 yield* stack.destroy();
98 }).pipe(logLevel),
99 {
100 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
101 timeout: 120_000,
102 },
103);
104
105// `list()` enumerates every GRE tunnel in the account. On an unentitled
106// account the underlying `listGreTunnels` rejects with the typed
107// `MagicTransitNotOnboarded` / `Forbidden` tags, which the provider maps to
108// `[]` — so the read-only assertion is safe to run unconditionally. On an
109// entitled account (CLOUDFLARE_TEST_MAGIC_TRANSIT=1) we deploy a tunnel and
110// assert it shows up in the exhaustively-enumerated result.
111test.provider(
112 "list enumerates the deployed GRE tunnels",
113 (stack) =>
114 Effect.gen(function* () {
115 yield* stack.destroy();
116
117 const provider = yield* Provider.findProvider(
118 Cloudflare.MagicTransit.GreTunnel,
119 );
120
121 if (!entitled) {
122 // Unentitled: list() swallows the typed entitlement tag and yields [].
123 const all = yield* provider.list();
124 expect(Array.isArray(all)).toBe(true);
125 return;
126 }
127
128 const tunnel = yield* stack.deploy(
129 Cloudflare.MagicTransit.GreTunnel("ListGre", {
130 name: "alch-gre-list",
131 cloudflareGreEndpoint: cfEndpoint,
132 customerGreEndpoint: "198.51.100.20",
133 interfaceAddress: "10.213.20.20/31",
134 }),
135 );
136
137 const all = yield* provider.list();
138 expect(all.some((t) => t.tunnelId === tunnel.tunnelId)).toBe(true);
139 expect(all.some((t) => t.name === "alch-gre-list")).toBe(true);
140
141 yield* stack.destroy();
142 yield* expectGone(tunnel.accountId, tunnel.tunnelId);
143 }).pipe(logLevel),
144 {
145 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
146 timeout: 120_000,
147 },
148);
149
150test.provider.skipIf(!entitled)(
151 "creates a GRE tunnel, updates mutable props in place, and destroys it",
152 (stack) =>
153 Effect.gen(function* () {
154 const { accountId } = yield* yield* CloudflareEnvironment;
155
156 yield* stack.destroy();
157
158 const tunnel = yield* stack.deploy(
159 Cloudflare.MagicTransit.GreTunnel("Gre", {
160 name: "alch-gre-test1",
161 cloudflareGreEndpoint: cfEndpoint,
162 customerGreEndpoint: "198.51.100.10",
163 interfaceAddress: "10.213.10.10/31",
164 description: "alchemy gre tunnel test",
165 ttl: 64,
166 mtu: 1476,
167 }),
168 );
169
170 expect(tunnel.tunnelId).toBeTruthy();
171 expect(tunnel.accountId).toEqual(accountId);
172 expect(tunnel.name).toEqual("alch-gre-test1");
173 expect(tunnel.cloudflareGreEndpoint).toEqual(cfEndpoint);
174 expect(tunnel.customerGreEndpoint).toEqual("198.51.100.10");
175 expect(tunnel.interfaceAddress).toEqual("10.213.10.10/31");
176
177 // Out-of-band verification via the distilled API.
178 const live = yield* getTunnel(accountId, tunnel.tunnelId);
179 expect(live.greTunnel?.name).toEqual("alch-gre-test1");
180 expect(live.greTunnel?.description).toEqual("alchemy gre tunnel test");
181
182 // Update mutable props in place — same tunnelId.
183 const updated = yield* stack.deploy(
184 Cloudflare.MagicTransit.GreTunnel("Gre", {
185 name: "alch-gre-test1",
186 cloudflareGreEndpoint: cfEndpoint,
187 customerGreEndpoint: "198.51.100.10",
188 interfaceAddress: "10.213.10.10/31",
189 description: "alchemy gre tunnel test v2",
190 ttl: 60,
191 mtu: 1400,
192 }),
193 );
194
195 expect(updated.tunnelId).toEqual(tunnel.tunnelId);
196 expect(updated.description).toEqual("alchemy gre tunnel test v2");
197 expect(updated.ttl).toEqual(60);
198 expect(updated.mtu).toEqual(1400);
199
200 // The tunnel name is its routing identity — changing it replaces.
201 const replaced = yield* stack.deploy(
202 Cloudflare.MagicTransit.GreTunnel("Gre", {
203 name: "alch-gre-test2",
204 cloudflareGreEndpoint: cfEndpoint,
205 customerGreEndpoint: "198.51.100.10",
206 interfaceAddress: "10.213.10.10/31",
207 }),
208 );
209
210 expect(replaced.tunnelId).not.toEqual(tunnel.tunnelId);
211 expect(replaced.name).toEqual("alch-gre-test2");
212 yield* expectGone(accountId, tunnel.tunnelId);
213
214 yield* stack.destroy();
215
216 yield* expectGone(accountId, replaced.tunnelId);
217 }).pipe(logLevel),
218 {
219 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
220 timeout: 120_000,
221 },
222);