1import * as AWS from "@/AWS";
2import { RegexPatternSet } from "@/AWS/WAFv2";
3import * as Test from "@/Test/Alchemy";
4import * as wafv2 from "@distilled.cloud/aws/wafv2";
5import { expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12class RegexPatternSetStillExists extends Data.TaggedError(
13 "RegexPatternSetStillExists",
14)<{
15 readonly name: string;
16}> {}
17
18const assertDeleted = (name: string, id: string) =>
19 wafv2.getRegexPatternSet({ Name: name, Scope: "REGIONAL", Id: id }).pipe(
20 Effect.flatMap(() => Effect.fail(new RegexPatternSetStillExists({ name }))),
21 Effect.catchTag("WAFNonexistentItemException", () => Effect.void),
22 Effect.retry({
23 while: (e) => e._tag === "RegexPatternSetStillExists",
24 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
25 }),
26 );
27
28test.provider(
29 "create, update expressions and tags, delete",
30 (stack) =>
31 Effect.gen(function* () {
32 // reconcile away any prior partial/crashed deployment
33 yield* stack.destroy();
34
35 const set = yield* stack.deploy(
36 Effect.gen(function* () {
37 return yield* RegexPatternSet("LifecycleRegexSet", {
38 regularExpressions: ["^/wp-admin", "\\.php$"],
39 description: "bad paths",
40 tags: { Environment: "test" },
41 });
42 }),
43 );
44
45 expect(set.scope).toBe("REGIONAL");
46 expect([...set.regularExpressions].sort()).toEqual([
47 "\\.php$",
48 "^/wp-admin",
49 ]);
50
51 // out-of-band verification via distilled
52 const created = yield* wafv2.getRegexPatternSet({
53 Name: set.regexPatternSetName,
54 Scope: "REGIONAL",
55 Id: set.regexPatternSetId,
56 });
57 expect(
58 (created.RegexPatternSet?.RegularExpressionList ?? [])
59 .map((r) => r.RegexString)
60 .sort(),
61 ).toEqual(["\\.php$", "^/wp-admin"]);
62 expect(created.RegexPatternSet?.Description).toBe("bad paths");
63
64 const tags = yield* wafv2.listTagsForResource({
65 ResourceARN: set.regexPatternSetArn,
66 });
67 const tagRecord = Object.fromEntries(
68 (tags.TagInfoForResource?.TagList ?? []).map((t) => [t.Key, t.Value]),
69 );
70 expect(tagRecord.Environment).toBe("test");
71 expect(tagRecord["alchemy::id"]).toBe("LifecycleRegexSet");
72
73 // expressions are mutable — updated in place
74 const updated = yield* stack.deploy(
75 Effect.gen(function* () {
76 return yield* RegexPatternSet("LifecycleRegexSet", {
77 regularExpressions: ["^/admin"],
78 description: "bad paths",
79 tags: { Environment: "test", Extra: "1" },
80 });
81 }),
82 );
83 expect(updated.regexPatternSetId).toBe(set.regexPatternSetId);
84 expect(updated.regularExpressions).toEqual(["^/admin"]);
85
86 const afterUpdate = yield* wafv2.getRegexPatternSet({
87 Name: set.regexPatternSetName,
88 Scope: "REGIONAL",
89 Id: set.regexPatternSetId,
90 });
91 expect(
92 (afterUpdate.RegexPatternSet?.RegularExpressionList ?? []).map(
93 (r) => r.RegexString,
94 ),
95 ).toEqual(["^/admin"]);
96
97 yield* stack.destroy();
98 yield* assertDeleted(set.regexPatternSetName, set.regexPatternSetId);
99 }),
100 { tags: ["provider:aws", "provider:aws:wafv2", "live"], timeout: 120_000 },
101);