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 { 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// Ride out 403 blips (`Forbidden`) while the harness-minted token
19// propagates across Cloudflare's edge.
20const getLocation = (accountId: string, locationId: string) =>
21 zeroTrust.getGatewayLocation({ accountId, locationId }).pipe(
22 Effect.retry({
23 while: (e) => e._tag === "Forbidden",
24 schedule: Schedule.exponential("500 millis"),
25 times: 8,
26 }),
27 );
28
29// A deleted location surfaces as `GatewayLocationNotFound` (code 1103).
30const expectGone = (accountId: string, locationId: string) =>
31 getLocation(accountId, locationId).pipe(
32 Effect.flatMap(() => Effect.fail({ _tag: "LocationNotDeleted" } as const)),
33 Effect.catchTag("GatewayLocationNotFound", () => Effect.void),
34 Effect.retry({
35 while: (e) => e._tag === "LocationNotDeleted",
36 schedule: Schedule.max([
37 Schedule.exponential("500 millis"),
38 Schedule.recurs(10),
39 ]),
40 }),
41 );
42
43test.provider(
44 "create, verify, and destroy a location",
45 (stack) =>
46 Effect.gen(function* () {
47 const { accountId } = yield* yield* CloudflareEnvironment;
48
49 yield* stack.destroy();
50
51 const location = yield* stack.deploy(
52 Cloudflare.Gateway.Location("BasicLocation", {
53 name: "alchemy-zt-location-basic",
54 ecsSupport: false,
55 }),
56 );
57
58 expect(location.locationId).toBeTruthy();
59 expect(location.accountId).toEqual(accountId);
60 expect(location.name).toEqual("alchemy-zt-location-basic");
61 expect(location.clientDefault).toBe(false);
62 // Cloudflare assigns a stable DoH subdomain to every location.
63 expect(location.dohSubdomain).toBeTruthy();
64
65 const live = yield* getLocation(accountId, location.locationId);
66 expect(live.name).toEqual("alchemy-zt-location-basic");
67 expect(live.dohSubdomain).toEqual(location.dohSubdomain);
68
69 yield* stack.destroy();
70 yield* expectGone(accountId, location.locationId);
71 }).pipe(logLevel),
72 { tags: ["provider:cloudflare", "provider:cloudflare:gateway", "live"] },
73);
74
75test.provider(
76 "update name and ecsSupport in place (same id)",
77 (stack) =>
78 Effect.gen(function* () {
79 const { accountId } = yield* yield* CloudflareEnvironment;
80
81 yield* stack.destroy();
82
83 const initial = yield* stack.deploy(
84 Cloudflare.Gateway.Location("UpdateLocation", {
85 name: "alchemy-zt-location-update",
86 ecsSupport: false,
87 }),
88 );
89 expect(initial.ecsSupport).toBe(false);
90
91 const updated = yield* stack.deploy(
92 Cloudflare.Gateway.Location("UpdateLocation", {
93 name: "alchemy-zt-location-update-v2",
94 ecsSupport: true,
95 }),
96 );
97
98 // Same location mutated in place — not a replacement; the
99 // server-assigned DoH subdomain is stable across updates.
100 expect(updated.locationId).toEqual(initial.locationId);
101 expect(updated.dohSubdomain).toEqual(initial.dohSubdomain);
102 expect(updated.name).toEqual("alchemy-zt-location-update-v2");
103 expect(updated.ecsSupport).toBe(true);
104
105 const live = yield* getLocation(accountId, updated.locationId);
106 expect(live.name).toEqual("alchemy-zt-location-update-v2");
107 expect(live.ecsSupport).toBe(true);
108
109 // Redeploying identical props is a no-op (still the same location).
110 const noop = yield* stack.deploy(
111 Cloudflare.Gateway.Location("UpdateLocation", {
112 name: "alchemy-zt-location-update-v2",
113 ecsSupport: true,
114 }),
115 );
116 expect(noop.locationId).toEqual(initial.locationId);
117
118 yield* stack.destroy();
119 yield* expectGone(accountId, initial.locationId);
120 }).pipe(logLevel),
121 { tags: ["provider:cloudflare", "provider:cloudflare:gateway", "live"] },
122);
123
124test.provider(
125 "recreates after out-of-band delete",
126 (stack) =>
127 Effect.gen(function* () {
128 const { accountId } = yield* yield* CloudflareEnvironment;
129
130 yield* stack.destroy();
131
132 const location = yield* stack.deploy(
133 Cloudflare.Gateway.Location("HealLocation", {
134 name: "alchemy-zt-location-heal",
135 ecsSupport: false,
136 }),
137 );
138
139 yield* zeroTrust
140 .deleteGatewayLocation({ accountId, locationId: location.locationId })
141 .pipe(
142 Effect.retry({
143 while: (e) => e._tag === "Forbidden",
144 schedule: Schedule.exponential("500 millis"),
145 times: 8,
146 }),
147 );
148
149 // Change a prop to force reconcile — it must observe the location as
150 // missing and recreate it instead of failing on a 404.
151 const healed = yield* stack.deploy(
152 Cloudflare.Gateway.Location("HealLocation", {
153 name: "alchemy-zt-location-heal",
154 ecsSupport: true,
155 }),
156 );
157
158 expect(healed.locationId).not.toEqual(location.locationId);
159 expect(healed.ecsSupport).toBe(true);
160 const live = yield* getLocation(accountId, healed.locationId);
161 expect(live.ecsSupport).toBe(true);
162
163 yield* stack.destroy();
164 yield* expectGone(accountId, healed.locationId);
165 }).pipe(logLevel),
166 { tags: ["provider:cloudflare", "provider:cloudflare:gateway", "live"] },
167);
168
169// Canonical `list()` test (account-scoped collection): deploy a location,
170// then enumerate every Gateway location in the account via the typed
171// provider and assert the deployed one is present in the exhaustively
172// paginated result.
173test.provider(
174 "list enumerates deployed gateway locations",
175 (stack) =>
176 Effect.gen(function* () {
177 yield* stack.destroy();
178
179 const location = yield* stack.deploy(
180 Cloudflare.Gateway.Location("ListLocation", {
181 name: "alchemy-zt-location-list",
182 ecsSupport: false,
183 }),
184 );
185
186 const provider = yield* Provider.findProvider(
187 Cloudflare.Gateway.Location,
188 );
189 const all = yield* provider.list();
190
191 // The deployed location appears with the exact `read` Attributes shape.
192 const found = all.find((l) => l.locationId === location.locationId);
193 expect(found).toBeDefined();
194 expect(found?.name).toEqual("alchemy-zt-location-list");
195 expect(found?.accountId).toEqual(location.accountId);
196
197 yield* stack.destroy();
198 yield* expectGone(location.accountId, location.locationId);
199 }).pipe(logLevel),
200 { tags: ["provider:cloudflare", "provider:cloudflare:gateway", "live"] },
201);