1import * as AWS from "@/AWS";
2import { VpcConnector } from "@/AWS/AppRunner";
3import { SecurityGroup, Subnet, Vpc } from "@/AWS/EC2";
4import * as Test from "@/Test/Alchemy";
5import * as apprunner from "@distilled.cloud/aws/apprunner";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import {
9 assertSecurityGroupGone,
10 assertSubnetGone,
11 assertVpcGone,
12} from "../EC2/Gone.ts";
13
14const { test } = Test.make({ providers: AWS.providers() });
15
16// VPC connector creation is fast (deletion marks it INACTIVE immediately;
17// ENI teardown happens asynchronously server-side), so the lifecycle runs
18// ungated on a deterministic, stack-owned network.
19test.provider(
20 "create and destroy a VPC connector on a stack-owned VPC",
21 (stack) =>
22 Effect.gen(function* () {
23 // Clean slate in case a previous run died mid-flight.
24 yield* stack.destroy();
25
26 const { connector, securityGroup, subnet, vpc } = yield* stack.deploy(
27 Effect.gen(function* () {
28 const vpc = yield* Vpc("ConnectorVpc", {
29 cidrBlock: "10.84.0.0/16",
30 });
31 const subnet = yield* Subnet("ConnectorSubnet", {
32 vpcId: vpc.vpcId,
33 cidrBlock: "10.84.1.0/24",
34 });
35 const securityGroup = yield* SecurityGroup("ConnectorSecurityGroup", {
36 vpcId: vpc.vpcId,
37 });
38 const connector = yield* VpcConnector("Connector", {
39 subnets: [subnet.subnetId],
40 securityGroups: [securityGroup.groupId],
41 });
42 return { connector, securityGroup, subnet, vpc };
43 }),
44 );
45 expect(connector.vpcConnectorName.length).toBeGreaterThanOrEqual(4);
46 expect(connector.vpcConnectorArn).toContain(":vpcconnector/");
47 // App Runner returns lowercase statuses despite documenting uppercase.
48 expect(connector.status.toUpperCase()).toBe("ACTIVE");
49 expect(connector.vpcConnectorRevision).toBeGreaterThanOrEqual(1);
50
51 // Out-of-band verification via distilled.
52 const described = yield* apprunner.describeVpcConnector({
53 VpcConnectorArn: connector.vpcConnectorArn,
54 });
55 expect(described.VpcConnector.VpcConnectorName).toBe(
56 connector.vpcConnectorName,
57 );
58 expect(described.VpcConnector.Status?.toUpperCase()).toBe("ACTIVE");
59 expect(described.VpcConnector.Subnets).toEqual([subnet.subnetId]);
60 expect(described.VpcConnector.SecurityGroups).toEqual([
61 securityGroup.groupId,
62 ]);
63
64 // Destroy and verify deletion out-of-band: deleted connectors read
65 // INACTIVE (or disappear entirely).
66 yield* stack.destroy();
67 const after = yield* apprunner
68 .describeVpcConnector({
69 VpcConnectorArn: connector.vpcConnectorArn,
70 })
71 .pipe(
72 Effect.map((r) =>
73 (r.VpcConnector.Status ?? "INACTIVE").toUpperCase(),
74 ),
75 Effect.catchTag("ResourceNotFoundException", () =>
76 Effect.succeed("GONE" as const),
77 ),
78 );
79 expect(["INACTIVE", "GONE"]).toContain(after);
80
81 // The connector must release its managed ENIs before the dependency
82 // graph is considered destroyed, leaving no App Runner test network
83 // resources for nuke to discover.
84 yield* assertSubnetGone(subnet.subnetId);
85 yield* assertSecurityGroupGone(securityGroup.groupId);
86 yield* assertVpcGone(vpc.vpcId);
87 }),
88 {
89 tags: [
90 "provider:aws",
91 "provider:aws:apprunner",
92 "provider:aws:ec2",
93 "live",
94 ],
95 timeout: 180_000,
96 },
97);