1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import { poll } from "@/Util/poll.ts";
6import * as mtls from "@distilled.cloud/cloudflare/mtls-certificates";
7import { describe, 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 { CA_CERT_1, CA_CERT_2, LEAF_CERT, LEAF_KEY } from "./fixtures/certs.ts";
13
14const { test } = Test.make({ providers: Cloudflare.providers() });
15
16const logLevel = Effect.provideService(
17 MinimumLogLevel,
18 process.env.DEBUG ? "Debug" : "Info",
19);
20
21// Cloudflare dedups mTLS certificates by PEM content account-wide and rejects
22// a duplicate upload with `CertificateAlreadyExists` (code 1471). Three of
23// these cases upload the same `CA_CERT_1`, so under the suite's default
24// concurrent execution they collide on identical content (and the
25// adopt-by-content recovery races a sibling that is mid-create or mid-delete).
26// `describe.sequential` runs them one at a time so each create→…→destroy owns
27// the content exclusively.
28describe.sequential(
29 "MtlsCertificate",
30 {
31 tags: [
32 "provider:cloudflare",
33 "provider:cloudflare:mtlscertificate",
34 "live",
35 ],
36 },
37 () => {
38 test.provider("create and delete a CA certificate", (stack) =>
39 Effect.gen(function* () {
40 const { accountId } = yield* yield* CloudflareEnvironment;
41
42 yield* stack.destroy();
43
44 const cert = yield* stack.deploy(
45 Cloudflare.MtlsCertificate.MtlsCertificate("CaCert", {
46 ca: true,
47 certificates: CA_CERT_1,
48 }),
49 );
50
51 expect(cert.mtlsCertificateId).toBeDefined();
52 expect(cert.ca).toEqual(true);
53 expect(cert.name).toBeDefined();
54 expect(cert.expiresOn).toBeDefined();
55
56 const actual = yield* mtls.getMtlsCertificate({
57 accountId,
58 mtlsCertificateId: cert.mtlsCertificateId,
59 });
60 expect(actual.id).toEqual(cert.mtlsCertificateId);
61 expect(actual.ca).toEqual(true);
62 expect(actual.name).toEqual(cert.name);
63
64 yield* stack.destroy();
65
66 yield* waitForDelete(accountId, cert.mtlsCertificateId);
67 }).pipe(logLevel),
68 );
69
70 test.provider(
71 "create and delete a leaf certificate with private key",
72 (stack) =>
73 Effect.gen(function* () {
74 const { accountId } = yield* yield* CloudflareEnvironment;
75
76 yield* stack.destroy();
77
78 const cert = yield* stack.deploy(
79 Cloudflare.MtlsCertificate.MtlsCertificate("LeafCert", {
80 ca: false,
81 certificates: LEAF_CERT,
82 privateKey: Redacted.make(LEAF_KEY),
83 }),
84 );
85
86 expect(cert.mtlsCertificateId).toBeDefined();
87 expect(cert.ca).toEqual(false);
88
89 const actual = yield* mtls.getMtlsCertificate({
90 accountId,
91 mtlsCertificateId: cert.mtlsCertificateId,
92 });
93 expect(actual.id).toEqual(cert.mtlsCertificateId);
94 expect(actual.ca).toEqual(false);
95
96 yield* stack.destroy();
97
98 yield* waitForDelete(accountId, cert.mtlsCertificateId);
99 }).pipe(logLevel),
100 );
101
102 test.provider("replaces the certificate when the PEM changes", (stack) =>
103 Effect.gen(function* () {
104 const { accountId } = yield* yield* CloudflareEnvironment;
105
106 yield* stack.destroy();
107
108 const original = yield* stack.deploy(
109 Cloudflare.MtlsCertificate.MtlsCertificate("ReplaceCert", {
110 ca: true,
111 certificates: CA_CERT_1,
112 }),
113 );
114
115 const replaced = yield* stack.deploy(
116 Cloudflare.MtlsCertificate.MtlsCertificate("ReplaceCert", {
117 ca: true,
118 certificates: CA_CERT_2,
119 }),
120 );
121
122 expect(replaced.mtlsCertificateId).toBeDefined();
123 expect(replaced.mtlsCertificateId).not.toEqual(
124 original.mtlsCertificateId,
125 );
126 expect(replaced.serialNumber).not.toEqual(original.serialNumber);
127
128 // The old certificate must be gone after the replacement completes.
129 yield* waitForDelete(accountId, original.mtlsCertificateId);
130
131 const actual = yield* mtls.getMtlsCertificate({
132 accountId,
133 mtlsCertificateId: replaced.mtlsCertificateId,
134 });
135 expect(actual.id).toEqual(replaced.mtlsCertificateId);
136
137 yield* stack.destroy();
138
139 yield* waitForDelete(accountId, replaced.mtlsCertificateId);
140 }).pipe(logLevel),
141 );
142
143 test.provider("list enumerates the deployed mTLS certificate", (stack) =>
144 Effect.gen(function* () {
145 const { accountId } = yield* yield* CloudflareEnvironment;
146
147 yield* stack.destroy();
148
149 const cert = yield* stack.deploy(
150 Cloudflare.MtlsCertificate.MtlsCertificate("ListCert", {
151 ca: true,
152 certificates: CA_CERT_1,
153 }),
154 );
155
156 const provider = yield* Provider.findProvider(
157 Cloudflare.MtlsCertificate.MtlsCertificate,
158 );
159
160 // A freshly-deployed certificate is eventually consistent in the
161 // account-wide list(); poll until it appears before asserting.
162 const all = yield* poll({
163 description: "list() includes the deployed mTLS certificate",
164 effect: provider.list(),
165 predicate: (all) =>
166 all.some((c) => c.mtlsCertificateId === cert.mtlsCertificateId),
167 });
168
169 expect(
170 all.some((c) => c.mtlsCertificateId === cert.mtlsCertificateId),
171 ).toBe(true);
172
173 yield* stack.destroy();
174
175 yield* waitForDelete(accountId, cert.mtlsCertificateId);
176 }).pipe(logLevel),
177 );
178 },
179);
180
181const waitForDelete = (accountId: string, mtlsCertificateId: string) =>
182 mtls.getMtlsCertificate({ accountId, mtlsCertificateId }).pipe(
183 Effect.flatMap((cert) =>
184 cert.id === mtlsCertificateId
185 ? Effect.fail({ _tag: "CertificateNotDeleted" } as const)
186 : Effect.void,
187 ),
188 Effect.catchTag("CertificateNotFound", () => Effect.void),
189 Effect.retry({
190 while: (e) => e._tag === "CertificateNotDeleted",
191 schedule: Schedule.max([
192 Schedule.exponential("500 millis"),
193 Schedule.recurs(10),
194 ]),
195 }),
196 );