1import * as AWS from "@/AWS";
2import { DBCluster, DBInstance, DBSubnetGroup } from "@/AWS/DocDB";
3import * as Test from "@/Test/Alchemy";
4import * as docdb from "@distilled.cloud/aws/docdb";
5import * as EC2 from "@distilled.cloud/aws/ec2";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9import { getDefaultVpc } from "../DefaultVpc.ts";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// Ungated typed-error probes: prove the distilled error union carries the
14// not-found tags this provider's read/delete paths depend on. These run in
15// every CI pass at near-zero cost, unlike the gated lifecycle below.
16test.provider(
17 "describeDBClusters on a nonexistent cluster fails with DBClusterNotFoundFault",
18 () =>
19 Effect.gen(function* () {
20 const error = yield* Effect.flip(
21 docdb.describeDBClusters({
22 DBClusterIdentifier: "alchemy-nonexistent-docdb-cluster-probe",
23 }),
24 );
25 expect(error._tag).toBe("DBClusterNotFoundFault");
26 }),
27 { tags: ["provider:aws", "provider:aws:docdb", "live"] },
28);
29
30test.provider(
31 "describeDBInstances on a nonexistent instance fails with DBInstanceNotFoundFault",
32 () =>
33 Effect.gen(function* () {
34 const error = yield* Effect.flip(
35 docdb.describeDBInstances({
36 DBInstanceIdentifier: "alchemy-nonexistent-docdb-instance-probe",
37 }),
38 );
39 expect(error._tag).toBe("DBInstanceNotFoundFault");
40 }),
41 { tags: ["provider:aws", "provider:aws:docdb", "live"] },
42);
43
44test.provider(
45 "describeDBSubnetGroups on a nonexistent group fails with DBSubnetGroupNotFoundFault",
46 () =>
47 Effect.gen(function* () {
48 const error = yield* Effect.flip(
49 docdb.describeDBSubnetGroups({
50 DBSubnetGroupName: "alchemy-nonexistent-docdb-subnet-probe",
51 }),
52 );
53 expect(error._tag).toBe("DBSubnetGroupNotFoundFault");
54 }),
55 { tags: ["provider:aws", "provider:aws:docdb", "live"] },
56);
57
58// Resolve two default-for-AZ subnets and the default security group.
59const defaultNetwork = Effect.gen(function* () {
60 const vpc = yield* getDefaultVpc;
61 const subnets = yield* EC2.describeSubnets({
62 Filters: [
63 { Name: "vpc-id", Values: [vpc.vpcId] },
64 { Name: "default-for-az", Values: ["true"] },
65 ],
66 });
67 const subnetIds = (subnets.Subnets ?? [])
68 .map((s) => s.SubnetId)
69 .filter((id): id is `subnet-${string}` => id !== undefined)
70 .sort()
71 .slice(0, 2);
72 const groups = yield* EC2.describeSecurityGroups({
73 Filters: [
74 { Name: "vpc-id", Values: [vpc.vpcId] },
75 { Name: "group-name", Values: ["default"] },
76 ],
77 });
78 const securityGroupId = groups.SecurityGroups?.[0]?.GroupId;
79 if (subnetIds.length < 2 || securityGroupId === undefined) {
80 return yield* Effect.die(
81 new Error("default VPC is missing subnets or its default security group"),
82 );
83 }
84 return { subnetIds, securityGroupIds: [securityGroupId] };
85});
86
87// Deletion is verified as INITIATED (status `deleting`, irreversible) or fully
88// gone. Full disappearance takes several more minutes server-side; waiting for
89// it would push the test into its timeout.
90const assertClusterDeleting = (identifier: string) =>
91 Effect.gen(function* () {
92 const status = yield* docdb
93 .describeDBClusters({ DBClusterIdentifier: identifier })
94 .pipe(
95 Effect.map((r) => r.DBClusters?.[0]?.Status ?? "gone"),
96 Effect.catchTag("DBClusterNotFoundFault", () =>
97 Effect.succeed("gone" as const),
98 ),
99 );
100 if (status !== "gone" && status !== "deleting") {
101 return yield* Effect.fail(
102 new Error(`cluster '${identifier}' still exists (status: ${status})`),
103 );
104 }
105 }).pipe(
106 Effect.retry({
107 schedule: Schedule.max([
108 Schedule.fixed("10 seconds"),
109 Schedule.recurs(18),
110 ]),
111 }),
112 );
113
114// A DocumentDB cluster + instance take ~5-10 minutes to provision and bill per
115// instance-hour while they exist. The full lifecycle is gated behind
116// AWS_TEST_SLOW=1 and always destroys what it created.
117test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
118 "create DocumentDB cluster + instance, verify, destroy",
119 (stack) =>
120 Effect.gen(function* () {
121 yield* stack.destroy();
122
123 const network = yield* defaultNetwork;
124
125 const { cluster, instance } = yield* stack.deploy(
126 Effect.gen(function* () {
127 const subnetGroup = yield* DBSubnetGroup("Subnets", {
128 description: "alchemy docdb cluster subnets",
129 subnetIds: network.subnetIds,
130 });
131 const cluster = yield* DBCluster("Cluster", {
132 dbSubnetGroupName: subnetGroup.dbSubnetGroupName,
133 vpcSecurityGroupIds: network.securityGroupIds,
134 masterUsername: "alchemy",
135 manageMasterUserPassword: true,
136 backupRetentionPeriod: "1 day",
137 deletionProtection: false,
138 tags: { fixture: "docdb-cluster" },
139 });
140 const instance = yield* DBInstance("Writer", {
141 dbClusterIdentifier: cluster.dbClusterIdentifier,
142 dbInstanceClass: "db.t3.medium",
143 tags: { fixture: "docdb-instance" },
144 });
145 return { cluster, instance };
146 }),
147 );
148
149 expect(cluster.dbClusterIdentifier).toBeDefined();
150 expect(cluster.dbClusterArn).toContain(":cluster:");
151 expect(cluster.engine).toBe("docdb");
152 expect(cluster.status).toBe("available");
153 expect(cluster.endpoint).toContain("docdb");
154 expect(cluster.masterUserSecretArn).toBeDefined();
155
156 expect(instance.dbInstanceIdentifier).toBeDefined();
157 expect(instance.dbClusterIdentifier).toBe(cluster.dbClusterIdentifier);
158 expect(instance.dbInstanceClass).toBe("db.t3.medium");
159 expect(instance.status).toBe("available");
160
161 // Out-of-band verification via distilled.
162 const describedCluster = yield* docdb.describeDBClusters({
163 DBClusterIdentifier: cluster.dbClusterIdentifier,
164 });
165 const observedCluster = describedCluster.DBClusters?.[0];
166 expect(observedCluster?.Status).toBe("available");
167 expect(observedCluster?.Engine).toBe("docdb");
168 expect(observedCluster?.DBClusterMembers?.length).toBe(1);
169
170 const describedInstance = yield* docdb.describeDBInstances({
171 DBInstanceIdentifier: instance.dbInstanceIdentifier,
172 });
173 expect(describedInstance.DBInstances?.[0]?.DBInstanceStatus).toBe(
174 "available",
175 );
176
177 // Destroy immediately — instances bill while they exist — and verify
178 // cluster deletion was initiated out-of-band.
179 yield* stack.destroy();
180 yield* assertClusterDeleting(cluster.dbClusterIdentifier);
181 }),
182 // cluster + instance create (~5-10 min) + delete initiation, one test.
183 {
184 tags: ["provider:aws", "provider:aws:docdb", "provider:aws:ec2", "live"],
185 timeout: 1_500_000,
186 },
187);