1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import { findZoneByName } from "@/Cloudflare/Zone/lookup";
4import * as Provider from "@/Provider";
5import * as Test from "@/Test/Alchemy";
6import * as customCertificates from "@distilled.cloud/cloudflare/custom-certificates";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import * as Redacted from "effect/Redacted";
10import { MinimumLogLevel } from "effect/References";
11import * as Schedule from "effect/Schedule";
12import * as FileSystem from "effect/FileSystem";
13import * as Path from "effect/Path";
14
15const { test } = Test.make({ providers: Cloudflare.providers() });
16
17const logLevel = Effect.provideService(
18 MinimumLogLevel,
19 process.env.DEBUG ? "Debug" : "Info",
20);
21
22const zoneName =
23 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
24
25// Custom (BYO) edge certificates are a Business/Enterprise feature. On the
26// testing account's zone every custom_certificates call fails with
27// "Plan level does not allow custom certificates ..." (Cloudflare error
28// code 1011), surfaced as the typed `PlanLevelNotAllowed` error. The full
29// lifecycle test below is gated behind an entitled zone id supplied via env.
30const entitledZoneId = process.env.CLOUDFLARE_TEST_CUSTOM_CERT_ZONE_ID;
31
32const resolveZoneId = Effect.gen(function* () {
33 const { accountId } = yield* yield* CloudflareEnvironment;
34 const zone = yield* findZoneByName({ accountId, name: zoneName });
35 if (!zone) {
36 return yield* Effect.die(
37 new Error(`zone "${zoneName}" not found in account`),
38 );
39 }
40 return zone.id;
41});
42
43// Checked-in self-signed PEM fixtures (CN=alchemy-test-2.us, 10y validity).
44// The upload API accepts self-signed certificates — validation only requires
45// that the key matches the certificate — keeping the test hermetic.
46const readFixture = (name: string) =>
47 Effect.gen(function* () {
48 const fs = yield* FileSystem.FileSystem;
49 const path = yield* Path.Path;
50 return yield* fs.readFileString(
51 path.join(import.meta.dirname, "fixtures", name),
52 );
53 });
54
55// A freshly minted scoped token propagates eventually-consistently across
56// Cloudflare's edge; ride out 403 blips on out-of-band verification calls.
57const forbiddenRetrySchedule = Schedule.exponential("500 millis");
58
59const getCertificate = (zoneId: string, customCertificateId: string) =>
60 customCertificates.getCustomCertificate({ zoneId, customCertificateId }).pipe(
61 Effect.retry({
62 while: (e) => e._tag === "Forbidden",
63 schedule: forbiddenRetrySchedule,
64 times: 8,
65 }),
66 );
67
68test.provider(
69 "surfaces the typed PlanLevelNotAllowed error on unentitled zones",
70 (stack) =>
71 Effect.gen(function* () {
72 const zoneId = yield* resolveZoneId;
73 const certificate = yield* readFixture("cert1.pem");
74 const privateKey = yield* readFixture("key1.pem");
75
76 yield* stack.destroy();
77
78 // The standard testing zone is not on a Business/Enterprise plan —
79 // the distilled call must fail with the typed entitlement tag.
80 const error = yield* customCertificates
81 .createCustomCertificate({
82 zoneId,
83 certificate,
84 privateKey,
85 type: "sni_custom",
86 })
87 .pipe(
88 Effect.retry({
89 while: (e) => e._tag === "Forbidden",
90 schedule: forbiddenRetrySchedule,
91 times: 8,
92 }),
93 Effect.flip,
94 );
95 expect(error._tag).toEqual("PlanLevelNotAllowed");
96
97 yield* stack.destroy();
98 }).pipe(logLevel),
99 {
100 tags: [
101 "provider:cloudflare",
102 "provider:cloudflare:customcertificate",
103 "provider:cloudflare:zone",
104 "live",
105 ],
106 },
107);
108
109// `list()` fans out over every zone in the account, paginates each zone's
110// custom certificates, and skips plan-gated zones via the typed
111// `PlanLevelNotAllowed`/`Forbidden` tags. On the testing account (no
112// Business/Enterprise zones) every zone is skipped, so the call must still
113// succeed and return a well-typed array (empty here) rather than throwing.
114test.provider(
115 "list enumerates custom certificates across zones",
116 (stack) =>
117 Effect.gen(function* () {
118 yield* stack.destroy();
119
120 const provider = yield* Provider.findProvider(
121 Cloudflare.CustomCertificate.CustomCertificate,
122 );
123 const all = yield* provider.list();
124
125 expect(Array.isArray(all)).toBe(true);
126 // Every item carries the full read Attributes shape.
127 for (const cert of all) {
128 expect(typeof cert.certificateId).toBe("string");
129 expect(typeof cert.zoneId).toBe("string");
130 }
131
132 // On an entitled zone, the certificate we deploy must appear in the
133 // exhaustively-paginated result.
134 if (entitledZoneId) {
135 const cert1 = yield* readFixture("cert1.pem");
136 const key1 = yield* readFixture("key1.pem");
137 const deployed = yield* stack.deploy(
138 Effect.gen(function* () {
139 return yield* Cloudflare.CustomCertificate.CustomCertificate(
140 "ListEdgeCert",
141 {
142 zoneId: entitledZoneId,
143 certificate: cert1,
144 privateKey: Redacted.make(key1),
145 type: "sni_custom",
146 bundleMethod: "force",
147 },
148 );
149 }),
150 );
151 const after = yield* provider.list();
152 expect(
153 after.some((c) => c.certificateId === deployed.certificateId),
154 ).toBe(true);
155 }
156
157 yield* stack.destroy();
158 }).pipe(logLevel),
159 {
160 tags: [
161 "provider:cloudflare",
162 "provider:cloudflare:customcertificate",
163 "live",
164 ],
165 timeout: 120_000,
166 },
167);
168
169test.provider.skipIf(!entitledZoneId)(
170 "uploads, rotates in place, replaces on type change, and deletes",
171 (stack) =>
172 Effect.gen(function* () {
173 const zoneId = entitledZoneId!;
174 const cert1 = yield* readFixture("cert1.pem");
175 const key1 = yield* readFixture("key1.pem");
176 const cert2 = yield* readFixture("cert2.pem");
177 const key2 = yield* readFixture("key2.pem");
178
179 yield* stack.destroy();
180
181 // Create — upload the first certificate/key pair.
182 const created = yield* stack.deploy(
183 Effect.gen(function* () {
184 return yield* Cloudflare.CustomCertificate.CustomCertificate(
185 "EdgeCert",
186 {
187 zoneId,
188 certificate: cert1,
189 privateKey: Redacted.make(key1),
190 type: "sni_custom",
191 bundleMethod: "force",
192 },
193 );
194 }),
195 );
196
197 expect(created.certificateId).toBeDefined();
198 expect(created.zoneId).toEqual(zoneId);
199 expect(created.type).toEqual("sni_custom");
200 expect(created.hosts).toContain("alchemy-test-2.us");
201 expect(created.contentHash).toHaveLength(64);
202
203 // Out-of-band verification via the distilled API.
204 const live = yield* getCertificate(zoneId, created.certificateId);
205 expect(live.id).toEqual(created.certificateId);
206
207 // Rotate in place — a new cert/key pair PATCHes the same id.
208 const rotated = yield* stack.deploy(
209 Effect.gen(function* () {
210 return yield* Cloudflare.CustomCertificate.CustomCertificate(
211 "EdgeCert",
212 {
213 zoneId,
214 certificate: cert2,
215 privateKey: Redacted.make(key2),
216 type: "sni_custom",
217 bundleMethod: "force",
218 },
219 );
220 }),
221 );
222 expect(rotated.certificateId).toEqual(created.certificateId);
223 expect(rotated.contentHash).not.toEqual(created.contentHash);
224 // cert2 has a different validity window than cert1.
225 expect(rotated.expiresOn).not.toEqual(created.expiresOn);
226
227 // Changing the immutable `type` triggers a replacement.
228 const replaced = yield* stack.deploy(
229 Effect.gen(function* () {
230 return yield* Cloudflare.CustomCertificate.CustomCertificate(
231 "EdgeCert",
232 {
233 zoneId,
234 certificate: cert2,
235 privateKey: Redacted.make(key2),
236 type: "legacy_custom",
237 bundleMethod: "force",
238 },
239 );
240 }),
241 );
242 expect(replaced.certificateId).not.toEqual(rotated.certificateId);
243 expect(replaced.type).toEqual("legacy_custom");
244
245 yield* stack.destroy();
246
247 // Destroy deleted the certificate — the typed NotFound tag confirms.
248 const gone = yield* customCertificates
249 .getCustomCertificate({
250 zoneId,
251 customCertificateId: replaced.certificateId,
252 })
253 .pipe(
254 Effect.retry({
255 while: (e) => e._tag === "Forbidden",
256 schedule: forbiddenRetrySchedule,
257 times: 8,
258 }),
259 Effect.flip,
260 );
261 expect(gone._tag).toEqual("CustomCertificateNotFound");
262 }).pipe(logLevel),
263 {
264 tags: [
265 "provider:cloudflare",
266 "provider:cloudflare:customcertificate",
267 "live",
268 ],
269 timeout: 120_000,
270 },
271);