1import * as AWS from "@/AWS";
2import { ResponseHeadersPolicy } 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.ResponseHeadersPolicy",
14 { tags: ["provider:aws", "provider:aws:cloudfront", "live"] },
15 () => {
16 test.provider(
17 "create, update, and delete a response headers 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* ResponseHeadersPolicy("AppResponseHeaders", {
25 comment: "initial",
26 securityHeadersConfig: {
27 StrictTransportSecurity: {
28 AccessControlMaxAgeSec: 31536000,
29 IncludeSubdomains: true,
30 Preload: true,
31 Override: true,
32 },
33 ContentTypeOptions: { Override: true },
34 FrameOptions: { FrameOption: "DENY", Override: true },
35 ReferrerPolicy: {
36 ReferrerPolicy: "no-referrer",
37 Override: true,
38 },
39 },
40 });
41 }),
42 );
43
44 const initial = yield* cloudfront.getResponseHeadersPolicy({
45 Id: created.responseHeadersPolicyId,
46 });
47 expect(initial.ResponseHeadersPolicy?.Id).toEqual(
48 created.responseHeadersPolicyId,
49 );
50 expect(
51 initial.ResponseHeadersPolicy?.ResponseHeadersPolicyConfig?.Comment,
52 ).toEqual("initial");
53 expect(
54 initial.ResponseHeadersPolicy?.ResponseHeadersPolicyConfig
55 ?.SecurityHeadersConfig?.FrameOptions?.FrameOption,
56 ).toEqual("DENY");
57
58 const updated = yield* stack.deploy(
59 Effect.gen(function* () {
60 return yield* ResponseHeadersPolicy("AppResponseHeaders", {
61 comment: "updated",
62 corsConfig: {
63 AccessControlAllowOrigins: {
64 Quantity: 1,
65 Items: ["https://app.example.com"],
66 },
67 AccessControlAllowMethods: {
68 Quantity: 2,
69 Items: ["GET", "OPTIONS"],
70 },
71 AccessControlAllowHeaders: {
72 Quantity: 1,
73 Items: ["Authorization"],
74 },
75 AccessControlAllowCredentials: false,
76 OriginOverride: true,
77 },
78 securityHeadersConfig: {
79 FrameOptions: { FrameOption: "SAMEORIGIN", Override: true },
80 },
81 });
82 }),
83 );
84
85 expect(updated.responseHeadersPolicyId).toEqual(
86 created.responseHeadersPolicyId,
87 );
88
89 // Control-plane reads are eventually consistent — poll until the
90 // update is visible, then assert.
91 const after = yield* cloudfront
92 .getResponseHeadersPolicy({ Id: updated.responseHeadersPolicyId })
93 .pipe(
94 Effect.repeat({
95 schedule: Schedule.fixed("2 seconds"),
96 until: (r) =>
97 r.ResponseHeadersPolicy?.ResponseHeadersPolicyConfig
98 ?.Comment === "updated",
99 times: 15,
100 }),
101 );
102 expect(
103 after.ResponseHeadersPolicy?.ResponseHeadersPolicyConfig?.Comment,
104 ).toEqual("updated");
105 expect(
106 after.ResponseHeadersPolicy?.ResponseHeadersPolicyConfig
107 ?.SecurityHeadersConfig?.FrameOptions?.FrameOption,
108 ).toEqual("SAMEORIGIN");
109 expect(
110 after.ResponseHeadersPolicy?.ResponseHeadersPolicyConfig?.CorsConfig
111 ?.AccessControlAllowOrigins?.Items,
112 ).toEqual(["https://app.example.com"]);
113
114 yield* stack.destroy();
115 yield* assertResponseHeadersPolicyDeleted(
116 updated.responseHeadersPolicyId,
117 );
118 }),
119 { timeout: 300_000 },
120 );
121
122 test.provider(
123 "list enumerates the deployed response headers policy",
124 (stack) =>
125 Effect.gen(function* () {
126 yield* stack.destroy();
127
128 const deployed = yield* stack.deploy(
129 Effect.gen(function* () {
130 return yield* ResponseHeadersPolicy("ListResponseHeaders", {
131 comment: "list",
132 securityHeadersConfig: {
133 ContentTypeOptions: { Override: true },
134 },
135 });
136 }),
137 );
138
139 const provider = yield* Provider.findProvider(ResponseHeadersPolicy);
140 const all = yield* provider.list();
141
142 expect(
143 all.some(
144 (p) =>
145 p.responseHeadersPolicyId === deployed.responseHeadersPolicyId,
146 ),
147 ).toBe(true);
148
149 yield* stack.destroy();
150 yield* assertResponseHeadersPolicyDeleted(
151 deployed.responseHeadersPolicyId,
152 );
153 }),
154 { timeout: 300_000 },
155 );
156 },
157);
158
159const assertResponseHeadersPolicyDeleted = (id: string) =>
160 cloudfront.getResponseHeadersPolicy({ Id: id }).pipe(
161 Effect.flatMap(() =>
162 Effect.fail(new Error("ResponseHeadersPolicyStillExists")),
163 ),
164 Effect.catchTag("NoSuchResponseHeadersPolicy", () => Effect.void),
165 Effect.retry({
166 while: (error) =>
167 error instanceof Error &&
168 error.message === "ResponseHeadersPolicyStillExists",
169 schedule: Schedule.max([
170 Schedule.fixed("5 seconds"),
171 Schedule.recurs(24),
172 ]),
173 }),
174 );