1import * as AWS from "@/AWS";
2import { RuleGroup } from "@/AWS/WAFv2";
3import * as Test from "@/Test/Alchemy";
4import type * as WAFV2 from "@distilled.cloud/aws/wafv2";
5import * as wafv2 from "@distilled.cloud/aws/wafv2";
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
13class RuleGroupStillExists extends Data.TaggedError("RuleGroupStillExists")<{
14 readonly name: string;
15}> {}
16
17const assertRuleGroupDeleted = (name: string, id: string) =>
18 wafv2.getRuleGroup({ Name: name, Scope: "REGIONAL", Id: id }).pipe(
19 Effect.flatMap(() => Effect.fail(new RuleGroupStillExists({ name }))),
20 Effect.catchTag("WAFNonexistentItemException", () => Effect.void),
21 Effect.retry({
22 while: (e) => e._tag === "RuleGroupStillExists",
23 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
24 }),
25 );
26
27const visibility = (metricName: string): WAFV2.VisibilityConfig => ({
28 SampledRequestsEnabled: true,
29 CloudWatchMetricsEnabled: true,
30 MetricName: metricName,
31});
32
33const blockPathRule = (searchString: string): WAFV2.Rule => ({
34 Name: "block-path",
35 Priority: 0,
36 Statement: {
37 ByteMatchStatement: {
38 SearchString: new TextEncoder().encode(searchString),
39 FieldToMatch: { UriPath: {} },
40 TextTransformations: [{ Priority: 0, Type: "LOWERCASE" }],
41 PositionalConstraint: "STARTS_WITH",
42 },
43 },
44 Action: { Block: {} },
45 VisibilityConfig: visibility("block-path"),
46});
47
48test.provider(
49 "create, update rules, replace on capacity change, delete",
50 (stack) =>
51 Effect.gen(function* () {
52 // reconcile away any prior partial/crashed deployment
53 yield* stack.destroy();
54
55 const group = yield* stack.deploy(
56 Effect.gen(function* () {
57 return yield* RuleGroup("LifecycleRuleGroup", {
58 capacity: 50,
59 rules: [blockPathRule("/admin")],
60 tags: { Environment: "test" },
61 });
62 }),
63 );
64
65 expect(group.capacity).toBe(50);
66 expect(group.scope).toBe("REGIONAL");
67 expect(group.ruleGroupArn).toContain("/rulegroup/");
68
69 // out-of-band verification via distilled
70 const created = yield* wafv2.getRuleGroup({
71 Name: group.ruleGroupName,
72 Scope: "REGIONAL",
73 Id: group.ruleGroupId,
74 });
75 expect(created.RuleGroup?.Capacity).toBe(50);
76 expect(created.RuleGroup?.Rules?.length).toBe(1);
77 expect(
78 created.RuleGroup?.Rules?.[0]?.Statement?.ByteMatchStatement
79 ?.PositionalConstraint,
80 ).toBe("STARTS_WITH");
81
82 const tags = yield* wafv2.listTagsForResource({
83 ResourceARN: group.ruleGroupArn,
84 });
85 const tagRecord = Object.fromEntries(
86 (tags.TagInfoForResource?.TagList ?? []).map((t) => [t.Key, t.Value]),
87 );
88 expect(tagRecord["alchemy::id"]).toBe("LifecycleRuleGroup");
89
90 // rules are mutable within the fixed capacity — updated in place
91 const updated = yield* stack.deploy(
92 Effect.gen(function* () {
93 return yield* RuleGroup("LifecycleRuleGroup", {
94 capacity: 50,
95 rules: [blockPathRule("/internal")],
96 description: "blocks internal paths",
97 tags: { Environment: "test" },
98 });
99 }),
100 );
101 expect(updated.ruleGroupId).toBe(group.ruleGroupId);
102
103 const afterUpdate = yield* wafv2.getRuleGroup({
104 Name: group.ruleGroupName,
105 Scope: "REGIONAL",
106 Id: group.ruleGroupId,
107 });
108 expect(afterUpdate.RuleGroup?.Description).toBe("blocks internal paths");
109 const search =
110 afterUpdate.RuleGroup?.Rules?.[0]?.Statement?.ByteMatchStatement
111 ?.SearchString;
112 expect(new TextDecoder().decode(search)).toBe("/internal");
113
114 // capacity is immutable ⇒ replacement
115 const replaced = yield* stack.deploy(
116 Effect.gen(function* () {
117 return yield* RuleGroup("LifecycleRuleGroup", {
118 capacity: 60,
119 rules: [blockPathRule("/internal")],
120 });
121 }),
122 );
123 expect(replaced.ruleGroupId).not.toBe(group.ruleGroupId);
124 expect(replaced.capacity).toBe(60);
125 yield* assertRuleGroupDeleted(group.ruleGroupName, group.ruleGroupId);
126
127 yield* stack.destroy();
128 yield* assertRuleGroupDeleted(
129 replaced.ruleGroupName,
130 replaced.ruleGroupId,
131 );
132 }),
133 { tags: ["provider:aws", "provider:aws:wafv2", "live"], timeout: 120_000 },
134);