1import * as AWS from "@/AWS";
2import { Distribution, OriginAccessControl } from "@/AWS/CloudFront";
3import type { PolicyStatement } from "@/AWS/IAM/Policy";
4import { Bucket } from "@/AWS/S3";
5import * as Output from "@/Output";
6import * as Provider from "@/Provider";
7import * as Test from "@/Test/Alchemy";
8import * as cloudfront from "@distilled.cloud/aws/cloudfront";
9import * as S3 from "@distilled.cloud/aws/s3";
10import { expect } from "alchemy-test";
11import * as Effect from "effect/Effect";
12import * as Schedule from "effect/Schedule";
13
14const { test } = Test.make({ providers: AWS.providers() });
15
16test.provider.skipIf(!!process.env.FAST)(
17 "create invalidation with explicit paths and wait for completion",
18 (stack) =>
19 Effect.gen(function* () {
20 yield* stack.destroy();
21
22 const deployed = yield* stack.deploy(
23 Effect.gen(function* () {
24 const bucket = yield* Bucket("WebsiteBucket", {
25 forceDestroy: true,
26 });
27 const oac = yield* OriginAccessControl("WebsiteOac", {
28 originType: "s3",
29 });
30 const distribution = yield* Distribution("WebsiteDistribution", {
31 origins: [
32 {
33 id: "site",
34 domainName: bucket.bucketRegionalDomainName,
35 s3Origin: true,
36 originAccessControlId: oac.originAccessControlId,
37 },
38 ],
39 defaultRootObject: "index.html",
40 defaultCacheBehavior: {
41 targetOriginId: "site",
42 viewerProtocolPolicy: "redirect-to-https",
43 compress: true,
44 allowedMethods: ["GET", "HEAD"],
45 cachedMethods: ["GET", "HEAD"],
46 forwardedValues: {
47 QueryString: false,
48 Cookies: {
49 Forward: "none",
50 },
51 },
52 },
53 });
54
55 const statement: PolicyStatement = {
56 Effect: "Allow",
57 Principal: {
58 Service: "cloudfront.amazonaws.com",
59 },
60 Action: ["s3:GetObject"],
61 Resource: [Output.interpolate`${bucket.bucketArn}/*` as any],
62 Condition: {
63 StringEquals: {
64 "AWS:SourceArn": distribution.distributionArn as any,
65 },
66 },
67 };
68
69 yield* bucket.bind`Allow(${distribution}, CloudFront.Read(${bucket}))`(
70 {
71 policyStatements: [statement],
72 },
73 );
74
75 const invalidation = yield* AWS.CloudFront.Invalidation(
76 "InvalidateDocs",
77 {
78 distributionId: distribution.distributionId,
79 version: "v2",
80 wait: true,
81 paths: ["/index.html", "/docs/*"],
82 },
83 );
84
85 return {
86 bucket,
87 distribution,
88 invalidation,
89 };
90 }),
91 );
92
93 yield* S3.putObject({
94 Bucket: deployed.bucket.bucketName,
95 Key: "index.html",
96 Body: "<html>ok</html>",
97 ContentType: "text/html; charset=utf-8",
98 });
99
100 const current = yield* cloudfront.getInvalidation({
101 DistributionId: deployed.distribution.distributionId,
102 Id: deployed.invalidation.invalidationId,
103 });
104 expect(current.Invalidation?.Status).toEqual("Completed");
105 // CloudFront returns invalidation paths in arbitrary order.
106 expect(
107 [
108 ...(current.Invalidation?.InvalidationBatch?.Paths?.Items ?? []),
109 ].sort(),
110 ).toEqual(["/docs/*", "/index.html"]);
111
112 yield* stack.destroy();
113 yield* assertDistributionDeleted(deployed.distribution.distributionId);
114 }),
115 {
116 tags: [
117 "provider:aws",
118 "provider:aws:cloudfront",
119 "provider:aws:iam",
120 "provider:aws:s3",
121 "live",
122 ],
123 timeout: 600_000,
124 },
125);
126
127test.provider(
128 "list returns [] for the non-listable ephemeral invalidation",
129 () =>
130 Effect.gen(function* () {
131 const provider = yield* Provider.findProvider(
132 AWS.CloudFront.Invalidation,
133 );
134 const all = yield* provider.list();
135 expect(all).toEqual([]);
136 }),
137 { tags: ["provider:aws", "provider:aws:cloudfront", "live"] },
138);
139
140const assertDistributionDeleted = (distributionId: string) =>
141 cloudfront.getDistribution({ Id: distributionId }).pipe(
142 Effect.flatMap(() => Effect.fail(new Error("DistributionStillExists"))),
143 Effect.catchTag("NoSuchDistribution", () => Effect.void),
144 Effect.retry({
145 while: (error) =>
146 error instanceof Error && error.message === "DistributionStillExists",
147 schedule: Schedule.max([
148 Schedule.fixed("10 seconds"),
149 Schedule.recurs(60),
150 ]),
151 }),
152 );