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 certificateIdOf = (name: string) => name.split("/").pop() ?? name;
68
69const hasCertificate = (
70 names: readonly string[] | undefined,
71 certName: string,
72) =>
73 (names ?? []).some(
74 (name) => certificateIdOf(name) === certificateIdOf(certName),
75 );
76
77const waitUntilGone = (name: string) =>
78 certificatemanager
79 .getProjectsLocationsCertificateMapsCertificateMapEntries({ name })
80 .pipe(
81 Effect.as("found" as const),
82 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
83 Effect.repeat({
84 schedule: Schedule.spaced("1 second"),
85 until: (status) => status === "gone",
86 times: 10,
87 }),
88 );
89
90test.provider(
91 "create, update, and delete a certificate map entry",
92 (stack) =>
93 Effect.gen(function* () {
94 yield* stack.destroy();
95
96 const created = yield* stack.deploy(
97 Effect.gen(function* () {
98 const map = yield* GCP.CertificateManager.CertificateMap(
99 "FrontendMap",
100 {
101 location: "global",
102 description: "frontend map for entries",
103 },
104 );
105 const certA = yield* GCP.CertificateManager.Certificate("TlsA", {
106 location: "global",
107 pemCertificate: CERT_A,
108 pemPrivateKey: KEY_A,
109 });
110 const entry = yield* GCP.CertificateManager.CertificateMapEntry(
111 "Www",
112 {
113 certificateMap: map.name,
114 description: "entry a",
115 labels: { env: "test" },
116 certificates: [certA.name],
117 hostname: "alchemy-ssl-a.test",
118 },
119 );
120 return { map, certA, entry };
121 }),
122 );
123
124 expect(created.entry.name).toContain("/certificateMapEntries/");
125 expect(created.entry.certificateMapEntryId).toEqual(expect.any(String));
126 expect(created.entry.certificateMap).toEqual(created.map.name);
127 expect(created.entry.location).toEqual("global");
128 expect(created.entry.description).toEqual("entry a");
129 expect(created.entry.labels).toMatchObject({ env: "test" });
130 expect(created.entry.hostname).toEqual("alchemy-ssl-a.test");
131 expect(created.entry.certificates).toContain(created.certA.name);
132 expect(created.entry.createTime).toEqual(expect.any(String));
133
134 const fetched =
135 yield* certificatemanager.getProjectsLocationsCertificateMapsCertificateMapEntries(
136 {
137 name: created.entry.name,
138 },
139 );
140 expect(fetched.name).toEqual(created.entry.name);
141 expect(fetched.hostname).toEqual("alchemy-ssl-a.test");
142 expect(fetched.description).toEqual("entry a");
143 expect(fetched.labels?.env).toEqual("test");
144 expect(hasCertificate(fetched.certificates, created.certA.name)).toEqual(
145 true,
146 );
147 expect(
148 Object.keys(fetched.labels ?? {}).some((key) =>
149 key.startsWith("alchemy-"),
150 ),
151 ).toEqual(true);
152
153 const updated = yield* stack.deploy(
154 Effect.gen(function* () {
155 const map = yield* GCP.CertificateManager.CertificateMap(
156 "FrontendMap",
157 {
158 certificateMapId: created.map.certificateMapId,
159 location: "global",
160 description: "frontend map for entries",
161 },
162 );
163 const certA = yield* GCP.CertificateManager.Certificate("TlsA", {
164 certificateId: created.certA.certificateId,
165 location: "global",
166 pemCertificate: CERT_A,
167 pemPrivateKey: KEY_A,
168 });
169 const entry = yield* GCP.CertificateManager.CertificateMapEntry(
170 "Www",
171 {
172 certificateMap: map.name,
173 certificateMapEntryId: created.entry.certificateMapEntryId,
174 description: "entry b",
175 labels: { env: "prod", role: "sni" },
176 certificates: [certA.name],
177 hostname: "alchemy-ssl-a.test",
178 },
179 );
180 return { map, certA, entry };
181 }),
182 );
183
184 expect(updated.entry.name).toEqual(created.entry.name);
185 expect(updated.entry.description).toEqual("entry b");
186 expect(updated.entry.labels).toMatchObject({ env: "prod", role: "sni" });
187 expect(updated.entry.hostname).toEqual("alchemy-ssl-a.test");
188 expect(updated.entry.certificates).toContain(created.certA.name);
189
190 const refetched =
191 yield* certificatemanager.getProjectsLocationsCertificateMapsCertificateMapEntries(
192 {
193 name: created.entry.name,
194 },
195 );
196 expect(refetched.description).toEqual("entry b");
197 expect(refetched.labels?.env).toEqual("prod");
198 expect(refetched.labels?.role).toEqual("sni");
199 expect(refetched.hostname).toEqual("alchemy-ssl-a.test");
200 expect(
201 hasCertificate(refetched.certificates, created.certA.name),
202 ).toEqual(true);
203
204 yield* stack.destroy();
205
206 const gone = yield* waitUntilGone(created.entry.name);
207 expect(gone).toEqual("gone");
208 }).pipe(logLevel),
209 {
210 tags: ["provider:gcp", "provider:gcp:certificatemanager", "live"],
211 timeout: 120_000,
212 },
213);