1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as diagnostics from "@distilled.cloud/cloudflare/diagnostics";
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
18const getHealthcheck = (accountId: string, id: string) =>
19 diagnostics.getEndpointHealthcheck({ accountId, id });
20
21// A GET issued immediately after a create/update can transiently 404
22// (`EndpointHealthcheckNotFound`, code 1022) while the new state
23// propagates across Cloudflare's edge — bounded-retry the out-of-band
24// verification read until it resolves.
25const getHealthcheckLive = (accountId: string, id: string) =>
26 getHealthcheck(accountId, id).pipe(
27 Effect.retry({
28 while: (e) => e._tag === "EndpointHealthcheckNotFound",
29 schedule: Schedule.max([
30 Schedule.exponential("500 millis"),
31 Schedule.recurs(10),
32 ]),
33 }),
34 );
35
36// Poll until the healthcheck is gone after destroy. Cloudflare answers
37// GET for a missing healthcheck with the typed
38// `EndpointHealthcheckNotFound` (code 1022, 404).
39const expectGone = (accountId: string, id: string) =>
40 getHealthcheck(accountId, id).pipe(
41 Effect.flatMap(() =>
42 Effect.fail({ _tag: "HealthcheckNotDeleted" } as const),
43 ),
44 Effect.catchTag("EndpointHealthcheckNotFound", () => Effect.void),
45 Effect.retry({
46 while: (e) => e._tag === "HealthcheckNotDeleted",
47 schedule: Schedule.max([
48 Schedule.exponential("500 millis"),
49 Schedule.recurs(10),
50 ]),
51 }),
52 );
53
54test.provider(
55 "rejects public IPs with the typed InvalidHealthcheckEndpoint error",
56 (stack) =>
57 Effect.gen(function* () {
58 const { accountId } = yield* yield* CloudflareEnvironment;
59
60 yield* stack.destroy();
61
62 // Cloudflare only accepts on-net (private) IPs; a public IP fails
63 // with code 1002 — pinned here as the typed tag.
64 const error = yield* diagnostics
65 .createEndpointHealthcheck({
66 accountId,
67 checkType: "icmp",
68 endpoint: "1.1.1.1",
69 name: "alchemy-diag-ehc-invalid-probe",
70 })
71 .pipe(Effect.flip);
72 expect(error._tag).toEqual("InvalidHealthcheckEndpoint");
73
74 yield* stack.destroy();
75 }).pipe(logLevel),
76 {
77 tags: ["provider:cloudflare", "provider:cloudflare:diagnostics", "live"],
78 timeout: 120_000,
79 },
80);
81
82// Out-of-band cleanup of healthchecks leaked by previous failed runs —
83// the test state store does not persist across runs, so a mid-test
84// failure leaves the cloud resource behind and the cold read would
85// (correctly) gate it behind the adopt policy.
86const cleanLeftovers = (accountId: string) =>
87 diagnostics.listEndpointHealthchecks({ accountId }).pipe(
88 Effect.flatMap(
89 Effect.fn(function* (list) {
90 for (const hc of list) {
91 if (hc.name?.startsWith("alchemy-diag-ehc") && hc.id) {
92 yield* diagnostics
93 .deleteEndpointHealthcheck({ accountId, id: hc.id })
94 .pipe(
95 Effect.catchTag(
96 "EndpointHealthcheckNotFound",
97 () => Effect.void,
98 ),
99 );
100 }
101 }
102 }),
103 ),
104 );
105
106test.provider(
107 "creates an endpoint healthcheck, updates in place, and destroys",
108 (stack) =>
109 Effect.gen(function* () {
110 const { accountId } = yield* yield* CloudflareEnvironment;
111
112 yield* stack.destroy();
113 yield* cleanLeftovers(accountId);
114
115 const check = yield* stack.deploy(
116 Cloudflare.Diagnostics.EndpointHealthcheck("Check", {
117 endpoint: "10.77.0.1",
118 name: "alchemy-diag-ehc",
119 }),
120 );
121
122 expect(check.healthcheckId).toBeTruthy();
123 expect(check.accountId).toEqual(accountId);
124 expect(check.checkType).toEqual("icmp");
125 expect(check.endpoint).toEqual("10.77.0.1");
126 expect(check.name).toEqual("alchemy-diag-ehc");
127
128 // Out-of-band verification via the distilled API.
129 const live = yield* getHealthcheckLive(accountId, check.healthcheckId);
130 expect(live.endpoint).toEqual("10.77.0.1");
131 expect(live.name).toEqual("alchemy-diag-ehc");
132
133 // Update the probed endpoint in place — same UUID.
134 const updated = yield* stack.deploy(
135 Cloudflare.Diagnostics.EndpointHealthcheck("Check", {
136 endpoint: "10.77.0.2",
137 name: "alchemy-diag-ehc",
138 }),
139 );
140 expect(updated.healthcheckId).toEqual(check.healthcheckId);
141 expect(updated.endpoint).toEqual("10.77.0.2");
142 expect(updated.name).toEqual("alchemy-diag-ehc");
143
144 const liveUpdated = yield* getHealthcheckLive(
145 accountId,
146 check.healthcheckId,
147 );
148 expect(liveUpdated.endpoint).toEqual("10.77.0.2");
149 expect(liveUpdated.name).toEqual("alchemy-diag-ehc");
150
151 // No-op deploy — converges without churn, same UUID.
152 const noop = yield* stack.deploy(
153 Cloudflare.Diagnostics.EndpointHealthcheck("Check", {
154 endpoint: "10.77.0.2",
155 name: "alchemy-diag-ehc",
156 }),
157 );
158 expect(noop.healthcheckId).toEqual(check.healthcheckId);
159
160 // Changing the name is create-only on Cloudflare's side — the
161 // resource is replaced with a fresh UUID and the old one removed.
162 // The endpoint changes too: endpoints are unique per account and
163 // replacement creates the new healthcheck before deleting the old.
164 const replaced = yield* stack.deploy(
165 Cloudflare.Diagnostics.EndpointHealthcheck("Check", {
166 endpoint: "10.77.0.3",
167 name: "alchemy-diag-ehc-v2",
168 }),
169 );
170 expect(replaced.healthcheckId).not.toEqual(check.healthcheckId);
171 expect(replaced.name).toEqual("alchemy-diag-ehc-v2");
172 yield* expectGone(accountId, check.healthcheckId);
173
174 const liveReplaced = yield* getHealthcheckLive(
175 accountId,
176 replaced.healthcheckId,
177 );
178 expect(liveReplaced.name).toEqual("alchemy-diag-ehc-v2");
179
180 yield* stack.destroy();
181
182 yield* expectGone(accountId, replaced.healthcheckId);
183 }).pipe(logLevel),
184 {
185 tags: ["provider:cloudflare", "provider:cloudflare:diagnostics", "live"],
186 timeout: 180_000,
187 },
188);
189
190// Canonical `list()` test (account collection): deploy a real healthcheck,
191// then assert its UUID appears in the exhaustively-enumerated result.
192test.provider(
193 "list enumerates the deployed endpoint healthcheck",
194 (stack) =>
195 Effect.gen(function* () {
196 const { accountId } = yield* yield* CloudflareEnvironment;
197
198 yield* stack.destroy();
199 yield* cleanLeftovers(accountId);
200
201 const deployed = yield* stack.deploy(
202 Cloudflare.Diagnostics.EndpointHealthcheck("ListCheck", {
203 endpoint: "10.78.0.1",
204 name: "alchemy-diag-ehc-list",
205 }),
206 );
207
208 const provider = yield* Provider.findProvider(
209 Cloudflare.Diagnostics.EndpointHealthcheck,
210 );
211 const all = yield* provider.list();
212
213 expect(
214 all.some((hc) => hc.healthcheckId === deployed.healthcheckId),
215 ).toBe(true);
216 // Each listed item is the full `read` Attributes shape.
217 const found = all.find(
218 (hc) => hc.healthcheckId === deployed.healthcheckId,
219 );
220 expect(found?.accountId).toEqual(accountId);
221 expect(found?.endpoint).toEqual("10.78.0.1");
222 expect(found?.checkType).toEqual("icmp");
223
224 yield* stack.destroy();
225 yield* expectGone(accountId, deployed.healthcheckId);
226 }).pipe(logLevel),
227 {
228 tags: ["provider:cloudflare", "provider:cloudflare:diagnostics", "live"],
229 timeout: 180_000,
230 },
231);