1import * as Lambda from "@/AWS/Lambda";
2import * as MemoryDB from "@/AWS/MemoryDB";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import { HttpServerRequest } from "effect/http/HttpServerRequest";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7import path from "pathe";
8
9const main = path.resolve(import.meta.dirname, "bindings-handler.ts");
10
11export class MemoryDBBindingsTestFunction extends Lambda.Function<Lambda.Function>()(
12 "MemoryDBBindingsTestFunction",
13) {}
14
15/**
16 * Account-level binding fixture: no MemoryDB cluster is ever created (a
17 * cluster takes 10-15 minutes and bills per node). List routes prove each
18 * grant and response decode against real (possibly empty) account data;
19 * probe routes drive name-addressed operations against nonexistent names
20 * (passed by the test as a query parameter) and must surface the service's
21 * typed not-found tag — an IAM gap would surface AccessDeniedException and
22 * fail the route with an opaque 500.
23 *
24 * The cluster-scoped `Connect`, `CreateSnapshot`, and `FailoverShard`
25 * bindings need a live cluster and are exercised only alongside the
26 * AWS_TEST_SLOW-gated cluster lifecycle.
27 */
28export default MemoryDBBindingsTestFunction.make(
29 {
30 main,
31 functionUrl: true,
32 },
33 Effect.gen(function* () {
34 const describeClusters = yield* MemoryDB.DescribeClusters();
35 const describeSnapshots = yield* MemoryDB.DescribeSnapshots();
36 const describeEvents = yield* MemoryDB.DescribeEvents();
37 const deleteSnapshot = yield* MemoryDB.DeleteSnapshot();
38 const copySnapshot = yield* MemoryDB.CopySnapshot();
39 const describeServiceUpdates = yield* MemoryDB.DescribeServiceUpdates();
40 const describeEngineVersions = yield* MemoryDB.DescribeEngineVersions();
41 const batchUpdateCluster = yield* MemoryDB.BatchUpdateCluster();
42
43 const bound = {
44 describeClusters,
45 describeSnapshots,
46 describeEvents,
47 deleteSnapshot,
48 copySnapshot,
49 describeServiceUpdates,
50 describeEngineVersions,
51 batchUpdateCluster,
52 };
53
54 return {
55 fetch: Effect.gen(function* () {
56 const request = yield* HttpServerRequest;
57 const url = new URL(request.originalUrl);
58 const pathname = url.pathname;
59 const name = url.searchParams.get("name") ?? "";
60
61 if (request.method === "GET" && pathname === "/bindings") {
62 return yield* HttpServerResponse.json({
63 bound: Object.keys(bound),
64 });
65 }
66
67 if (request.method === "GET" && pathname === "/clusters") {
68 const result = yield* describeClusters();
69 return yield* HttpServerResponse.json({
70 count: (result.Clusters ?? []).length,
71 });
72 }
73
74 if (request.method === "GET" && pathname === "/cluster-probe") {
75 const tag = yield* describeClusters({ ClusterName: name }).pipe(
76 Effect.map(() => "Found"),
77 Effect.catchTag("ClusterNotFoundFault", (e) =>
78 Effect.succeed(e._tag),
79 ),
80 );
81 return yield* HttpServerResponse.json({ tag });
82 }
83
84 if (request.method === "GET" && pathname === "/snapshots") {
85 const result = yield* describeSnapshots();
86 return yield* HttpServerResponse.json({
87 count: (result.Snapshots ?? []).length,
88 });
89 }
90
91 if (request.method === "GET" && pathname === "/events") {
92 const result = yield* describeEvents({ SourceType: "cluster" });
93 return yield* HttpServerResponse.json({
94 count: (result.Events ?? []).length,
95 });
96 }
97
98 if (request.method === "GET" && pathname === "/delete-probe") {
99 const tag = yield* deleteSnapshot({ SnapshotName: name }).pipe(
100 Effect.map(() => "Deleted"),
101 Effect.catchTag(
102 ["SnapshotNotFoundFault", "ServiceLinkedRoleNotFoundFault"],
103 (e) => Effect.succeed(e._tag),
104 ),
105 );
106 return yield* HttpServerResponse.json({ tag });
107 }
108
109 if (request.method === "GET" && pathname === "/service-updates") {
110 const result = yield* describeServiceUpdates();
111 return yield* HttpServerResponse.json({
112 count: (result.ServiceUpdates ?? []).length,
113 });
114 }
115
116 if (request.method === "GET" && pathname === "/engine-versions") {
117 const result = yield* describeEngineVersions({ Engine: "valkey" });
118 return yield* HttpServerResponse.json({
119 count: (result.EngineVersions ?? []).length,
120 });
121 }
122
123 if (request.method === "GET" && pathname === "/batch-probe") {
124 // A nonexistent service update is rejected with the typed
125 // ServiceUpdateNotFoundFault ("Service Update ... not found") —
126 // proving the grant and the typed union. A call with NO
127 // ServiceUpdate instead fails with the (distilled-patched)
128 // InvalidParameterCombinationException "No modifications were
129 // requested". An IAM gap would surface AccessDeniedException and
130 // 500 the route.
131 const result = yield* batchUpdateCluster({
132 ClusterNames: [name],
133 ServiceUpdate: {
134 ServiceUpdateNameToApply: `${name}-service-update`,
135 },
136 }).pipe(
137 Effect.map((r) => ({
138 unprocessed: (r.UnprocessedClusters ?? []).length,
139 tag: "Ok",
140 })),
141 Effect.catchTag(
142 [
143 "InvalidParameterCombinationException",
144 "InvalidParameterValueException",
145 "ServiceUpdateNotFoundFault",
146 ],
147 (e) => Effect.succeed({ unprocessed: 0, tag: e._tag }),
148 ),
149 );
150 return yield* HttpServerResponse.json(result);
151 }
152
153 if (request.method === "GET" && pathname === "/copy-probe") {
154 const tag = yield* copySnapshot({
155 SourceSnapshotName: name,
156 TargetSnapshotName: `${name}-copy`,
157 }).pipe(
158 Effect.map(() => "Copied"),
159 Effect.catch((e) => Effect.succeed(e._tag)),
160 );
161 return yield* HttpServerResponse.json({ tag });
162 }
163
164 return yield* HttpServerResponse.json(
165 { error: "Not found", method: request.method, pathname },
166 { status: 404 },
167 );
168 }).pipe(Effect.orDie),
169 };
170 }).pipe(
171 Effect.provide(
172 Layer.mergeAll(
173 MemoryDB.DescribeClustersHttp,
174 MemoryDB.DescribeSnapshotsHttp,
175 MemoryDB.DescribeEventsHttp,
176 MemoryDB.DeleteSnapshotHttp,
177 MemoryDB.CopySnapshotHttp,
178 MemoryDB.DescribeServiceUpdatesHttp,
179 MemoryDB.DescribeEngineVersionsHttp,
180 MemoryDB.BatchUpdateClusterHttp,
181 ),
182 ),
183 ),
184);