1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import { Region } from "@distilled.cloud/aws/Region";
5import * as sts from "@distilled.cloud/aws/sts";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import NeptuneBindingsTestFunctionLive, {
11 NeptuneBindingsTestFunction,
12} from "./fixtures/handler";
13
14const testOptions = { providers: AWS.providers() };
15const { test, beforeAll, afterAll } = Test.make(testOptions);
16const sharedStack = Core.scratchStack(testOptions, "NeptuneBindings");
17
18// Lambda function URL cold-start (DNS, IAM propagation, init) can take well
19// over 60s on a fresh deploy.
20const readinessPolicy = Schedule.max([
21 Schedule.fixed("2 seconds"),
22 Schedule.recurs(75),
23]);
24
25let baseUrl: string;
26
27const getJson = (path: string) =>
28 HttpClient.get(`${baseUrl}${path}`).pipe(
29 Effect.flatMap((response) =>
30 response.status >= 500
31 ? Effect.fail(new Error(`transient upstream ${response.status}`))
32 : Effect.succeed(response),
33 ),
34 Effect.retry({
35 schedule: Schedule.max([
36 Schedule.exponential("500 millis"),
37 Schedule.recurs(6),
38 ]),
39 }),
40 Effect.flatMap((r) => r.json),
41 );
42
43describe.sequential(
44 "Neptune Bindings",
45 {
46 tags: [
47 "provider:aws",
48 "provider:aws:lambda",
49 "provider:aws:neptune",
50 "live",
51 ],
52 },
53 () => {
54 beforeAll(
55 Effect.gen(function* () {
56 yield* Effect.logInfo(
57 "Neptune test setup: destroying previous resources",
58 );
59 yield* sharedStack.destroy();
60
61 yield* Effect.logInfo("Neptune test setup: deploying fixture");
62 const { functionUrl } = yield* sharedStack.deploy(
63 Effect.gen(function* () {
64 return yield* NeptuneBindingsTestFunction;
65 }).pipe(Effect.provide(NeptuneBindingsTestFunctionLive)),
66 );
67
68 expect(functionUrl).toBeTruthy();
69 baseUrl = functionUrl!.replace(/\/+$/, "");
70
71 const readinessUrl = `${baseUrl}/bindings`;
72 yield* HttpClient.get(readinessUrl).pipe(
73 Effect.flatMap((response) =>
74 response.status === 200
75 ? Effect.succeed(response)
76 : Effect.fail(
77 new Error(`Function not ready: ${response.status}`),
78 ),
79 ),
80 Effect.retry({ schedule: readinessPolicy }),
81 );
82 }),
83 { timeout: 240_000 },
84 );
85
86 afterAll(sharedStack.destroy(), { timeout: 120_000 });
87
88 describe("binding registration", () => {
89 test.provider("all capabilities initialize in the runtime", (_stack) =>
90 Effect.gen(function* () {
91 const response = yield* getJson("/bindings");
92 expect((response as any).bound).toHaveLength(9);
93 }),
94 );
95 });
96
97 describe("DescribeDBClusters", () => {
98 test.provider(
99 "surfaces the typed not-found tag for a nonexistent cluster",
100 (_stack) =>
101 Effect.gen(function* () {
102 const response = yield* getJson("/clusters");
103 expect((response as any).tag).toBe("DBClusterNotFoundFault");
104 }),
105 );
106 });
107
108 describe("DescribeDBInstances", () => {
109 test.provider(
110 "surfaces the typed not-found tag for a nonexistent instance",
111 (_stack) =>
112 Effect.gen(function* () {
113 const response = yield* getJson("/instances");
114 expect((response as any).tag).toBe("DBInstanceNotFoundFault");
115 }),
116 );
117 });
118
119 describe("DescribeEvents", () => {
120 test.provider("lists the account's recent Neptune events", (_stack) =>
121 Effect.gen(function* () {
122 const response = yield* getJson("/events");
123 expect((response as any).count).toBeGreaterThanOrEqual(0);
124 }),
125 );
126 });
127
128 describe("DescribeDBClusterSnapshots", () => {
129 test.provider("lists the account's cluster snapshots", (_stack) =>
130 Effect.gen(function* () {
131 const response = yield* getJson("/snapshots");
132 expect((response as any).count).toBeGreaterThanOrEqual(0);
133 }),
134 );
135 });
136
137 describe("DescribeDBClusterEndpoints", () => {
138 test.provider("lists the account's cluster endpoints", (_stack) =>
139 Effect.gen(function* () {
140 const response = yield* getJson("/endpoints");
141 expect((response as any).count).toBeGreaterThanOrEqual(0);
142 }),
143 );
144 });
145
146 describe("DeleteDBClusterSnapshot", () => {
147 test.provider(
148 "surfaces the typed not-found tag for a nonexistent snapshot",
149 (_stack) =>
150 Effect.gen(function* () {
151 const response = yield* getJson("/delete-snapshot-probe");
152 expect((response as any).tag).toBe(
153 "DBClusterSnapshotNotFoundFault",
154 );
155 }),
156 );
157 });
158
159 describe("CopyDBClusterSnapshot", () => {
160 test.provider(
161 "surfaces the typed not-found tag for a nonexistent source snapshot",
162 (_stack) =>
163 Effect.gen(function* () {
164 const response = yield* getJson("/copy-snapshot-probe");
165 expect((response as any).tag).toBe(
166 "DBClusterSnapshotNotFoundFault",
167 );
168 }),
169 );
170 });
171
172 describe("DescribePendingMaintenanceActions", () => {
173 test.provider(
174 "lists the account's pending maintenance actions",
175 (_stack) =>
176 Effect.gen(function* () {
177 const response = yield* getJson("/pending-maintenance");
178 expect((response as any).count).toBeGreaterThanOrEqual(0);
179 }),
180 );
181 });
182
183 describe("ApplyPendingMaintenanceAction", () => {
184 test.provider(
185 "surfaces the typed not-found tag for a nonexistent resource ARN",
186 (_stack) =>
187 Effect.gen(function* () {
188 // Build a well-formed cluster ARN in this account/region that
189 // cannot exist — the apply call must decode ResourceNotFoundFault.
190 // (`Region`'s service value is itself an Effect — resolve twice.)
191 const region = yield* yield* Region;
192 const identity = yield* sts.getCallerIdentity({});
193 const arn = `arn:aws:rds:${region}:${identity.Account}:cluster:alchemy-nonexistent-neptune-probe`;
194 const response = yield* getJson(
195 `/apply-probe?arn=${encodeURIComponent(arn)}`,
196 );
197 expect((response as any).tag).toBe("ResourceNotFoundFault");
198 }),
199 );
200 });
201 },
202);