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 WAN sites (and their LANs / WANs / ACLs) are entitlement-gated.
19// On the standard testing account every site call fails with the typed
20// `MagicWanUnauthorized` error (Cloudflare code 1025) or `Forbidden`
21// (403) depending on token scope. The lifecycle test below is gated
22// behind an explicit opt-in env flag for entitled accounts; the probe
23// test always runs and pins the typed tag.
24const entitled = !!process.env.CLOUDFLARE_TEST_MAGIC_WAN;
25
26const getSite = (accountId: string, siteId: string) =>
27 magicTransit.getSite({ accountId, siteId });
28
29// Poll until the site is gone after destroy. Cloudflare answers GET for
30// a missing site with the typed `SiteNotFound` (404).
31const expectGone = (accountId: string, siteId: string) =>
32 getSite(accountId, siteId).pipe(
33 Effect.flatMap(() => Effect.fail({ _tag: "SiteNotDeleted" } as const)),
34 Effect.catchTag("SiteNotFound", () => Effect.void),
35 Effect.retry({
36 while: (e) => e._tag === "SiteNotDeleted",
37 schedule: Schedule.max([
38 Schedule.exponential("500 millis"),
39 Schedule.recurs(10),
40 ]),
41 }),
42 );
43
44test.provider(
45 "unentitled accounts surface the typed MagicWanUnauthorized error",
46 (stack) =>
47 Effect.gen(function* () {
48 const { accountId } = yield* yield* CloudflareEnvironment;
49
50 yield* stack.destroy();
51
52 const canList = yield* magicTransit.listSites({ accountId }).pipe(
53 Effect.as(true),
54 Effect.catchTag(["MagicWanUnauthorized", "Forbidden"], () =>
55 Effect.succeed(false),
56 ),
57 );
58 if (canList) {
59 // Entitled account — the gated lifecycle test covers real behavior.
60 yield* Effect.logInfo(
61 "account is Magic WAN-entitled; probe test is a no-op",
62 );
63 return;
64 }
65
66 // The typed tag — not UnknownCloudflareError, not a status check.
67 const error = yield* magicTransit
68 .listSites({ accountId })
69 .pipe(Effect.flip);
70 expect(["MagicWanUnauthorized", "Forbidden"]).toContain(error._tag);
71
72 const createError = yield* magicTransit
73 .createSite({ accountId, name: "alchemy-site-probe" })
74 .pipe(Effect.flip);
75 expect(["MagicWanUnauthorized", "Forbidden"]).toContain(createError._tag);
76
77 yield* stack.destroy();
78 }).pipe(logLevel),
79 {
80 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
81 timeout: 120_000,
82 },
83);
84
85// Canonical `list()` test (account collection): Magic WAN sites are
86// account-scoped, so `list()` paginates the account-wide sites API and
87// hydrates each into the `read` Attributes shape. On unentitled accounts
88// enumeration is rejected with the typed `MagicWanUnauthorized` (1025) or
89// `Forbidden` (403) and `list()` returns a well-typed `[]`. On entitled
90// accounts (CLOUDFLARE_TEST_MAGIC_WAN=1) we deploy a site and assert it
91// appears in the exhaustively-paginated result.
92test.provider(
93 "list enumerates account Magic WAN sites",
94 (stack) =>
95 Effect.gen(function* () {
96 yield* stack.destroy();
97
98 const provider = yield* Provider.findProvider(
99 Cloudflare.MagicTransit.MagicSite,
100 );
101
102 if (!entitled) {
103 // Unentitled account — `list()` swallows the typed entitlement /
104 // permission rejection and yields an empty, well-typed array.
105 const all = yield* provider.list();
106 expect(all).toEqual([]);
107 yield* stack.destroy();
108 return;
109 }
110
111 const site = yield* stack.deploy(
112 Cloudflare.MagicTransit.MagicSite("ListSite", {
113 name: "alchemy-magic-site-list",
114 description: "alchemy magic site list test",
115 }),
116 );
117 expect(site.siteId).toBeTruthy();
118
119 const all = yield* provider.list();
120 expect(all.some((s) => s.siteId === site.siteId)).toBe(true);
121
122 yield* stack.destroy();
123 }).pipe(logLevel),
124 {
125 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
126 timeout: 180_000,
127 },
128);
129
130test.provider.skipIf(!entitled)(
131 "creates a site with WAN, LAN, and ACL, updates in place, and destroys",
132 (stack) =>
133 Effect.gen(function* () {
134 const { accountId } = yield* yield* CloudflareEnvironment;
135
136 yield* stack.destroy();
137
138 const site = yield* stack.deploy(
139 Cloudflare.MagicTransit.MagicSite("Site", {
140 name: "alchemy-magic-site",
141 description: "alchemy magic site test",
142 location: { lat: "37.7749", lon: "-122.4194" },
143 }),
144 );
145
146 expect(site.siteId).toBeTruthy();
147 expect(site.accountId).toEqual(accountId);
148 expect(site.name).toEqual("alchemy-magic-site");
149 expect(site.description).toEqual("alchemy magic site test");
150
151 // Out-of-band verification via the distilled API.
152 const live = yield* getSite(accountId, site.siteId);
153 expect(live.name).toEqual("alchemy-magic-site");
154
155 const wan = yield* stack.deploy(
156 Cloudflare.MagicTransit.MagicSiteWan("Wan", {
157 siteId: site.siteId,
158 physport: 1,
159 name: "alchemy-site-wan",
160 priority: 10,
161 }),
162 );
163 expect(wan.wanId).toBeTruthy();
164 expect(wan.siteId).toEqual(site.siteId);
165
166 const lan1 = yield* stack.deploy(
167 Cloudflare.MagicTransit.MagicSiteLan("Lan1", {
168 siteId: site.siteId,
169 physport: 2,
170 name: "alchemy-site-lan1",
171 vlanTag: 10,
172 staticAddressing: { address: "192.168.10.1/24" },
173 }),
174 );
175 const lan2 = yield* stack.deploy(
176 Cloudflare.MagicTransit.MagicSiteLan("Lan2", {
177 siteId: site.siteId,
178 physport: 3,
179 name: "alchemy-site-lan2",
180 vlanTag: 20,
181 staticAddressing: { address: "192.168.20.1/24" },
182 }),
183 );
184 expect(lan1.lanId).toBeTruthy();
185 expect(lan2.lanId).toBeTruthy();
186
187 const acl = yield* stack.deploy(
188 Cloudflare.MagicTransit.MagicSiteAcl("Acl", {
189 siteId: site.siteId,
190 name: "alchemy-site-acl",
191 lan1: { lanId: lan1.lanId, ports: [443] },
192 lan2: { lanId: lan2.lanId },
193 protocols: ["tcp"],
194 }),
195 );
196 expect(acl.aclId).toBeTruthy();
197 expect(acl.siteId).toEqual(site.siteId);
198 expect(acl.name).toEqual("alchemy-site-acl");
199
200 // Update the site description in place — same siteId.
201 const updated = yield* stack.deploy(
202 Cloudflare.MagicTransit.MagicSite("Site", {
203 name: "alchemy-magic-site",
204 description: "alchemy magic site test v2",
205 location: { lat: "37.7749", lon: "-122.4194" },
206 }),
207 );
208 expect(updated.siteId).toEqual(site.siteId);
209 expect(updated.description).toEqual("alchemy magic site test v2");
210
211 yield* stack.destroy();
212
213 yield* expectGone(accountId, site.siteId);
214 }).pipe(logLevel),
215 {
216 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
217 timeout: 180_000,
218 },
219);