1import * as AWS from "@/AWS";
2import { KeyGroup, PublicKey } from "@/AWS/CloudFront";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as cloudfront from "@distilled.cloud/aws/cloudfront";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12const PRIMARY_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
13MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTkfqkMHU8HMmIRKJaMl
14IoD691g60aS15QlaP/DVkpuoeEp8JA8YDs5vQFu6HSIYCTQ7WwFx9oRvN08i7yXB
15EHt3x7uZVpdkp6JBbjR9BGNsAVri6DZ0TJQ11zWeN3keqhnUdFhQjPwT+u4r6oKk
16kNvkl7eU2nFK+UIaPlD+rA+AlYT0m7gSVcd9KaLf/UzBrtSy1dbXYDT4dHChMUVy
174gDsQ6D4u6lRAHY9jcKxlgEIM+O8ODKyzlbergv2EwhANG4P27DBeDhA/off3upM
18TTVTGKZeoABtqM0ZiYq0cDgf8KUn9NPxSdnJ4+cbigLjJBPS93VYWzWX0HXlZpQ3
19HQIDAQAB
20-----END PUBLIC KEY-----
21`;
22
23const SECONDARY_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
24MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6QT6PtJ4O4Xcy6C8BkI
25NHDRF8TnPbWZlVFYXr4R9miLMlORiH34sUtyYAwYlvHyYlrL76Z4A8HaLTMDfCf5
26eswQr0r7qVOstRJrEW8ycTXJw/8Mj+1KKSrs8QopVZBMEnOfiUsentnWhQgKSpbI
27mA4sLTg45jBoaZi9c8M40psgAp/1vPn5zeloAjoku5ax18FM32yRi9C21J84BWCd
28vp6E7NtzJh9XlH22xOPGmbCh1r3uAtiujpXj1UNw/QEMooiEsJAeoROpvHZkh6/m
29VZUU1njrXaTMl4OUd+I+Y96eWWo539vU0dD5XnUUNuYybwI6FeVmrouHR9wG4RJL
30JQIDAQAB
31-----END PUBLIC KEY-----
32`;
33
34describe(
35 "AWS.CloudFront.KeyGroup",
36 { tags: ["provider:aws", "provider:aws:cloudfront", "live"] },
37 () => {
38 test.provider(
39 "create, update items, and delete a key group",
40 (stack) =>
41 Effect.gen(function* () {
42 yield* stack.destroy();
43
44 const created = yield* stack.deploy(
45 Effect.gen(function* () {
46 const primary = yield* PublicKey("PrimarySigningKey", {
47 encodedKey: PRIMARY_PUBLIC_KEY,
48 comment: "primary",
49 });
50 const secondary = yield* PublicKey("SecondarySigningKey", {
51 encodedKey: SECONDARY_PUBLIC_KEY,
52 comment: "secondary",
53 });
54 const group = yield* KeyGroup("SignedUrlKeys", {
55 comment: "initial",
56 items: [primary.publicKeyId],
57 });
58 return { primary, secondary, group };
59 }),
60 );
61
62 const initial = yield* cloudfront.getKeyGroup({
63 Id: created.group.keyGroupId,
64 });
65 expect(initial.KeyGroup?.Id).toEqual(created.group.keyGroupId);
66 expect(initial.KeyGroup?.KeyGroupConfig?.Comment).toEqual("initial");
67 expect(initial.KeyGroup?.KeyGroupConfig?.Items).toEqual([
68 created.primary.publicKeyId,
69 ]);
70
71 const updated = yield* stack.deploy(
72 Effect.gen(function* () {
73 const primary = yield* PublicKey("PrimarySigningKey", {
74 encodedKey: PRIMARY_PUBLIC_KEY,
75 comment: "primary",
76 });
77 const secondary = yield* PublicKey("SecondarySigningKey", {
78 encodedKey: SECONDARY_PUBLIC_KEY,
79 comment: "secondary",
80 });
81 const group = yield* KeyGroup("SignedUrlKeys", {
82 comment: "updated",
83 items: [primary.publicKeyId, secondary.publicKeyId],
84 });
85 return { primary, secondary, group };
86 }),
87 );
88
89 expect(updated.group.keyGroupId).toEqual(created.group.keyGroupId);
90
91 // `getKeyGroup` right after `updateKeyGroup` can serve the
92 // pre-update config (control-plane reads are eventually
93 // consistent) — poll until the update is visible, then assert.
94 const after = yield* cloudfront
95 .getKeyGroup({ Id: updated.group.keyGroupId })
96 .pipe(
97 Effect.repeat({
98 schedule: Schedule.fixed("2 seconds"),
99 until: (response) =>
100 response.KeyGroup?.KeyGroupConfig?.Items?.length === 2,
101 times: 15,
102 }),
103 );
104 expect(after.KeyGroup?.KeyGroupConfig?.Comment).toEqual("updated");
105 // CloudFront does not preserve the order of key-group items —
106 // compare as sets.
107 expect(
108 [...(after.KeyGroup?.KeyGroupConfig?.Items ?? [])].sort(),
109 ).toEqual(
110 [updated.primary.publicKeyId, updated.secondary.publicKeyId].sort(),
111 );
112
113 yield* stack.destroy();
114 yield* assertKeyGroupDeleted(updated.group.keyGroupId);
115 yield* assertPublicKeyDeleted(updated.primary.publicKeyId);
116 yield* assertPublicKeyDeleted(updated.secondary.publicKeyId);
117 }),
118 { timeout: 300_000 },
119 );
120
121 test.provider(
122 "list enumerates the deployed key group",
123 (stack) =>
124 Effect.gen(function* () {
125 yield* stack.destroy();
126
127 const deployed = yield* stack.deploy(
128 Effect.gen(function* () {
129 const primary = yield* PublicKey("PrimarySigningKey", {
130 encodedKey: PRIMARY_PUBLIC_KEY,
131 comment: "primary",
132 });
133 const group = yield* KeyGroup("ListKeyGroup", {
134 comment: "list",
135 items: [primary.publicKeyId],
136 });
137 return { primary, group };
138 }),
139 );
140
141 const provider = yield* Provider.findProvider(KeyGroup);
142 const all = yield* provider.list();
143
144 expect(
145 all.some((g) => g.keyGroupId === deployed.group.keyGroupId),
146 ).toBe(true);
147
148 yield* stack.destroy();
149 yield* assertKeyGroupDeleted(deployed.group.keyGroupId);
150 yield* assertPublicKeyDeleted(deployed.primary.publicKeyId);
151 }),
152 { timeout: 300_000 },
153 );
154 },
155);
156
157const assertKeyGroupDeleted = (id: string) =>
158 cloudfront.getKeyGroup({ Id: id }).pipe(
159 Effect.flatMap(() => Effect.fail(new Error("KeyGroupStillExists"))),
160 Effect.catchTag("NoSuchResource", () => Effect.void),
161 Effect.retry({
162 while: (error) =>
163 error instanceof Error && error.message === "KeyGroupStillExists",
164 schedule: Schedule.max([
165 Schedule.fixed("5 seconds"),
166 Schedule.recurs(24),
167 ]),
168 }),
169 );
170
171const assertPublicKeyDeleted = (id: string) =>
172 cloudfront.getPublicKey({ Id: id }).pipe(
173 Effect.flatMap(() => Effect.fail(new Error("PublicKeyStillExists"))),
174 Effect.catchTag("NoSuchPublicKey", () => Effect.void),
175 Effect.retry({
176 while: (error) =>
177 error instanceof Error && error.message === "PublicKeyStillExists",
178 schedule: Schedule.max([
179 Schedule.fixed("2 seconds"),
180 Schedule.recurs(15),
181 ]),
182 }),
183 );