1import { adopt } from "@/AdoptPolicy";
2import * as Cloudflare from "@/Cloudflare";
3import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
4import * as Provider from "@/Provider";
5import * as Test from "@/Test/Alchemy";
6import * as zeroTrust from "@distilled.cloud/cloudflare/zero-trust";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({ providers: Cloudflare.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19// The scoped API token the test harness mints propagates eventually-
20// consistently across Cloudflare's edge — ride out 403 blips (`Forbidden`,
21// declared in the distilled error union) on out-of-band verification calls.
22const getVnet = (accountId: string, virtualNetworkId: string) =>
23 zeroTrust.getNetworkVirtualNetwork({ accountId, virtualNetworkId }).pipe(
24 Effect.retry({
25 while: (e) => e._tag === "Forbidden",
26 schedule: Schedule.exponential("500 millis"),
27 times: 8,
28 }),
29 );
30
31// A destroyed virtual network either 404s (`VirtualNetworkNotFound`, code
32// 1046) or is soft-deleted with `deletedAt` set.
33const expectGone = (accountId: string, virtualNetworkId: string) =>
34 getVnet(accountId, virtualNetworkId).pipe(
35 Effect.flatMap((vnet) =>
36 vnet.deletedAt
37 ? Effect.void
38 : Effect.fail({ _tag: "VnetNotDeleted" } as const),
39 ),
40 Effect.catchTag("VirtualNetworkNotFound", () => Effect.void),
41 Effect.retry({
42 while: (e) => e._tag === "VnetNotDeleted",
43 schedule: Schedule.max([
44 Schedule.exponential("500 millis"),
45 Schedule.recurs(10),
46 ]),
47 }),
48 );
49
50test.provider(
51 "create, verify, and destroy a virtual network",
52 (stack) =>
53 Effect.gen(function* () {
54 const { accountId } = yield* yield* CloudflareEnvironment;
55
56 yield* stack.destroy();
57
58 const vnet = yield* stack.deploy(
59 Cloudflare.Tunnel.VirtualNetwork("BasicVnet", {
60 name: "alchemy-zt-vnet-basic",
61 comment: "alchemy test vnet",
62 }).pipe(adopt(true)),
63 );
64
65 expect(vnet.virtualNetworkId).toBeTruthy();
66 expect(vnet.accountId).toEqual(accountId);
67 expect(vnet.name).toEqual("alchemy-zt-vnet-basic");
68 expect(vnet.comment).toEqual("alchemy test vnet");
69 expect(vnet.isDefaultNetwork).toBe(false);
70
71 const live = yield* getVnet(accountId, vnet.virtualNetworkId);
72 expect(live.name).toEqual("alchemy-zt-vnet-basic");
73 expect(live.comment).toEqual("alchemy test vnet");
74
75 yield* stack.destroy();
76 yield* expectGone(accountId, vnet.virtualNetworkId);
77 }).pipe(logLevel),
78 { tags: ["provider:cloudflare", "provider:cloudflare:tunnel", "live"] },
79);
80
81test.provider(
82 "update name and comment in place (same id)",
83 (stack) =>
84 Effect.gen(function* () {
85 const { accountId } = yield* yield* CloudflareEnvironment;
86
87 yield* stack.destroy();
88
89 const initial = yield* stack.deploy(
90 Cloudflare.Tunnel.VirtualNetwork("UpdateVnet", {
91 name: "alchemy-zt-vnet-update",
92 comment: "v1",
93 }).pipe(adopt(true)),
94 );
95
96 const updated = yield* stack.deploy(
97 Cloudflare.Tunnel.VirtualNetwork("UpdateVnet", {
98 name: "alchemy-zt-vnet-update-v2",
99 comment: "v2",
100 }).pipe(adopt(true)),
101 );
102
103 // Same vnet mutated in place — not a replacement.
104 expect(updated.virtualNetworkId).toEqual(initial.virtualNetworkId);
105 expect(updated.name).toEqual("alchemy-zt-vnet-update-v2");
106 expect(updated.comment).toEqual("v2");
107
108 const live = yield* getVnet(accountId, updated.virtualNetworkId);
109 expect(live.name).toEqual("alchemy-zt-vnet-update-v2");
110 expect(live.comment).toEqual("v2");
111
112 // Redeploying identical props is a no-op.
113 const noop = yield* stack.deploy(
114 Cloudflare.Tunnel.VirtualNetwork("UpdateVnet", {
115 name: "alchemy-zt-vnet-update-v2",
116 comment: "v2",
117 }).pipe(adopt(true)),
118 );
119 expect(noop.virtualNetworkId).toEqual(initial.virtualNetworkId);
120
121 yield* stack.destroy();
122 yield* expectGone(accountId, initial.virtualNetworkId);
123 }).pipe(logLevel),
124 { tags: ["provider:cloudflare", "provider:cloudflare:tunnel", "live"] },
125);
126
127test.provider(
128 "recreates after out-of-band delete",
129 (stack) =>
130 Effect.gen(function* () {
131 const { accountId } = yield* yield* CloudflareEnvironment;
132
133 yield* stack.destroy();
134
135 const vnet = yield* stack.deploy(
136 Cloudflare.Tunnel.VirtualNetwork("HealVnet", {
137 name: "alchemy-zt-vnet-heal",
138 comment: "v1",
139 }).pipe(adopt(true)),
140 );
141
142 yield* zeroTrust
143 .deleteNetworkVirtualNetwork({
144 accountId,
145 virtualNetworkId: vnet.virtualNetworkId,
146 })
147 .pipe(
148 Effect.retry({
149 while: (e) => e._tag === "Forbidden",
150 schedule: Schedule.exponential("500 millis"),
151 times: 8,
152 }),
153 );
154
155 // Change a prop to force reconcile — it must observe the vnet as
156 // missing and recreate it instead of failing on a 404.
157 const healed = yield* stack.deploy(
158 Cloudflare.Tunnel.VirtualNetwork("HealVnet", {
159 name: "alchemy-zt-vnet-heal",
160 comment: "v2",
161 }).pipe(adopt(true)),
162 );
163
164 expect(healed.virtualNetworkId).not.toEqual(vnet.virtualNetworkId);
165 expect(healed.comment).toEqual("v2");
166 const live = yield* getVnet(accountId, healed.virtualNetworkId);
167 expect(live.comment).toEqual("v2");
168
169 yield* stack.destroy();
170 yield* expectGone(accountId, healed.virtualNetworkId);
171 }).pipe(logLevel),
172 { tags: ["provider:cloudflare", "provider:cloudflare:tunnel", "live"] },
173);
174
175test.provider(
176 "list enumerates the deployed virtual network",
177 (stack) =>
178 Effect.gen(function* () {
179 yield* stack.destroy();
180
181 const deployed = yield* stack.deploy(
182 Cloudflare.Tunnel.VirtualNetwork("ListVnet", {
183 name: "alchemy-zt-vnet-list",
184 comment: "alchemy list test vnet",
185 }).pipe(adopt(true)),
186 );
187
188 const provider = yield* Provider.findProvider(
189 Cloudflare.Tunnel.VirtualNetwork,
190 );
191 const all = yield* provider.list();
192
193 expect(
194 all.some((v) => v.virtualNetworkId === deployed.virtualNetworkId),
195 ).toBe(true);
196 const found = all.find(
197 (v) => v.virtualNetworkId === deployed.virtualNetworkId,
198 );
199 expect(found?.name).toEqual("alchemy-zt-vnet-list");
200
201 yield* stack.destroy();
202 yield* expectGone(deployed.accountId, deployed.virtualNetworkId);
203 }).pipe(logLevel),
204 { tags: ["provider:cloudflare", "provider:cloudflare:tunnel", "live"] },
205);