1import * as AWS from "@/AWS";
2import {
3 CachePolicy,
4 Distribution,
5 KeyGroup,
6 OriginAccessControl,
7 OriginRequestPolicy,
8 PublicKey,
9 ResponseHeadersPolicy,
10} from "@/AWS/CloudFront";
11import type { PolicyStatement } from "@/AWS/IAM/Policy";
12import { Bucket } from "@/AWS/S3";
13import * as Output from "@/Output";
14import * as Test from "@/Test/Alchemy";
15import * as cloudfront from "@distilled.cloud/aws/cloudfront";
16import { describe, expect } from "alchemy-test";
17import * as Effect from "effect/Effect";
18import * as Schedule from "effect/Schedule";
19
20const { test } = Test.make({ providers: AWS.providers() });
21
22const runLive = !process.env.FAST;
23
24const SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
25MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTkfqkMHU8HMmIRKJaMl
26IoD691g60aS15QlaP/DVkpuoeEp8JA8YDs5vQFu6HSIYCTQ7WwFx9oRvN08i7yXB
27EHt3x7uZVpdkp6JBbjR9BGNsAVri6DZ0TJQ11zWeN3keqhnUdFhQjPwT+u4r6oKk
28kNvkl7eU2nFK+UIaPlD+rA+AlYT0m7gSVcd9KaLf/UzBrtSy1dbXYDT4dHChMUVy
294gDsQ6D4u6lRAHY9jcKxlgEIM+O8ODKyzlbergv2EwhANG4P27DBeDhA/off3upM
30TTVTGKZeoABtqM0ZiYq0cDgf8KUn9NPxSdnJ4+cbigLjJBPS93VYWzWX0HXlZpQ3
31HQIDAQAB
32-----END PUBLIC KEY-----
33`;
34
35describe(
36 "AWS.CloudFront smoke",
37 {
38 tags: [
39 "provider:aws",
40 "provider:aws:cloudfront",
41 "provider:aws:iam",
42 "provider:aws:s3",
43 "live",
44 ],
45 },
46 () => {
47 test.provider.skipIf(!runLive)(
48 "deploy distribution wired to cache, origin request, and response headers policies alongside a key group",
49 (stack) =>
50 Effect.gen(function* () {
51 yield* stack.destroy();
52
53 const deployed = yield* stack.deploy(
54 Effect.gen(function* () {
55 const bucket = yield* Bucket("SmokeBucket", {
56 forceDestroy: true,
57 });
58 const oac = yield* OriginAccessControl("SmokeOac", {
59 originType: "s3",
60 });
61
62 const cachePolicy = yield* CachePolicy("SmokeCachePolicy", {
63 comment: "smoke",
64 minTTL: 0,
65 defaultTTL: "60 seconds",
66 maxTTL: "1 day",
67 parametersInCacheKeyAndForwardedToOrigin: {
68 EnableAcceptEncodingGzip: true,
69 EnableAcceptEncodingBrotli: true,
70 HeadersConfig: { HeaderBehavior: "none" },
71 CookiesConfig: { CookieBehavior: "none" },
72 QueryStringsConfig: { QueryStringBehavior: "none" },
73 },
74 });
75
76 const originRequestPolicy = yield* OriginRequestPolicy(
77 "SmokeOriginRequestPolicy",
78 {
79 comment: "smoke",
80 headersConfig: { HeaderBehavior: "none" },
81 cookiesConfig: { CookieBehavior: "none" },
82 queryStringsConfig: { QueryStringBehavior: "all" },
83 },
84 );
85
86 const responseHeadersPolicy = yield* ResponseHeadersPolicy(
87 "SmokeResponseHeadersPolicy",
88 {
89 comment: "smoke",
90 securityHeadersConfig: {
91 StrictTransportSecurity: {
92 AccessControlMaxAgeSec: 31536000,
93 IncludeSubdomains: true,
94 Preload: true,
95 Override: true,
96 },
97 ContentTypeOptions: { Override: true },
98 FrameOptions: { FrameOption: "DENY", Override: true },
99 },
100 },
101 );
102
103 const publicKey = yield* PublicKey("SmokeSigningKey", {
104 encodedKey: SIGNING_PUBLIC_KEY,
105 comment: "smoke signed-url key",
106 });
107
108 const keyGroup = yield* KeyGroup("SmokeSigningKeys", {
109 comment: "smoke signed-url group",
110 items: [publicKey.publicKeyId],
111 });
112
113 const distribution = yield* Distribution("SmokeDistribution", {
114 origins: [
115 {
116 id: "site",
117 domainName: bucket.bucketRegionalDomainName,
118 s3Origin: true,
119 originAccessControlId: oac.originAccessControlId,
120 },
121 ],
122 defaultRootObject: "index.html",
123 defaultCacheBehavior: {
124 targetOriginId: "site",
125 viewerProtocolPolicy: "redirect-to-https",
126 compress: true,
127 allowedMethods: ["GET", "HEAD"],
128 cachedMethods: ["GET", "HEAD"],
129 cachePolicyId: cachePolicy.cachePolicyId,
130 originRequestPolicyId:
131 originRequestPolicy.originRequestPolicyId,
132 responseHeadersPolicyId:
133 responseHeadersPolicy.responseHeadersPolicyId,
134 },
135 });
136
137 const statement: PolicyStatement = {
138 Effect: "Allow",
139 Principal: { Service: "cloudfront.amazonaws.com" },
140 Action: ["s3:GetObject"],
141 Resource: [Output.interpolate`${bucket.bucketArn}/*` as any],
142 Condition: {
143 StringEquals: {
144 "AWS:SourceArn": distribution.distributionArn as any,
145 },
146 },
147 };
148
149 yield* bucket.bind`Allow(${distribution}, CloudFront.Read(${bucket}))`(
150 { policyStatements: [statement] },
151 );
152
153 return {
154 bucket,
155 oac,
156 cachePolicy,
157 originRequestPolicy,
158 responseHeadersPolicy,
159 publicKey,
160 keyGroup,
161 distribution,
162 };
163 }),
164 );
165
166 const dist = yield* cloudfront.getDistribution({
167 Id: deployed.distribution.distributionId,
168 });
169 expect(dist.Distribution?.Status).toEqual("Deployed");
170
171 const defaultBehavior =
172 dist.Distribution?.DistributionConfig?.DefaultCacheBehavior;
173 expect(defaultBehavior?.CachePolicyId).toEqual(
174 deployed.cachePolicy.cachePolicyId,
175 );
176 expect(defaultBehavior?.OriginRequestPolicyId).toEqual(
177 deployed.originRequestPolicy.originRequestPolicyId,
178 );
179 expect(defaultBehavior?.ResponseHeadersPolicyId).toEqual(
180 deployed.responseHeadersPolicy.responseHeadersPolicyId,
181 );
182
183 const cachePolicy = yield* cloudfront.getCachePolicy({
184 Id: deployed.cachePolicy.cachePolicyId,
185 });
186 expect(cachePolicy.CachePolicy?.Id).toEqual(
187 deployed.cachePolicy.cachePolicyId,
188 );
189
190 const originRequestPolicy = yield* cloudfront.getOriginRequestPolicy({
191 Id: deployed.originRequestPolicy.originRequestPolicyId,
192 });
193 expect(originRequestPolicy.OriginRequestPolicy?.Id).toEqual(
194 deployed.originRequestPolicy.originRequestPolicyId,
195 );
196
197 const responseHeadersPolicy =
198 yield* cloudfront.getResponseHeadersPolicy({
199 Id: deployed.responseHeadersPolicy.responseHeadersPolicyId,
200 });
201 expect(responseHeadersPolicy.ResponseHeadersPolicy?.Id).toEqual(
202 deployed.responseHeadersPolicy.responseHeadersPolicyId,
203 );
204
205 const keyGroup = yield* cloudfront.getKeyGroup({
206 Id: deployed.keyGroup.keyGroupId,
207 });
208 expect(keyGroup.KeyGroup?.KeyGroupConfig?.Items).toEqual([
209 deployed.publicKey.publicKeyId,
210 ]);
211
212 yield* stack.destroy();
213 yield* assertDistributionDeleted(
214 deployed.distribution.distributionId,
215 );
216 yield* assertCachePolicyDeleted(deployed.cachePolicy.cachePolicyId);
217 yield* assertOriginRequestPolicyDeleted(
218 deployed.originRequestPolicy.originRequestPolicyId,
219 );
220 yield* assertResponseHeadersPolicyDeleted(
221 deployed.responseHeadersPolicy.responseHeadersPolicyId,
222 );
223 yield* assertKeyGroupDeleted(deployed.keyGroup.keyGroupId);
224 yield* assertPublicKeyDeleted(deployed.publicKey.publicKeyId);
225 }),
226 900_000,
227 );
228 },
229);
230
231const retrying = (label: string) =>
232 Effect.retry({
233 while: (error: unknown) =>
234 error instanceof Error && error.message === label,
235 schedule: Schedule.max([Schedule.fixed("10 seconds"), Schedule.recurs(60)]),
236 });
237
238const assertDistributionDeleted = (id: string) =>
239 cloudfront.getDistribution({ Id: id }).pipe(
240 Effect.flatMap(() => Effect.fail(new Error("DistributionStillExists"))),
241 Effect.catchTag("NoSuchDistribution", () => Effect.void),
242 retrying("DistributionStillExists"),
243 );
244
245const assertCachePolicyDeleted = (id: string) =>
246 cloudfront.getCachePolicy({ Id: id }).pipe(
247 Effect.flatMap(() => Effect.fail(new Error("CachePolicyStillExists"))),
248 Effect.catchTag("NoSuchCachePolicy", () => Effect.void),
249 retrying("CachePolicyStillExists"),
250 );
251
252const assertOriginRequestPolicyDeleted = (id: string) =>
253 cloudfront.getOriginRequestPolicy({ Id: id }).pipe(
254 Effect.flatMap(() =>
255 Effect.fail(new Error("OriginRequestPolicyStillExists")),
256 ),
257 Effect.catchTag("NoSuchOriginRequestPolicy", () => Effect.void),
258 retrying("OriginRequestPolicyStillExists"),
259 );
260
261const assertResponseHeadersPolicyDeleted = (id: string) =>
262 cloudfront.getResponseHeadersPolicy({ Id: id }).pipe(
263 Effect.flatMap(() =>
264 Effect.fail(new Error("ResponseHeadersPolicyStillExists")),
265 ),
266 Effect.catchTag("NoSuchResponseHeadersPolicy", () => Effect.void),
267 retrying("ResponseHeadersPolicyStillExists"),
268 );
269
270const assertKeyGroupDeleted = (id: string) =>
271 cloudfront.getKeyGroup({ Id: id }).pipe(
272 Effect.flatMap(() => Effect.fail(new Error("KeyGroupStillExists"))),
273 Effect.catchTag("NoSuchResource", () => Effect.void),
274 retrying("KeyGroupStillExists"),
275 );
276
277const assertPublicKeyDeleted = (id: string) =>
278 cloudfront.getPublicKey({ Id: id }).pipe(
279 Effect.flatMap(() => Effect.fail(new Error("PublicKeyStillExists"))),
280 Effect.catchTag("NoSuchPublicKey", () => Effect.void),
281 retrying("PublicKeyStillExists"),
282 );