1import * as AWS from "@/AWS";
2import { Subnet, Vpc } from "@/AWS/EC2";
3import { LogGroup } from "@/AWS/Logs";
4import {
5 Firewall,
6 FirewallPolicy,
7 LoggingConfiguration,
8} from "@/AWS/NetworkFirewall";
9import * as Test from "@/Test/Alchemy";
10import * as nfw from "@distilled.cloud/aws/network-firewall";
11import { expect } from "alchemy-test";
12import * as Effect from "effect/Effect";
13import * as Schedule from "effect/Schedule";
14
15const { test } = Test.make({ providers: AWS.providers() });
16
17// Ungated typed-error probes: prove the distilled error union carries the
18// not-found tags this provider's read/delete paths depend on.
19test.provider(
20 "describeFirewall on a nonexistent firewall fails with ResourceNotFoundException",
21 () =>
22 Effect.gen(function* () {
23 const error = yield* Effect.flip(
24 nfw.describeFirewall({
25 FirewallName: "alchemy-nonexistent-nfw-firewall-probe",
26 }),
27 );
28 expect(error._tag).toBe("ResourceNotFoundException");
29 }),
30 { tags: ["provider:aws", "provider:aws:networkfirewall", "live"] },
31);
32
33test.provider(
34 "describeLoggingConfiguration on a nonexistent firewall fails with ResourceNotFoundException",
35 () =>
36 Effect.gen(function* () {
37 const error = yield* Effect.flip(
38 nfw.describeLoggingConfiguration({
39 FirewallName: "alchemy-nonexistent-nfw-firewall-probe",
40 }),
41 );
42 expect(error._tag).toBe("ResourceNotFoundException");
43 }),
44 { tags: ["provider:aws", "provider:aws:networkfirewall", "live"] },
45);
46
47const assertFirewallGone = (name: string) =>
48 Effect.gen(function* () {
49 const error = yield* Effect.flip(
50 nfw.describeFirewall({ FirewallName: name }),
51 );
52 if (error._tag !== "ResourceNotFoundException") {
53 return yield* Effect.fail(
54 new Error(`firewall '${name}' still exists (${error._tag})`),
55 );
56 }
57 }).pipe(
58 Effect.retry({
59 schedule: Schedule.max([
60 Schedule.fixed("10 seconds"),
61 Schedule.recurs(18),
62 ]),
63 }),
64 );
65
66// A firewall takes ~5-10 minutes to provision its endpoints and a similar
67// window to deprovision on delete. The full lifecycle is gated behind
68// AWS_TEST_NETWORKFIREWALL=1 and always destroys what it created.
69test.provider.skipIf(!process.env.AWS_TEST_NETWORKFIREWALL)(
70 "create firewall with logging configuration, verify, destroy",
71 (stack) =>
72 Effect.gen(function* () {
73 yield* stack.destroy();
74
75 const { firewall, logging, policy } = yield* stack.deploy(
76 Effect.gen(function* () {
77 const vpc = yield* Vpc("Vpc", {
78 cidrBlock: "10.77.0.0/16",
79 tags: { fixture: "nfw-firewall" },
80 });
81 const subnet = yield* Subnet("FirewallSubnet", {
82 vpcId: vpc.vpcId,
83 cidrBlock: "10.77.1.0/24",
84 tags: { fixture: "nfw-firewall" },
85 });
86 const policy = yield* FirewallPolicy("Policy", {
87 firewallPolicy: {
88 StatelessDefaultActions: ["aws:pass"],
89 StatelessFragmentDefaultActions: ["aws:pass"],
90 },
91 });
92 const firewall = yield* Firewall("Firewall", {
93 firewallPolicyArn: policy.firewallPolicyArn,
94 vpcId: vpc.vpcId,
95 subnetMappings: [{ SubnetId: subnet.subnetId }],
96 description: "alchemy network firewall test",
97 tags: { fixture: "nfw-firewall" },
98 });
99 const logGroup = yield* LogGroup("FlowLogs", {
100 retention: "1 day",
101 });
102 const logging = yield* LoggingConfiguration("Logging", {
103 firewallArn: firewall.firewallArn,
104 logDestinationConfigs: [
105 {
106 LogType: "FLOW",
107 LogDestinationType: "CloudWatchLogs",
108 LogDestination: { logGroup: logGroup.logGroupName },
109 },
110 ],
111 });
112 return { firewall, logging, policy };
113 }),
114 );
115
116 expect(firewall.firewallArn).toContain(":firewall/");
117 expect(firewall.firewallId).toBeDefined();
118 expect(firewall.endpointIds.length).toBe(1);
119 expect(firewall.endpointIds[0]).toMatch(/^vpce-/);
120 expect(logging.firewallArn).toBe(firewall.firewallArn);
121
122 // Out-of-band verification via distilled.
123 const observed = yield* nfw.describeFirewall({
124 FirewallName: firewall.firewallName,
125 });
126 expect(observed.FirewallStatus?.Status).toBe("READY");
127 expect(observed.Firewall?.FirewallPolicyArn).toBe(
128 policy.firewallPolicyArn,
129 );
130
131 const observedLogging = yield* nfw.describeLoggingConfiguration({
132 FirewallArn: firewall.firewallArn,
133 });
134 expect(
135 observedLogging.LoggingConfiguration?.LogDestinationConfigs?.[0]
136 ?.LogType,
137 ).toBe("FLOW");
138
139 // Destroy immediately (delete waits for endpoint deprovisioning) and
140 // verify the firewall is gone out-of-band.
141 yield* stack.destroy();
142 yield* assertFirewallGone(firewall.firewallName);
143 }),
144 // firewall create (~10 min) + delete (~10 min), one test.
145 {
146 tags: [
147 "provider:aws",
148 "provider:aws:ec2",
149 "provider:aws:logs",
150 "provider:aws:networkfirewall",
151 "live",
152 ],
153 timeout: 2_400_000,
154 },
155);