| 1 | import * as AWS from "@/AWS"; |
| 2 | import { KeyGroup, PublicKey } from "@/AWS/CloudFront"; |
| 3 | import * as Provider from "@/Provider"; |
| 4 | import * as Test from "@/Test/Alchemy"; |
| 5 | import * as cloudfront from "@distilled.cloud/aws/cloudfront"; |
| 6 | import { describe, expect } from "alchemy-test"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Schedule from "effect/Schedule"; |
| 9 | |
| 10 | const { test } = Test.make({ providers: AWS.providers() }); |
| 11 | |
| 12 | const PRIMARY_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- |
| 13 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTkfqkMHU8HMmIRKJaMl |
| 14 | IoD691g60aS15QlaP/DVkpuoeEp8JA8YDs5vQFu6HSIYCTQ7WwFx9oRvN08i7yXB |
| 15 | EHt3x7uZVpdkp6JBbjR9BGNsAVri6DZ0TJQ11zWeN3keqhnUdFhQjPwT+u4r6oKk |
| 16 | kNvkl7eU2nFK+UIaPlD+rA+AlYT0m7gSVcd9KaLf/UzBrtSy1dbXYDT4dHChMUVy |
| 17 | 4gDsQ6D4u6lRAHY9jcKxlgEIM+O8ODKyzlbergv2EwhANG4P27DBeDhA/off3upM |
| 18 | TTVTGKZeoABtqM0ZiYq0cDgf8KUn9NPxSdnJ4+cbigLjJBPS93VYWzWX0HXlZpQ3 |
| 19 | HQIDAQAB |
| 20 | -----END PUBLIC KEY----- |
| 21 | `; |
| 22 | |
| 23 | const SECONDARY_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- |
| 24 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6QT6PtJ4O4Xcy6C8BkI |
| 25 | NHDRF8TnPbWZlVFYXr4R9miLMlORiH34sUtyYAwYlvHyYlrL76Z4A8HaLTMDfCf5 |
| 26 | eswQr0r7qVOstRJrEW8ycTXJw/8Mj+1KKSrs8QopVZBMEnOfiUsentnWhQgKSpbI |
| 27 | mA4sLTg45jBoaZi9c8M40psgAp/1vPn5zeloAjoku5ax18FM32yRi9C21J84BWCd |
| 28 | vp6E7NtzJh9XlH22xOPGmbCh1r3uAtiujpXj1UNw/QEMooiEsJAeoROpvHZkh6/m |
| 29 | VZUU1njrXaTMl4OUd+I+Y96eWWo539vU0dD5XnUUNuYybwI6FeVmrouHR9wG4RJL |
| 30 | JQIDAQAB |
| 31 | -----END PUBLIC KEY----- |
| 32 | `; |
| 33 | |
| 34 | describe( |
| 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 | |
| 157 | const 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 | |
| 171 | const 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 | ); |