1import * as AWS from "@/AWS";
2import type { PolicyDocument } from "@/AWS/IAM/Policy.ts";
3import { ResourcePolicy } from "@/AWS/Logs/ResourcePolicy.ts";
4import * as Test from "@/Test/Alchemy";
5import * as logs from "@distilled.cloud/aws/cloudwatch-logs";
6import { expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// CAUTION: at most 10 resource policies per region per account. This suite
14// uses ONE deterministic policy name and guarantees deletion via
15// stack.destroy() at the start, at the end, and on error — never leak.
16const POLICY_NAME = "alchemy-test-logs-resource-policy";
17
18const findPolicy = Effect.fn(function* (policyName: string) {
19 // quota is 10 policies per region — one call is exhaustive
20 const described = yield* logs.describeResourcePolicies({ limit: 50 });
21 return (described.resourcePolicies ?? []).find(
22 (policy) => policy.policyName === policyName,
23 );
24});
25
26class ResourcePolicyStillExists extends Data.TaggedError(
27 "ResourcePolicyStillExists",
28)<{ readonly policyName: string }> {}
29
30const assertPolicyDeleted = (policyName: string) =>
31 findPolicy(policyName).pipe(
32 Effect.flatMap((policy) =>
33 policy === undefined
34 ? Effect.void
35 : Effect.fail(new ResourcePolicyStillExists({ policyName })),
36 ),
37 Effect.retry({
38 while: (e) => e._tag === "ResourcePolicyStillExists",
39 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
40 }),
41 );
42
43const route53Document = (sid: string): PolicyDocument => ({
44 Version: "2012-10-17",
45 Statement: [
46 {
47 Sid: sid,
48 Effect: "Allow",
49 Principal: { Service: "route53.amazonaws.com" },
50 Action: ["logs:CreateLogStream", "logs:PutLogEvents"],
51 Resource: ["arn:aws:logs:*:*:log-group:/aws/route53/alchemy-test-*"],
52 },
53 ],
54});
55
56test.provider(
57 "create, update document, delete resource policy",
58 (stack) =>
59 Effect.gen(function* () {
60 yield* stack.destroy();
61
62 const created = yield* stack.deploy(
63 Effect.gen(function* () {
64 return yield* ResourcePolicy("Route53QueryLogging", {
65 policyName: POLICY_NAME,
66 policyDocument: route53Document("AlchemyTestV1"),
67 });
68 }),
69 );
70
71 expect(created.policyName).toBe(POLICY_NAME);
72
73 // out-of-band verification via distilled
74 const observedCreated = yield* findPolicy(POLICY_NAME);
75 expect(observedCreated?.policyDocument).toContain("AlchemyTestV1");
76 expect(observedCreated?.policyDocument).toContain(
77 "route53.amazonaws.com",
78 );
79
80 // update the document in place (same policy name)
81 const updated = yield* stack.deploy(
82 Effect.gen(function* () {
83 return yield* ResourcePolicy("Route53QueryLogging", {
84 policyName: POLICY_NAME,
85 policyDocument: route53Document("AlchemyTestV2"),
86 });
87 }),
88 );
89 expect(updated.policyName).toBe(POLICY_NAME);
90
91 const observedUpdated = yield* findPolicy(POLICY_NAME);
92 expect(observedUpdated?.policyDocument).toContain("AlchemyTestV2");
93
94 yield* stack.destroy();
95 yield* assertPolicyDeleted(POLICY_NAME);
96 }).pipe(Effect.onError(() => stack.destroy().pipe(Effect.ignore))),
97 {
98 tags: ["provider:aws", "provider:aws:iam", "provider:aws:logs", "live"],
99 timeout: 120_000,
100 },
101);