1import * as AWS from "@/AWS";
2import { FirewallPolicy, RuleGroup } from "@/AWS/NetworkFirewall";
3import * as Test from "@/Test/Alchemy";
4import * as nfw from "@distilled.cloud/aws/network-firewall";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Ungated typed-error probe.
12test.provider(
13 "describeFirewallPolicy on a nonexistent policy fails with ResourceNotFoundException",
14 () =>
15 Effect.gen(function* () {
16 const error = yield* Effect.flip(
17 nfw.describeFirewallPolicy({
18 FirewallPolicyName: "alchemy-nonexistent-nfw-policy-probe",
19 }),
20 );
21 expect(error._tag).toBe("ResourceNotFoundException");
22 }),
23 { tags: ["provider:aws", "provider:aws:networkfirewall", "live"] },
24);
25
26// Deletion transitions through a DELETING status before the policy
27// disappears — poll (bounded) until describe returns the typed not-found
28// tag. Throttled polls count as "still waiting".
29const assertPolicyGone = (arn: string) =>
30 Effect.gen(function* () {
31 const status = yield* nfw
32 .describeFirewallPolicy({ FirewallPolicyArn: arn })
33 .pipe(
34 Effect.map(
35 (r) =>
36 r.FirewallPolicyResponse.FirewallPolicyStatus ??
37 ("UNKNOWN" as const),
38 ),
39 Effect.catchTag("ResourceNotFoundException", () =>
40 Effect.succeed("gone" as const),
41 ),
42 Effect.catchTag("ThrottlingException", () =>
43 Effect.succeed("THROTTLED" as const),
44 ),
45 );
46 if (status !== "gone") {
47 return yield* Effect.fail(
48 new Error(`firewall policy '${arn}' still exists (status: ${status})`),
49 );
50 }
51 }).pipe(
52 Effect.retry({
53 schedule: Schedule.max([
54 Schedule.fixed("10 seconds"),
55 Schedule.recurs(18),
56 ]),
57 }),
58 );
59
60test.provider(
61 "create, update, and delete a firewall policy referencing a rule group",
62 (stack) =>
63 Effect.gen(function* () {
64 yield* stack.destroy();
65
66 const { stateful, policy } = yield* stack.deploy(
67 Effect.gen(function* () {
68 const stateful = yield* RuleGroup("Stateful", {
69 type: "STATEFUL",
70 capacity: 100,
71 rules:
72 'pass tcp any any -> any 443 (msg:"allow https"; sid:200001; rev:1;)',
73 });
74 const policy = yield* FirewallPolicy("Policy", {
75 firewallPolicy: {
76 StatelessDefaultActions: ["aws:forward_to_sfe"],
77 StatelessFragmentDefaultActions: ["aws:forward_to_sfe"],
78 StatefulRuleGroupReferences: [
79 { ResourceArn: stateful.ruleGroupArn },
80 ],
81 },
82 description: "v1",
83 tags: { fixture: "nfw-policy" },
84 });
85 return { stateful, policy };
86 }),
87 );
88
89 expect(policy.firewallPolicyArn).toContain(":firewall-policy/");
90 expect(policy.firewallPolicyId).toBeDefined();
91
92 // Out-of-band verification via distilled.
93 const observed = yield* nfw.describeFirewallPolicy({
94 FirewallPolicyArn: policy.firewallPolicyArn,
95 });
96 expect(observed.FirewallPolicyResponse.FirewallPolicyStatus).toBe(
97 "ACTIVE",
98 );
99 expect(
100 observed.FirewallPolicy?.StatefulRuleGroupReferences?.[0]?.ResourceArn,
101 ).toBe(stateful.ruleGroupArn);
102 expect(observed.FirewallPolicyResponse.Description).toBe("v1");
103
104 yield* stack.destroy();
105 yield* assertPolicyGone(policy.firewallPolicyArn);
106 }),
107 {
108 tags: ["provider:aws", "provider:aws:networkfirewall", "live"],
109 timeout: 240_000,
110 },
111);
112
113test.provider(
114 "updates the policy definition in place",
115 (stack) =>
116 Effect.gen(function* () {
117 yield* stack.destroy();
118
119 const deploy = (statelessDefaultActions: string[], description: string) =>
120 stack.deploy(
121 Effect.gen(function* () {
122 const policy = yield* FirewallPolicy("PassPolicy", {
123 firewallPolicy: {
124 StatelessDefaultActions: statelessDefaultActions,
125 StatelessFragmentDefaultActions: statelessDefaultActions,
126 },
127 description,
128 });
129 return { policy };
130 }),
131 );
132
133 const first = yield* deploy(["aws:pass"], "pass everything");
134 const second = yield* deploy(["aws:drop"], "drop everything");
135
136 // In-place update: same ARN, new definition.
137 expect(second.policy.firewallPolicyArn).toBe(
138 first.policy.firewallPolicyArn,
139 );
140 const observed = yield* nfw.describeFirewallPolicy({
141 FirewallPolicyArn: second.policy.firewallPolicyArn,
142 });
143 expect(observed.FirewallPolicy?.StatelessDefaultActions).toEqual([
144 "aws:drop",
145 ]);
146 expect(observed.FirewallPolicyResponse.Description).toBe(
147 "drop everything",
148 );
149
150 yield* stack.destroy();
151 yield* assertPolicyGone(second.policy.firewallPolicyArn);
152 }),
153 {
154 tags: ["provider:aws", "provider:aws:networkfirewall", "live"],
155 timeout: 240_000,
156 },
157);