1import * as AWS from "@/AWS";
2import { ACL, Cluster, SubnetGroup, User } from "@/AWS/MemoryDB";
3import * as Test from "@/Test/Alchemy";
4import * as EC2 from "@distilled.cloud/aws/ec2";
5import * as memorydb from "@distilled.cloud/aws/memorydb";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Redacted from "effect/Redacted";
9import * as Schedule from "effect/Schedule";
10import { getDefaultVpc } from "../DefaultVpc.ts";
11
12const { test } = Test.make({ providers: AWS.providers() });
13
14// Ungated typed-error probe: proves the distilled error union carries the
15// not-found tag this provider's read/delete paths depend on.
16test.provider(
17 "describeClusters on a nonexistent cluster fails with ClusterNotFoundFault",
18 () =>
19 Effect.gen(function* () {
20 const error = yield* Effect.flip(
21 memorydb.describeClusters({
22 ClusterName: "alchemy-nonexistent-memorydb-probe",
23 }),
24 );
25 expect(error._tag).toBe("ClusterNotFoundFault");
26 }),
27 { tags: ["provider:aws", "provider:aws:memorydb", "live"] },
28);
29
30// Resolve two default-for-AZ subnets and the default security group.
31const defaultNetwork = Effect.gen(function* () {
32 const vpc = yield* getDefaultVpc;
33 const subnets = yield* EC2.describeSubnets({
34 Filters: [
35 { Name: "vpc-id", Values: [vpc.vpcId] },
36 { Name: "default-for-az", Values: ["true"] },
37 ],
38 });
39 const subnetIds = (subnets.Subnets ?? [])
40 .map((s) => s.SubnetId)
41 .filter((id): id is string => id !== undefined)
42 .sort()
43 .slice(0, 2);
44 const groups = yield* EC2.describeSecurityGroups({
45 Filters: [
46 { Name: "vpc-id", Values: [vpc.vpcId] },
47 { Name: "group-name", Values: ["default"] },
48 ],
49 });
50 const securityGroupId = groups.SecurityGroups?.[0]?.GroupId;
51 if (subnetIds.length < 2 || securityGroupId === undefined) {
52 return yield* Effect.die(
53 new Error("default VPC is missing subnets or its default security group"),
54 );
55 }
56 return { subnetIds, securityGroupIds: [securityGroupId] };
57});
58
59// Deletion is verified as INITIATED (status `deleting`, irreversible) or fully
60// gone. Full disappearance takes several more minutes server-side; waiting for
61// it would push the test into its timeout.
62const assertClusterDeleting = (name: string) =>
63 Effect.gen(function* () {
64 const status = yield* memorydb.describeClusters({ ClusterName: name }).pipe(
65 Effect.map((r) => r.Clusters?.[0]?.Status ?? "gone"),
66 Effect.catchTag("ClusterNotFoundFault", () =>
67 Effect.succeed("gone" as const),
68 ),
69 );
70 if (status !== "gone" && status !== "deleting") {
71 return yield* Effect.fail(
72 new Error(`cluster '${name}' still exists (status: ${status})`),
73 );
74 }
75 }).pipe(
76 Effect.retry({
77 schedule: Schedule.max([
78 Schedule.fixed("10 seconds"),
79 Schedule.recurs(18),
80 ]),
81 }),
82 );
83
84// MemoryDB clusters take ~10-15 minutes to provision and are billed per node
85// while they exist. The full lifecycle is gated behind AWS_TEST_SLOW=1 and
86// always destroys what it created.
87test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
88 "create single-node MemoryDB cluster, verify, destroy",
89 (stack) =>
90 Effect.gen(function* () {
91 yield* stack.destroy();
92
93 const network = yield* defaultNetwork;
94
95 const { cluster } = yield* stack.deploy(
96 Effect.gen(function* () {
97 const user = yield* User("ClusterUser", {
98 authenticationMode: {
99 type: "password",
100 passwords: [Redacted.make("AlchemyMemoryDbTestPass01")],
101 },
102 accessString: "on ~* +@all",
103 });
104 const acl = yield* ACL("ClusterAcl", {
105 userNames: [user.userName],
106 });
107 const subnetGroup = yield* SubnetGroup("ClusterSubnets", {
108 description: "alchemy memorydb cluster subnets",
109 subnetIds: network.subnetIds,
110 });
111 const cluster = yield* Cluster("Cache", {
112 nodeType: "db.t4g.small",
113 engine: "valkey",
114 aclName: acl.aclName,
115 subnetGroupName: subnetGroup.subnetGroupName,
116 securityGroupIds: network.securityGroupIds,
117 numShards: 1,
118 numReplicasPerShard: 0,
119 description: "alchemy memorydb fixture",
120 tags: { fixture: "memorydb-cluster" },
121 });
122 return { cluster };
123 }),
124 );
125
126 expect(cluster.clusterName).toBeDefined();
127 expect(cluster.clusterArn).toContain(":cluster/");
128 expect(cluster.status).toBe("available");
129 expect(cluster.nodeType).toBe("db.t4g.small");
130 expect(cluster.numberOfShards).toBe(1);
131 expect(cluster.endpointAddress).toContain("memorydb");
132
133 // Out-of-band verification via distilled.
134 const described = yield* memorydb.describeClusters({
135 ClusterName: cluster.clusterName,
136 });
137 const observed = described.Clusters?.[0];
138 expect(observed?.Status).toBe("available");
139 expect(observed?.TLSEnabled).toBe(true);
140 expect(observed?.ACLName).toBeDefined();
141
142 // Destroy immediately — clusters bill while they exist — and verify
143 // deletion was initiated out-of-band.
144 yield* stack.destroy();
145 yield* assertClusterDeleting(cluster.clusterName);
146 }),
147 // create (~10-15 min) + delete initiation, one test.
148 {
149 tags: ["provider:aws", "provider:aws:ec2", "provider:aws:memorydb", "live"],
150 timeout: 1_500_000,
151 },
152);