1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as certificatemanager from "@distilled.cloud/gcp/certificatemanager_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16// Self-signed RSA-2048 fixtures generated once with openssl (not at test time).
17const CERT_A = `-----BEGIN CERTIFICATE-----
18MIIDOjCCAiKgAwIBAgIUY07uz+vKYeBK3NYMhk03eWyTPKkwDQYJKoZIhvcNAQEL
19BQAwHTEbMBkGA1UEAwwSYWxjaGVteS1zc2wtYS50ZXN0MB4XDTI2MDgyNDE1NDM0
20NFoXDTM2MDgyMTE1NDM0NFowHTEbMBkGA1UEAwwSYWxjaGVteS1zc2wtYS50ZXN0
21MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUAYVVO5iOluteYkxOqA
22bBF+EnWrtfsUMoPJPXBEIrmT66G+vx+rArDsRxJqnWr9YstEQKsHtkErGF37avZr
2312jTIiKWk9auxyhN8O6WD2jND13m+IfsdeWKSIFx3j/RJ0McJQYu4YuN8ViZm3t9
24o3f5N+4DczgqzloV7U6/DFeMZ23nwBD9IgrUf6KyGoC/sJ4twTjpxSECekiOHgyn
25/M7SwrBTJYJTsMXzqYBPVjk7f17+7LAwNqBNzQx6HsGV8sZX4kDlcMAL53obLV1z
26kkJHhP3suaWCUl5uarZ9r5vc7DC2j6vvtghFsDvRWqqram3fCEOJOQmoAVlChbtq
27SwIDAQABo3IwcDAdBgNVHQ4EFgQUEicpezwWkZY3JMEwx1D0eraiFCEwHwYDVR0j
28BBgwFoAUEicpezwWkZY3JMEwx1D0eraiFCEwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
29HREEFjAUghJhbGNoZW15LXNzbC1hLnRlc3QwDQYJKoZIhvcNAQELBQADggEBAMRj
30u6+KYAYwX/cuJruXaX7Pa9kX4Es+637dDPWBP9lLedVOGEpOujiDn2JUuNZNl6vo
31eqaJrjQuJkSgVVYikz3dGWTEvqlDIJ5nbRIo8etW1d5eoZeWcd95t2VXNRlfULBS
32eNOFYOI6iaPW2CQXFvNAgER/Azj8Y4ilky504m0bqkZWTl/2f+x1KaiBqkxqiOdX
33n5gPpu+jujVOyJni7KJo4+iIdWtQI1xAazXyOFefTMkPGLkXeQ/+tXrmY9NoVvNO
34JBEe8o+wi/LxH/K9/It1rCfWckb5zHU57li4ZxetlwYssVW4Of25sMp9c4JNF3gJ
35mkhcNRA0cRkbnom+kn8=
36-----END CERTIFICATE-----`;
37
38const KEY_A = `-----BEGIN PRIVATE KEY-----
39MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNQBhVU7mI6W61
405iTE6oBsEX4Sdau1+xQyg8k9cEQiuZProb6/H6sCsOxHEmqdav1iy0RAqwe2QSsY
41Xftq9mvXaNMiIpaT1q7HKE3w7pYPaM0PXeb4h+x15YpIgXHeP9EnQxwlBi7hi43x
42WJmbe32jd/k37gNzOCrOWhXtTr8MV4xnbefAEP0iCtR/orIagL+wni3BOOnFIQJ6
43SI4eDKf8ztLCsFMlglOwxfOpgE9WOTt/Xv7ssDA2oE3NDHoewZXyxlfiQOVwwAvn
44ehstXXOSQkeE/ey5pYJSXm5qtn2vm9zsMLaPq++2CEWwO9Faqqtqbd8IQ4k5CagB
45WUKFu2pLAgMBAAECggEASEceaonZx69gV7jsUNW9lJDSYyDB74sz0RgceUC8Fbhh
46MTSbrcUynPd9tQd0uOuQwEYRSm6QACvRx7psy31se4ZD93zTpssOcD6ut73k5RdE
47QvmP2QxQhjHncOH4ncm+VwOoeRdE0hMpUIihSyIBG9wnTH1KBLyOQm1x1EgLOgZH
48DPtZPUg4j/KeMF5HbI0ZycSeMtWmwhYfsHrgFH/lIm/6TKaEJ0SNlFcW4GzMlxi3
49KEeoBw5RhKwY+Hs+zEy78udGqe+5aahSqk7IaO1UlDEmBFoNksuC6ZyEXITPJKbV
50BU8rH6jI/xdHqdOaWBI/rLYxgm2IMrqANR3B0hF78QKBgQDnpFmpyfoj2w7nxhlg
51d+9RuXC55Q0UMXPPIZ3b4lmNqmPcgkC55pu64D6snxj3HBeWvZ8a8E7WifW6fo0V
5268wpzBfMIiE7h0YWeNpfhCZnz61xOblshTpKy+QHIl5iBmrQUUfMuMjJKmIynjyI
53WRT/d+OzzWm2eDsNoajpT4gFcQKBgQDi1Uy76+xggrLvHYfhEFyz6UYn9xVe7Chl
54ru1vHQjTIx+V0pDmlHpKbK0+tyspZTeWYYMwRzqol7u76mmXZXZ7WK3aFA6Q8aYl
554KO6wS70Lryt5oB5QFvpkiSMFl1bzHGvCxCTX+uieR796Pfms6zHwzK0/ibe8lEJ
56KNplRXMdewKBgQC9aHG4l+LldrWVZzJQ40DY/lziZBxxqo4bjE1cApVfdTf6krcC
57S0KDZ+FXnS/4vwu6wopaqKyOWHiJaflLN2fVtYCv9iheWJpCvccx2wjcUcBsmNq5
58laa4ikeGXd/3H3AvroabK21isDljUmgExXKaAho6Z3hNL7p5xvoq7FE4wQKBgQC5
59bXCK9nOG+ZDYk6VuQHfnwrxNE1ju/dKQPQ1vlaaPItlBGp7FP38ws+JzsDyiXFGy
60pwgdQT0ccN1Q4nFrB9BxSK7l5Rt7NW+C6z4s/psplcM7zYAcnpYEPCmQMwAieOA+
61HadxMipn6OeC3R06BIsryc/70P9ppWDFQhY2Ty2pXQKBgCPjmTUuOwE1J0aegpb3
62eBZPBH701wZufH75uMVB5PApxO1PKMyRqeOMhbLGxws6jgf2YOPqmB9zSjQWEeQ/
63ME7uU5bUhTH24H7jqxo3U7wWVRWgy2TvR4gfcfI9QKVXSzDBauq1DiW86j6edI+2
64fygch3+OZKV+N0l93MQpSWv4
65-----END PRIVATE KEY-----`;
66
67const CERT_B = `-----BEGIN CERTIFICATE-----
68MIIDOjCCAiKgAwIBAgIUW1TNbunaoG4/NzJZSThMKRTZuw4wDQYJKoZIhvcNAQEL
69BQAwHTEbMBkGA1UEAwwSYWxjaGVteS1zc2wtYi50ZXN0MB4XDTI2MDgyNDE1NDM0
70NFoXDTM2MDgyMTE1NDM0NFowHTEbMBkGA1UEAwwSYWxjaGVteS1zc2wtYi50ZXN0
71MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA77IIHhQKx9IOuqN9lUwn
72oqPG+LeafxTE9cKaQsO21FgjlsSvHlZqGdtdojevDYucWYgWwtOpj5FASpMGW5Kg
732PcZk8dgpXT6byYIicDA8jJVx93sFBxn2BhYvvCfWRbqPSo3SoF3DCS6AQiZX8zl
74u+Cs63Tfo2OCbCqlPylm2DGhNfs1KI22lLrU3ay0zHjeUtiK4+CQVij8He+e48mH
75MwwWtWXKP/eikQlnV4c6OYA6rLLXBvKy3g1zavBAEY5GLECP/xt1bj2qgTUge8L4
76Xr45pBj3l6Ku1thDefS9mXcHt456VeEkWir0O7cnBN903BbzhNV7vTHVfS5I8ZTg
77jQIDAQABo3IwcDAdBgNVHQ4EFgQU07mM+3JilvETyfGSF5I6Al+B8gIwHwYDVR0j
78BBgwFoAU07mM+3JilvETyfGSF5I6Al+B8gIwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
79HREEFjAUghJhbGNoZW15LXNzbC1iLnRlc3QwDQYJKoZIhvcNAQELBQADggEBAOFT
80KwWAxvo2/g+ErYxKT7Tl3ObBmUGC93T2KX38bw2jq7+7dgTJnz2kN6V5PRaq1iXj
81lmCF++dJJpCPdSiBhYNkoUIk7st0n9Mz/3oJBzass3wFY0dHw7r895rTfjkRF+F5
82Fw9dTXzy0a0Gni4I6jEWflEVXAFb0q6RJ6zHdGiriJsXvXKC8eVBJqRCy+NbufpL
83efSfgplQfXSteOlOnRP9iWsK3AH1TVw6RwLaT3iVWSbWjICf3f+8xJ65j/vNjbnY
842F2+hBmKEXfRv5liZIhV1Q9gxbpjvzw2OzaoQxkZYNbaU96yy5TsG1jGd0wqX/vT
85MbTsvjb7gbJeTqYGQeA=
86-----END CERTIFICATE-----`;
87
88const KEY_B = `-----BEGIN PRIVATE KEY-----
89MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDvsggeFArH0g66
90o32VTCeio8b4t5p/FMT1wppCw7bUWCOWxK8eVmoZ212iN68Ni5xZiBbC06mPkUBK
91kwZbkqDY9xmTx2CldPpvJgiJwMDyMlXH3ewUHGfYGFi+8J9ZFuo9KjdKgXcMJLoB
92CJlfzOW74KzrdN+jY4JsKqU/KWbYMaE1+zUojbaUutTdrLTMeN5S2Irj4JBWKPwd
93757jyYczDBa1Zco/96KRCWdXhzo5gDqsstcG8rLeDXNq8EARjkYsQI//G3VuPaqB
94NSB7wvhevjmkGPeXoq7W2EN59L2Zdwe3jnpV4SRaKvQ7tycE33TcFvOE1Xu9MdV9
95LkjxlOCNAgMBAAECggEALtTNYjcPPihL6WpM/JEDA3NVSj3xKRghPVUWt6mufIF1
96/pySDG7czw19l1ai1JYs548/xzP8oojCE2/zY4vb8lgg/q8LMniO/41iSKvI5ukv
97uluhJvK/1Ug4eqHfuFPSUz3+zIdL/6rjTpVkBgQt9I26q0xL74vfLQwN+i+4IM/8
98v9C8SMtjxcDkrJWzuSpL4aeL8ld1d4Mja5o4GcL8LLx3zVBuJELD/36v+jWVu5kF
996G4KFlTIiK8xdPlvLpk/g0j1rrr+4h5XHlgns/W1VYrWhw5XMsMmHOPEcb+M+pKa
1001h0lQHMdaq8SL/WSa00Tg2YeLBUb37F4NsEcU6iDAQKBgQD33kKNRpG9qe4RdRkb
101PkpedauSG+YEi74CRdkbPm9/JIx2Lk5+sp7/DvJkqok5Fzg12bBTM86xDg193RWk
102RHgCTQcKUhuwGzlEtXwOYKa++UiE6hQmopU7Ge6C8GbB/vIcfBUWYDF67ayycNOS
103f6CTqKqV9Rqrt9pGnVv3PjocgQKBgQD3jyHTqrCyjI9GwLVDB0nmHnbS5Y9oVW/Y
104eJQn8EN9nGLENaKOmtA4s1Qwgv7nv8pz76abXnv4Bs5YBCJwshPvB/iOP7vkc3im
10561D8gt2UkbsUhmtqQuNjrszqYd5hSQ5h4uAtUSgctRFjSkdtb9AxO7UwPk2Ae5f1
106EurHZ2JuDQKBgQCnLzbca3jp4LA+/iiNA5rN07iBuGeRMWBb0Bj8E0TJETHclKdF
107Nd2gU8iStaQQ6eR0DfysYglLyxq2hmSOg9AeXS4ee/tI8VPTUEX4vKA7t5B+dhRZ
108atgVIQyLLPNibj+Hjvn6Oa+mJqGRSiCqc4MvlVhaBRSUNlH3xzO8tDAmAQKBgQCI
1099uzNMfvO3UuL5qSisA6nvaUaK537KIHX72Zw7lI41eQjEFWetnKLXFQw/tjPGWKG
110YEn6Xf0SjZluQuNXHH9A2VUgRozK6UQQfdLx0emMAFDUG0akpNsv1I9VAc7KXmQR
111rjc/e05JC7jSCU+ZcaprAtDNHzs74aWCFvREXKOtpQKBgQCrVxSthxuWGy96GpXV
112yrNQTQ25BaINmZFdvwF6r5Go5jMwBKV5x4oq8zioH7CXH8yAHIGOAYW5zjQ4Oebb
113uCSz3dimQ+PlEDhAWYsnJbwpJvWfKLk4WaePLqkKzQ5nNGnPXqltwMDaPZzgDVsm
1146oSOe7OlpzcSQhEUgsBvtSzTwg==
115-----END PRIVATE KEY-----`;
116
117const waitUntilGone = (name: string) =>
118 certificatemanager.getProjectsLocationsCertificates({ name }).pipe(
119 Effect.as("found" as const),
120 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
121 Effect.repeat({
122 schedule: Schedule.spaced("1 second"),
123 until: (status) => status === "gone",
124 times: 10,
125 }),
126 );
127
128test.provider(
129 "create, update, and delete a certificate",
130 (stack) =>
131 Effect.gen(function* () {
132 yield* stack.destroy();
133
134 const created = yield* stack.deploy(
135 Effect.gen(function* () {
136 return yield* GCP.CertificateManager.Certificate("FrontendTls", {
137 location: "global",
138 description: "frontend tls a",
139 labels: { env: "test" },
140 pemCertificate: CERT_A,
141 pemPrivateKey: KEY_A,
142 });
143 }),
144 );
145
146 expect(created.name).toContain("/certificates/");
147 expect(created.certificateId).toEqual(expect.any(String));
148 expect(created.location).toEqual("global");
149 expect(created.type).toEqual("SELF_MANAGED");
150 expect(created.scope).toEqual("DEFAULT");
151 expect(created.description).toEqual("frontend tls a");
152 expect(created.labels).toMatchObject({ env: "test" });
153 expect(created.pemCertificate).toEqual(expect.any(String));
154 expect(created.sanDnsnames).toContain("alchemy-ssl-a.test");
155 expect(created.expireTime).toEqual(expect.any(String));
156
157 const fetched =
158 yield* certificatemanager.getProjectsLocationsCertificates({
159 name: created.name,
160 });
161 expect(fetched.name).toEqual(created.name);
162 expect(fetched.labels?.env).toEqual("test");
163 expect(fetched.description).toEqual("frontend tls a");
164 expect(fetched.pemCertificate).toContain("BEGIN CERTIFICATE");
165 expect(fetched.sanDnsnames).toContain("alchemy-ssl-a.test");
166 expect(
167 Object.keys(fetched.labels ?? {}).some((key) =>
168 key.startsWith("alchemy-"),
169 ),
170 ).toEqual(true);
171
172 const updated = yield* stack.deploy(
173 Effect.gen(function* () {
174 return yield* GCP.CertificateManager.Certificate("FrontendTls", {
175 certificateId: created.certificateId,
176 location: "global",
177 description: "frontend tls b",
178 labels: { env: "prod", role: "tls" },
179 pemCertificate: CERT_A,
180 pemPrivateKey: KEY_A,
181 });
182 }),
183 );
184
185 expect(updated.name).toEqual(created.name);
186 expect(updated.description).toEqual("frontend tls b");
187 expect(updated.labels).toMatchObject({ env: "prod", role: "tls" });
188 expect(updated.sanDnsnames).toContain("alchemy-ssl-a.test");
189
190 const refetched =
191 yield* certificatemanager.getProjectsLocationsCertificates({
192 name: created.name,
193 });
194 expect(refetched.description).toEqual("frontend tls b");
195 expect(refetched.labels?.env).toEqual("prod");
196 expect(refetched.labels?.role).toEqual("tls");
197 expect(refetched.sanDnsnames).toContain("alchemy-ssl-a.test");
198
199 const replaced = yield* stack.deploy(
200 Effect.gen(function* () {
201 return yield* GCP.CertificateManager.Certificate("FrontendTls", {
202 certificateId: created.certificateId,
203 location: "global",
204 description: "frontend tls c",
205 labels: { env: "prod", role: "tls" },
206 pemCertificate: CERT_B,
207 pemPrivateKey: KEY_B,
208 });
209 }),
210 );
211
212 expect(replaced.certificateId).toEqual(created.certificateId);
213 expect(replaced.description).toEqual("frontend tls c");
214 expect(replaced.sanDnsnames).toContain("alchemy-ssl-b.test");
215 expect(replaced.sanDnsnames).not.toContain("alchemy-ssl-a.test");
216
217 const replacedFetched =
218 yield* certificatemanager.getProjectsLocationsCertificates({
219 name: created.name,
220 });
221 expect(replacedFetched.sanDnsnames).toContain("alchemy-ssl-b.test");
222 expect(replacedFetched.description).toEqual("frontend tls c");
223
224 yield* stack.destroy();
225
226 const gone = yield* waitUntilGone(created.name);
227 expect(gone).toEqual("gone");
228 }).pipe(logLevel),
229 {
230 tags: ["provider:gcp", "provider:gcp:certificatemanager", "live"],
231 timeout: 90_000,
232 },
233);