1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as zeroTrust from "@distilled.cloud/cloudflare/zero-trust";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Option from "effect/Option";
9import { MinimumLogLevel } from "effect/References";
10import * as Stream from "effect/Stream";
11
12const { test } = Test.make({ providers: Cloudflare.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19// Deterministic per-test CIDRs. Using 10.99.X.0/24 keeps each test's network
20// disjoint from the others and from typical lab address space, so reruns and
21// parallel runs don't collide.
22const NETWORK_DEFAULT = "10.99.1.0/24";
23const NETWORK_UPDATE = "10.99.2.0/24";
24const NETWORK_ADOPT = "10.99.3.0/24";
25const NETWORK_LIST = "10.99.4.0/24";
26
27test.provider(
28 "create and delete route with default props",
29 (stack) =>
30 Effect.gen(function* () {
31 const { accountId } = yield* yield* CloudflareEnvironment;
32
33 yield* stack.destroy();
34
35 const { tunnel, route } = yield* stack.deploy(
36 Effect.gen(function* () {
37 const tunnel = yield* Cloudflare.Tunnel.Tunnel("RouteHostTunnel", {
38 adopt: true,
39 });
40 const route = yield* Cloudflare.Tunnel.Route("DefaultRoute", {
41 tunnelId: tunnel.tunnelId,
42 network: NETWORK_DEFAULT,
43 adopt: true,
44 });
45 return { tunnel, route };
46 }),
47 );
48
49 expect(route.routeId).toBeDefined();
50 expect(route.network).toEqual(NETWORK_DEFAULT);
51 expect(route.tunnelId).toEqual(tunnel.tunnelId);
52 expect(route.accountId).toEqual(accountId);
53
54 const actual = yield* zeroTrust.getNetworkRoute({
55 accountId,
56 routeId: route.routeId,
57 });
58 expect(actual.id).toEqual(route.routeId);
59 expect(actual.network).toEqual(NETWORK_DEFAULT);
60 expect(actual.tunnelId).toEqual(tunnel.tunnelId);
61
62 yield* stack.destroy();
63 }).pipe(logLevel),
64 { tags: ["provider:cloudflare", "provider:cloudflare:tunnel", "live"] },
65);
66
67test.provider(
68 "updating the comment patches in place",
69 (stack) =>
70 Effect.gen(function* () {
71 const { accountId } = yield* yield* CloudflareEnvironment;
72
73 yield* stack.destroy();
74
75 const initial = yield* stack.deploy(
76 Effect.gen(function* () {
77 const tunnel = yield* Cloudflare.Tunnel.Tunnel("RouteUpdateTunnel", {
78 adopt: true,
79 });
80 const route = yield* Cloudflare.Tunnel.Route("UpdateRoute", {
81 tunnelId: tunnel.tunnelId,
82 network: NETWORK_UPDATE,
83 comment: "v1",
84 adopt: true,
85 });
86 return { tunnel, route };
87 }),
88 );
89
90 expect(initial.route.comment).toEqual("v1");
91
92 const updated = yield* stack.deploy(
93 Effect.gen(function* () {
94 const tunnel = yield* Cloudflare.Tunnel.Tunnel("RouteUpdateTunnel", {
95 adopt: true,
96 });
97 const route = yield* Cloudflare.Tunnel.Route("UpdateRoute", {
98 tunnelId: tunnel.tunnelId,
99 network: NETWORK_UPDATE,
100 comment: "v2",
101 adopt: true,
102 });
103 return { tunnel, route };
104 }),
105 );
106
107 expect(updated.route.routeId).toEqual(initial.route.routeId);
108 expect(updated.route.comment).toEqual("v2");
109
110 const actual = yield* zeroTrust.getNetworkRoute({
111 accountId,
112 routeId: updated.route.routeId,
113 });
114 expect(actual.comment).toEqual("v2");
115
116 yield* stack.destroy();
117 }).pipe(logLevel),
118 { tags: ["provider:cloudflare", "provider:cloudflare:tunnel", "live"] },
119);
120
121test.provider(
122 "adopt: takes over a pre-existing route",
123 (stack) =>
124 Effect.gen(function* () {
125 const { accountId } = yield* yield* CloudflareEnvironment;
126
127 yield* stack.destroy();
128
129 // Stand up just the tunnel via the stack so we have a stable tunnelId to
130 // attach a manually-created route to.
131 const { tunnel } = yield* stack.deploy(
132 Effect.gen(function* () {
133 const tunnel = yield* Cloudflare.Tunnel.Tunnel("RouteAdoptTunnel", {
134 adopt: true,
135 });
136 return { tunnel };
137 }),
138 );
139
140 // Manually create the route outside of alchemy's state. The reconcile
141 // path with `adopt: true` should find this and take ownership rather
142 // than failing or creating a duplicate. Route networks are unique
143 // account-wide, so on Conflict (a prior failed run leaked the route)
144 // reuse the existing one — that's an equally valid pre-existing route.
145 const pre = yield* zeroTrust
146 .createNetworkRoute({
147 accountId,
148 tunnelId: tunnel.tunnelId,
149 network: NETWORK_ADOPT,
150 comment: "pre-existing",
151 })
152 .pipe(
153 Effect.catch(() =>
154 zeroTrust.listNetworkRoutes
155 .items({
156 accountId,
157 isDeleted: false,
158 networkSubset: NETWORK_ADOPT,
159 networkSuperset: NETWORK_ADOPT,
160 })
161 .pipe(
162 Stream.filter((r) => r.network === NETWORK_ADOPT),
163 Stream.runHead,
164 Effect.map(Option.getOrUndefined),
165 ),
166 ),
167 );
168 expect(pre?.id).toBeDefined();
169
170 const adopted = yield* stack.deploy(
171 Effect.gen(function* () {
172 const tunnel = yield* Cloudflare.Tunnel.Tunnel("RouteAdoptTunnel", {
173 adopt: true,
174 });
175 const route = yield* Cloudflare.Tunnel.Route("AdoptRoute", {
176 tunnelId: tunnel.tunnelId,
177 network: NETWORK_ADOPT,
178 adopt: true,
179 });
180 return { route };
181 }),
182 );
183
184 expect(adopted.route.routeId).toEqual(pre?.id);
185 expect(adopted.route.network).toEqual(NETWORK_ADOPT);
186
187 yield* stack.destroy();
188 }).pipe(logLevel),
189 { tags: ["provider:cloudflare", "provider:cloudflare:tunnel", "live"] },
190);
191
192test.provider(
193 "list enumerates the deployed route",
194 (stack) =>
195 Effect.gen(function* () {
196 const { accountId } = yield* yield* CloudflareEnvironment;
197
198 yield* stack.destroy();
199
200 const { route } = yield* stack.deploy(
201 Effect.gen(function* () {
202 const tunnel = yield* Cloudflare.Tunnel.Tunnel("RouteListTunnel", {
203 adopt: true,
204 });
205 const route = yield* Cloudflare.Tunnel.Route("ListRoute", {
206 tunnelId: tunnel.tunnelId,
207 network: NETWORK_LIST,
208 adopt: true,
209 });
210 return { route };
211 }),
212 );
213
214 const provider = yield* Provider.findProvider(Cloudflare.Tunnel.Route);
215 const all = yield* provider.list();
216
217 const found = all.find((r) => r.routeId === route.routeId);
218 expect(found).toBeDefined();
219 expect(found?.network).toEqual(NETWORK_LIST);
220 expect(found?.accountId).toEqual(accountId);
221
222 yield* stack.destroy();
223 }).pipe(logLevel),
224 { tags: ["provider:cloudflare", "provider:cloudflare:tunnel", "live"] },
225);