1import * as AWS from "@/AWS";
2import { EnabledBaseline, EnabledControl } from "@/AWS/ControlTower";
3import * as Test from "@/Test/Alchemy";
4import * as controltower from "@distilled.cloud/aws/controltower";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Result from "effect/Result";
8import * as Schedule from "effect/Schedule";
9import * as Stream from "effect/Stream";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// AWS Control Tower requires an AWS Organizations MANAGEMENT account with a
14// landing zone deployed — neither is available on the shared testing
15// account, so every lifecycle is hard-gated behind AWS_TEST_CONTROLTOWER=1.
16// The ungated probes below prove the distilled wiring and typed error
17// unions the providers' read/delete paths depend on, at near-zero cost.
18
19describe(
20 "AWS.ControlTower",
21 { tags: ["provider:aws", "provider:aws:controltower", "live"] },
22 () => {
23 test.provider(
24 "getLandingZoneOperation on a bogus operation id yields a typed error",
25 () =>
26 Effect.gen(function* () {
27 const error = yield* Effect.flip(
28 controltower.getLandingZoneOperation({
29 operationIdentifier: "00000000-0000-0000-0000-000000000000",
30 }),
31 );
32 // A well-formed but absent operation id yields the typed
33 // ResourceNotFoundException the providers' waiters depend on —
34 // even on an account without a landing zone.
35 expect(error._tag).toBe("ResourceNotFoundException");
36 }),
37 { timeout: 60_000 },
38 );
39
40 test.provider(
41 "listLandingZones yields a typed result",
42 () =>
43 Effect.gen(function* () {
44 const result = yield* Effect.result(
45 controltower.listLandingZones({}),
46 );
47 if (Result.isSuccess(result)) {
48 // No landing zone on the testing account — an empty (or singleton)
49 // list, never a crash.
50 expect(Array.isArray(result.success.landingZones)).toBe(true);
51 } else {
52 // Some org configurations reject the call outright — but always
53 // with a typed tag (UnauthorizedException is a distilled patch).
54 expect([
55 "AccessDeniedException",
56 "UnauthorizedException",
57 ]).toContain(result.failure._tag);
58 }
59 }),
60 { timeout: 60_000 },
61 );
62
63 test.provider(
64 "getEnabledControl on a nonexistent identifier yields a typed error",
65 () =>
66 Effect.gen(function* () {
67 const error = yield* Effect.flip(
68 controltower.getEnabledControl({
69 enabledControlIdentifier:
70 "arn:aws:controltower:us-west-2:111111111111:enabledcontrol/AAAAAAAAAAAAAAAA",
71 }),
72 );
73 // Both a genuinely absent enabled control and a no-landing-zone
74 // account surface the typed ResourceNotFoundException the
75 // provider's observe/delete paths depend on.
76 expect(error._tag).toBe("ResourceNotFoundException");
77 }),
78 { timeout: 60_000 },
79 );
80
81 test.provider(
82 "getEnabledBaseline on a nonexistent identifier yields a typed error",
83 () =>
84 Effect.gen(function* () {
85 const error = yield* Effect.flip(
86 controltower.getEnabledBaseline({
87 enabledBaselineIdentifier:
88 "arn:aws:controltower:us-west-2:111111111111:enabledbaseline/AAAAAAAAAAAAAAAA",
89 }),
90 );
91 // The baseline API family rejects accounts without a landing zone
92 // with the wire error UnauthorizedException, which the Smithy model
93 // omits — surfaced as a typed tag via the distilled patch. An
94 // entitled account yields ResourceNotFoundException instead.
95 expect([
96 "UnauthorizedException",
97 "ResourceNotFoundException",
98 ]).toContain(error._tag);
99 }),
100 { timeout: 60_000 },
101 );
102
103 test.provider(
104 "listBaselines yields a typed result",
105 () =>
106 Effect.gen(function* () {
107 const result = yield* Effect.result(controltower.listBaselines({}));
108 if (Result.isSuccess(result)) {
109 // The baseline catalog is served even without a landing zone.
110 expect(result.success.baselines.length).toBeGreaterThan(0);
111 expect(
112 result.success.baselines.some(
113 (b) => b.name === "AWSControlTowerBaseline",
114 ),
115 ).toBe(true);
116 } else {
117 expect([
118 "AccessDeniedException",
119 "UnauthorizedException",
120 ]).toContain(result.failure._tag);
121 }
122 }),
123 { timeout: 60_000 },
124 );
125
126 // ---------------------------------------------------------------------
127 // Gated lifecycles — need an Organizations management account with a
128 // Control Tower landing zone. Provide:
129 // AWS_TEST_CONTROLTOWER=1
130 // AWS_TEST_CONTROLTOWER_OU target OU ARN (unregistered OU for
131 // the baseline test)
132 // AWS_TEST_CONTROLTOWER_CONTROL control ARN (defaults to the
133 // AWS-GR_ENCRYPTED_VOLUMES legacy ARN
134 // in us-west-2)
135 // AWS_TEST_CONTROLTOWER_BASELINE_VERSION (defaults to "4.0")
136 // ---------------------------------------------------------------------
137
138 const ouArn = process.env.AWS_TEST_CONTROLTOWER_OU ?? "";
139 const controlArn =
140 process.env.AWS_TEST_CONTROLTOWER_CONTROL ??
141 "arn:aws:controltower:us-west-2::control/AWS-GR_ENCRYPTED_VOLUMES";
142
143 test.provider.skipIf(!process.env.AWS_TEST_CONTROLTOWER)(
144 "enable a control on an OU, verify, then disable",
145 (stack) =>
146 Effect.gen(function* () {
147 yield* stack.destroy();
148
149 const { enabled } = yield* stack.deploy(
150 Effect.gen(function* () {
151 const enabled = yield* EnabledControl("Guardrail", {
152 controlIdentifier: controlArn,
153 targetIdentifier: ouArn,
154 tags: { fixture: "controltower-enabled-control" },
155 });
156 return { enabled };
157 }),
158 );
159
160 expect(enabled.enabledControlArn).toContain(":enabledcontrol/");
161 expect(enabled.controlIdentifier).toBe(controlArn);
162 expect(enabled.targetIdentifier).toBe(ouArn);
163
164 // Out-of-band verification via distilled.
165 const observed = yield* controltower.getEnabledControl({
166 enabledControlIdentifier: enabled.enabledControlArn,
167 });
168 expect(observed.enabledControlDetails.controlIdentifier).toBe(
169 controlArn,
170 );
171 expect(observed.enabledControlDetails.statusSummary?.status).toBe(
172 "SUCCEEDED",
173 );
174
175 // Destroy and verify the enablement is gone (typed wait-until-gone).
176 yield* stack.destroy();
177 yield* controltower
178 .getEnabledControl({
179 enabledControlIdentifier: enabled.enabledControlArn,
180 })
181 .pipe(
182 Effect.flatMap(() =>
183 Effect.fail(new Error("enabled control still exists")),
184 ),
185 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
186 Effect.retry({
187 schedule: Schedule.max([
188 Schedule.fixed("10 seconds"),
189 Schedule.recurs(18),
190 ]),
191 }),
192 );
193 }),
194 // enable (~minutes) + disable (~minutes), one test.
195 { timeout: 1_500_000 },
196 );
197
198 test.provider.skipIf(!process.env.AWS_TEST_CONTROLTOWER)(
199 "enable the Control Tower baseline on an OU, verify, then disable",
200 (stack) =>
201 Effect.gen(function* () {
202 yield* stack.destroy();
203
204 // Discover the AWSControlTowerBaseline ARN dynamically.
205 const baselines = yield* controltower.listBaselines.pages({}).pipe(
206 Stream.runCollect,
207 Effect.map((chunk) =>
208 Array.from(chunk).flatMap((page) => page.baselines),
209 ),
210 );
211 const controlTowerBaseline = baselines.find(
212 (b) => b.name === "AWSControlTowerBaseline",
213 );
214 expect(controlTowerBaseline).toBeDefined();
215 const baselineVersion =
216 process.env.AWS_TEST_CONTROLTOWER_BASELINE_VERSION ?? "4.0";
217
218 const { enabled } = yield* stack.deploy(
219 Effect.gen(function* () {
220 const enabled = yield* EnabledBaseline("OuBaseline", {
221 baselineIdentifier: controlTowerBaseline!.arn,
222 baselineVersion,
223 targetIdentifier: ouArn,
224 tags: { fixture: "controltower-enabled-baseline" },
225 });
226 return { enabled };
227 }),
228 );
229
230 expect(enabled.enabledBaselineArn).toContain(":enabledbaseline/");
231 expect(enabled.baselineIdentifier).toBe(controlTowerBaseline!.arn);
232 expect(enabled.targetIdentifier).toBe(ouArn);
233
234 // Out-of-band verification via distilled.
235 const observed = yield* controltower.getEnabledBaseline({
236 enabledBaselineIdentifier: enabled.enabledBaselineArn,
237 });
238 expect(observed.enabledBaselineDetails?.statusSummary.status).toBe(
239 "SUCCEEDED",
240 );
241
242 // Destroy and verify the enablement is gone (typed wait-until-gone).
243 yield* stack.destroy();
244 yield* controltower
245 .getEnabledBaseline({
246 enabledBaselineIdentifier: enabled.enabledBaselineArn,
247 })
248 .pipe(
249 Effect.flatMap(() =>
250 Effect.fail(new Error("enabled baseline still exists")),
251 ),
252 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
253 Effect.retry({
254 schedule: Schedule.max([
255 Schedule.fixed("10 seconds"),
256 Schedule.recurs(18),
257 ]),
258 }),
259 );
260 }),
261 { timeout: 1_500_000 },
262 );
263
264 // The LandingZone resource's create/decommission paths are implemented
265 // but deliberately NOT exercised even when gated — decommissioning a
266 // live organization's landing zone is an irreversible ~1 hour operation.
267 // On an entitled account this asserts the observe/read path end-to-end.
268 test.provider.skipIf(!process.env.AWS_TEST_CONTROLTOWER)(
269 "landing zone read path resolves the deployed landing zone",
270 () =>
271 Effect.gen(function* () {
272 const { landingZones } = yield* controltower.listLandingZones({});
273 expect(landingZones.length).toBe(1);
274 const arn = landingZones[0]!.arn!;
275 const { landingZone } = yield* controltower.getLandingZone({
276 landingZoneIdentifier: arn,
277 });
278 expect(landingZone.version).toBeDefined();
279 expect(landingZone.status).toBeDefined();
280 }),
281 { timeout: 120_000 },
282 );
283 },
284);