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 custom apps are entitlement-gated. On the standard testing
19// account every app call fails with the typed `MagicWanUnauthorized`
20// error (Cloudflare code 1025) or `Forbidden` (403) depending on token
21// scope. The lifecycle test below is gated behind an explicit opt-in env
22// flag for entitled accounts; the probe test always runs and pins the
23// typed tag.
24const entitled = !!process.env.CLOUDFLARE_TEST_MAGIC_WAN;
25
26// There is no getApp endpoint — scan the list for the app id.
27const findApp = (accountId: string, appId: string) =>
28 magicTransit
29 .listApps({ accountId })
30 .pipe(
31 Effect.map((r) =>
32 r.result.find(
33 (app) => "accountAppId" in app && app.accountAppId === appId,
34 ),
35 ),
36 );
37
38// Poll until the app is gone from the list after destroy.
39const expectGone = (accountId: string, appId: string) =>
40 findApp(accountId, appId).pipe(
41 Effect.flatMap((found) =>
42 found ? Effect.fail({ _tag: "AppNotDeleted" } as const) : Effect.void,
43 ),
44 Effect.retry({
45 while: (e) => e._tag === "AppNotDeleted",
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 MagicWanUnauthorized error",
55 (stack) =>
56 Effect.gen(function* () {
57 const { accountId } = yield* yield* CloudflareEnvironment;
58
59 yield* stack.destroy();
60
61 const canList = yield* magicTransit.listApps({ accountId }).pipe(
62 Effect.as(true),
63 Effect.catchTag(["MagicWanUnauthorized", "Forbidden"], () =>
64 Effect.succeed(false),
65 ),
66 );
67 if (canList) {
68 // Entitled account — the gated lifecycle test covers real behavior.
69 yield* Effect.logInfo(
70 "account is Magic WAN-entitled; probe test is a no-op",
71 );
72 return;
73 }
74
75 // The typed tag — not UnknownCloudflareError, not a status check.
76 const error = yield* magicTransit
77 .listApps({ accountId })
78 .pipe(Effect.flip);
79 expect(["MagicWanUnauthorized", "Forbidden"]).toContain(error._tag);
80
81 const createError = yield* magicTransit
82 .createApp({
83 accountId,
84 name: "alchemy-app-probe",
85 type: "Collaboration",
86 hostnames: ["probe.alchemy.test"],
87 })
88 .pipe(Effect.flip);
89 expect(["MagicWanUnauthorized", "Forbidden"]).toContain(createError._tag);
90
91 yield* stack.destroy();
92 }).pipe(logLevel),
93 {
94 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
95 timeout: 120_000,
96 },
97);
98
99test.provider.skipIf(!entitled)(
100 "creates a custom app, 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 app = yield* stack.deploy(
108 Cloudflare.MagicTransit.MagicApp("App", {
109 name: "alchemy-magic-app",
110 type: "Collaboration",
111 hostnames: ["crm.alchemy.test"],
112 }),
113 );
114
115 expect(app.appId).toBeTruthy();
116 expect(app.accountId).toEqual(accountId);
117 expect(app.name).toEqual("alchemy-magic-app");
118 expect(app.type).toEqual("Collaboration");
119 expect(app.hostnames).toEqual(["crm.alchemy.test"]);
120
121 // Out-of-band verification via the distilled API.
122 const live = yield* findApp(accountId, app.appId);
123 expect(live).toBeDefined();
124
125 // Update mutable props in place — same appId.
126 const updated = yield* stack.deploy(
127 Cloudflare.MagicTransit.MagicApp("App", {
128 name: "alchemy-magic-app-v2",
129 type: "Collaboration",
130 hostnames: ["crm.alchemy.test", "erp.alchemy.test"],
131 }),
132 );
133
134 expect(updated.appId).toEqual(app.appId);
135 expect(updated.name).toEqual("alchemy-magic-app-v2");
136 expect([...(updated.hostnames ?? [])].sort()).toEqual([
137 "crm.alchemy.test",
138 "erp.alchemy.test",
139 ]);
140
141 yield* stack.destroy();
142
143 yield* expectGone(accountId, app.appId);
144 }).pipe(logLevel),
145 {
146 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
147 timeout: 120_000,
148 },
149);
150
151// Read-only list assertion. Always safe: on an unentitled account the
152// account-scoped apps list rejects with the typed `MagicWanUnauthorized` /
153// `Forbidden`, which `list()` maps to a well-typed empty array.
154test.provider(
155 "list enumerates account apps (well-typed [] when unentitled)",
156 (stack) =>
157 Effect.gen(function* () {
158 yield* stack.destroy();
159
160 const provider = yield* Provider.findProvider(
161 Cloudflare.MagicTransit.MagicApp,
162 );
163 const all = yield* provider.list();
164
165 expect(Array.isArray(all)).toBe(true);
166 for (const app of all) {
167 expect(typeof app.appId).toBe("string");
168 expect(typeof app.accountId).toBe("string");
169 }
170
171 yield* stack.destroy();
172 }).pipe(logLevel),
173 {
174 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
175 timeout: 120_000,
176 },
177);
178
179// Entitled-account variant: deploy a real app and assert it appears in the
180// exhaustively-paginated list result.
181test.provider.skipIf(!entitled)(
182 "list includes a deployed custom app",
183 (stack) =>
184 Effect.gen(function* () {
185 yield* stack.destroy();
186
187 const deployed = yield* stack.deploy(
188 Cloudflare.MagicTransit.MagicApp("ListApp", {
189 name: "alchemy-magic-list-app",
190 type: "Collaboration",
191 hostnames: ["list.alchemy.test"],
192 }),
193 );
194
195 const provider = yield* Provider.findProvider(
196 Cloudflare.MagicTransit.MagicApp,
197 );
198 const all = yield* provider.list();
199
200 expect(all.some((app) => app.appId === deployed.appId)).toBe(true);
201
202 yield* stack.destroy();
203 }).pipe(logLevel),
204 {
205 tags: ["provider:cloudflare", "provider:cloudflare:magictransit", "live"],
206 timeout: 120_000,
207 },
208);