1import * as AWS from "@/AWS";
2import { CachePolicy } 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
12describe(
13 "AWS.CloudFront.CachePolicy",
14 { tags: ["provider:aws", "provider:aws:cloudfront", "live"] },
15 () => {
16 test.provider(
17 "create, update, and delete a cache policy",
18 (stack) =>
19 Effect.gen(function* () {
20 yield* stack.destroy();
21
22 const created = yield* stack.deploy(
23 Effect.gen(function* () {
24 return yield* CachePolicy("ApiCachePolicy", {
25 comment: "initial",
26 minTTL: 0,
27 defaultTTL: "60 seconds",
28 maxTTL: "1 hour",
29 parametersInCacheKeyAndForwardedToOrigin: {
30 EnableAcceptEncodingGzip: true,
31 EnableAcceptEncodingBrotli: true,
32 HeadersConfig: {
33 HeaderBehavior: "whitelist",
34 Headers: { Quantity: 1, Items: ["Authorization"] },
35 },
36 CookiesConfig: { CookieBehavior: "none" },
37 QueryStringsConfig: { QueryStringBehavior: "all" },
38 },
39 });
40 }),
41 );
42
43 const initial = yield* cloudfront.getCachePolicy({
44 Id: created.cachePolicyId,
45 });
46 expect(initial.CachePolicy?.Id).toEqual(created.cachePolicyId);
47 expect(initial.CachePolicy?.CachePolicyConfig?.Comment).toEqual(
48 "initial",
49 );
50 expect(initial.CachePolicy?.CachePolicyConfig?.DefaultTTL).toEqual(
51 60,
52 );
53
54 const updated = yield* stack.deploy(
55 Effect.gen(function* () {
56 return yield* CachePolicy("ApiCachePolicy", {
57 comment: "updated",
58 minTTL: 0,
59 defaultTTL: "120 seconds",
60 maxTTL: "1 day",
61 parametersInCacheKeyAndForwardedToOrigin: {
62 EnableAcceptEncodingGzip: true,
63 EnableAcceptEncodingBrotli: true,
64 HeadersConfig: { HeaderBehavior: "none" },
65 CookiesConfig: { CookieBehavior: "none" },
66 QueryStringsConfig: { QueryStringBehavior: "none" },
67 },
68 });
69 }),
70 );
71
72 expect(updated.cachePolicyId).toEqual(created.cachePolicyId);
73
74 // Control-plane reads are eventually consistent — poll until the
75 // update is visible, then assert the exact wire values.
76 const after = yield* cloudfront
77 .getCachePolicy({ Id: updated.cachePolicyId })
78 .pipe(
79 Effect.repeat({
80 schedule: Schedule.fixed("2 seconds"),
81 until: (r) =>
82 r.CachePolicy?.CachePolicyConfig?.Comment === "updated",
83 times: 15,
84 }),
85 );
86 expect(after.CachePolicy?.CachePolicyConfig?.Comment).toEqual(
87 "updated",
88 );
89 expect(after.CachePolicy?.CachePolicyConfig?.DefaultTTL).toEqual(120);
90 expect(after.CachePolicy?.CachePolicyConfig?.MaxTTL).toEqual(86400);
91
92 yield* stack.destroy();
93 yield* assertCachePolicyDeleted(updated.cachePolicyId);
94 }),
95 { timeout: 300_000 },
96 );
97
98 test.provider(
99 "list enumerates the deployed cache policy",
100 (stack) =>
101 Effect.gen(function* () {
102 yield* stack.destroy();
103
104 const deployed = yield* stack.deploy(
105 Effect.gen(function* () {
106 return yield* CachePolicy("ListCachePolicy", {
107 comment: "list",
108 minTTL: 0,
109 defaultTTL: "60 seconds",
110 maxTTL: "1 hour",
111 parametersInCacheKeyAndForwardedToOrigin: {
112 EnableAcceptEncodingGzip: true,
113 EnableAcceptEncodingBrotli: true,
114 HeadersConfig: { HeaderBehavior: "none" },
115 CookiesConfig: { CookieBehavior: "none" },
116 QueryStringsConfig: { QueryStringBehavior: "none" },
117 },
118 });
119 }),
120 );
121
122 const provider = yield* Provider.findProvider(CachePolicy);
123 const all = yield* provider.list();
124
125 expect(
126 all.some((p) => p.cachePolicyId === deployed.cachePolicyId),
127 ).toBe(true);
128
129 yield* stack.destroy();
130 yield* assertCachePolicyDeleted(deployed.cachePolicyId);
131 }),
132 { timeout: 300_000 },
133 );
134 },
135);
136
137const assertCachePolicyDeleted = (id: string) =>
138 cloudfront.getCachePolicy({ Id: id }).pipe(
139 Effect.flatMap(() => Effect.fail(new Error("CachePolicyStillExists"))),
140 Effect.catchTag("NoSuchCachePolicy", () => Effect.void),
141 Effect.retry({
142 while: (error) =>
143 error instanceof Error && error.message === "CachePolicyStillExists",
144 schedule: Schedule.max([
145 Schedule.fixed("5 seconds"),
146 Schedule.recurs(24),
147 ]),
148 }),
149 );