1import * as Cloudflare from "@/Cloudflare";
2import * as Provider from "@/Provider";
3import * as Test from "@/Test/Alchemy";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import Stack from "./fixtures/stack.ts";
10
11const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
12 providers: Cloudflare.providers(),
13});
14
15const logLevel = Effect.provideService(
16 MinimumLogLevel,
17 process.env.DEBUG ? "Debug" : "Info",
18);
19
20const stack = beforeAll(deploy(Stack));
21afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
22
23// Hit a worker route, retrying through fresh-worker edge propagation, and
24// decode the JSON body.
25const hit = Effect.fn(function* (path: string) {
26 const client = yield* HttpClient.HttpClient;
27 const res = yield* client.get(path).pipe(
28 Effect.flatMap((res) =>
29 res.status === 200
30 ? Effect.succeed(res)
31 : Effect.fail(new Error(`Worker not ready: ${res.status}`)),
32 ),
33 Effect.retry({ schedule: Schedule.exponential("500 millis"), times: 15 }),
34 );
35 return yield* res.json;
36});
37
38// Deterministic tunnel names: identical on every run so a crashed run never
39// leaks a uniquely-named tunnel. The fixture routes purge any leftover tunnel
40// with the same name before creating, making each run self-healing.
41const writeName = "alchemy-tunnel-test-write";
42const readWriteName = "alchemy-tunnel-test-readwrite";
43
44describe(
45 "Tunnel runtime bindings",
46 {
47 tags: [
48 "provider:cloudflare",
49 "provider:cloudflare:tunnel",
50 "provider:cloudflare:worker",
51 "live",
52 ],
53 },
54 () => {
55 test(
56 "TunnelRead lists tunnels with a read-scoped token",
57 Effect.gen(function* () {
58 const { effectUrl } = yield* stack;
59 const body = (yield* hit(`${effectUrl}/read`)) as { count: number };
60 expect(body.count).toBeTypeOf("number");
61 }).pipe(logLevel),
62 { timeout: 180_000 },
63 );
64
65 test(
66 "TunnelWrite creates and deletes a tunnel with a write-scoped token",
67 Effect.gen(function* () {
68 const { effectUrl } = yield* stack;
69 const body = (yield* hit(
70 `${effectUrl}/write?name=${encodeURIComponent(writeName)}`,
71 )) as { id: string; deleted: boolean };
72 expect(body.id).toBeTypeOf("string");
73 expect(body.id.length).toBeGreaterThan(0);
74 expect(body.deleted).toBe(true);
75 }).pipe(logLevel),
76 { timeout: 180_000 },
77 );
78
79 test(
80 "TunnelReadWrite drives the full CRUD surface",
81 Effect.gen(function* () {
82 const { effectUrl } = yield* stack;
83 const name = readWriteName;
84 const body = (yield* hit(
85 `${effectUrl}/readwrite?name=${encodeURIComponent(name)}`,
86 )) as {
87 id: string;
88 getName: string;
89 count: number;
90 updatedName: string;
91 hasToken: boolean;
92 deleted: boolean;
93 };
94 expect(body.id.length).toBeGreaterThan(0);
95 expect(body.getName).toBe(name);
96 expect(body.count).toBeGreaterThan(0);
97 expect(body.updatedName).toBe(`${name}-renamed`);
98 expect(body.hasToken).toBe(true);
99 expect(body.deleted).toBe(true);
100 }).pipe(logLevel),
101 { timeout: 180_000 },
102 );
103 },
104);
105
106// Canonical `list()` test (account collection): deploy a tunnel, resolve the
107// provider with the typed `Provider.findProvider`, enumerate every cfd_tunnel
108// in the account, and assert the deployed tunnel is present. Bracket with
109// `stack.destroy()` so the test is isolated and leaves no cloud residue.
110describe(
111 "Tunnel.list",
112 {
113 tags: [
114 "provider:cloudflare",
115 "provider:cloudflare:tunnel",
116 "provider:cloudflare:worker",
117 "live",
118 ],
119 },
120 () => {
121 test.provider("list enumerates the deployed tunnel", (stack) =>
122 Effect.gen(function* () {
123 yield* stack.destroy();
124
125 const deployed = yield* stack.deploy(
126 Effect.gen(function* () {
127 return yield* Cloudflare.Tunnel.Tunnel("ListTunnel");
128 }),
129 );
130
131 const provider = yield* Provider.findProvider(Cloudflare.Tunnel.Tunnel);
132 const all = yield* provider.list();
133
134 expect(all.some((t) => t.tunnelId === deployed.tunnelId)).toBe(true);
135
136 yield* stack.destroy();
137 }).pipe(logLevel),
138 );
139 },
140);