1import { Credentials } from "@distilled.cloud/aws/Credentials";
2import { sign } from "@distilled.cloud/aws/SigV4";
3import * as dsql from "@distilled.cloud/aws/dsql";
4import * as iam from "@distilled.cloud/aws/iam";
5import * as Alchemy from "alchemy";
6import * as AWS from "alchemy/AWS";
7import * as Test from "alchemy/Test/Bun";
8import * as Core from "alchemy/Test/Core";
9import { describe, expect } from "bun:test";
10import * as Effect from "effect/Effect";
11import * as Redacted from "effect/Redacted";
12import * as HttpClient from "effect/http/HttpClient";
13import * as HttpClientRequest from "effect/http/HttpClientRequest";
14import Stack from "../alchemy.run.ts";
15
16describe("Drizzle + Aurora DSQL", () => {
17 const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
18 providers: AWS.providers(),
19 state: Alchemy.localState(),
20 profile: process.env.ALCHEMY_PROFILE,
21 });
22 let clusterId: string | undefined;
23 const stack = beforeAll(
24 Effect.gen(function* () {
25 yield* destroy(Stack);
26 const outputs = yield* deploy(Stack);
27 clusterId = outputs.clusterId;
28 yield* deploy(Stack);
29 return outputs;
30 }),
31 { timeout: 600_000 },
32 );
33 afterAll(
34 Effect.gen(function* () {
35 yield* destroy(Stack);
36 if (clusterId) {
37 const status = yield* Core.withProviders(
38 dsql.getCluster({ identifier: clusterId }).pipe(
39 Effect.map((cluster) => cluster.status),
40 Effect.catchTag("ResourceNotFoundException", () =>
41 Effect.succeed("NOT_FOUND"),
42 ),
43 ),
44 { providers: AWS.providers() },
45 "aws-dsql-drizzle",
46 );
47 expect(["DELETING", "DELETED", "NOT_FOUND"]).toContain(status);
48 }
49 }),
50 { timeout: 600_000 },
51 );
52
53 test.provider(
54 "signed HTTP CRUD with a non-admin database role and verified TLS",
55 () =>
56 Effect.gen(function* () {
57 const { url, roleName } = yield* stack;
58 if (!url) return yield* Effect.fail(new Error("Missing Function URL"));
59 const policies = yield* iam.listRolePolicies({ RoleName: roleName });
60 const documents = yield* Effect.forEach(
61 policies.PolicyNames,
62 (PolicyName) =>
63 iam
64 .getRolePolicy({ RoleName: roleName, PolicyName })
65 .pipe(
66 Effect.map((policy) =>
67 decodeURIComponent(policy.PolicyDocument),
68 ),
69 ),
70 );
71 expect(documents.join("\n")).toContain('"dsql:DbConnect"');
72 expect(documents.join("\n")).not.toContain("DbConnectAdmin");
73
74 const client = yield* HttpClient.HttpClient;
75 const getCredentials = yield* Credentials;
76 expect((yield* client.get(`${url}health`)).status).toBe(403);
77 const request = Effect.fn(function* (
78 method: "GET" | "POST" | "PATCH" | "DELETE",
79 path: string,
80 body?: object,
81 ) {
82 const credentials = yield* getCredentials;
83 const payload = body === undefined ? undefined : JSON.stringify(body);
84 const signed = yield* sign({
85 method,
86 url: new URL(path, url).toString(),
87 body: payload,
88 service: "lambda",
89 region: credentials.region,
90 accessKeyId: Redacted.value(credentials.accessKeyId),
91 secretAccessKey: credentials.secretAccessKey,
92 sessionToken: credentials.sessionToken,
93 });
94 const base = HttpClientRequest.make(method)(signed.url).pipe(
95 HttpClientRequest.setHeaders(signed.headers),
96 );
97 const response = yield* client.execute(
98 payload === undefined
99 ? base
100 : base.pipe(
101 HttpClientRequest.bodyText(payload, "application/json"),
102 ),
103 );
104 return { status: response.status, body: yield* response.text };
105 });
106 const health = yield* request("GET", "/health");
107 expect(health.status).toBe(200);
108 expect(JSON.parse(health.body).rows).toEqual([
109 { username: "app_user" },
110 ]);
111 const id = "aaaaaaaa-0000-4000-8000-000000000001";
112 yield* request("DELETE", `/todos/${id}`);
113 expect((yield* request("GET", "/todos")).body).toBe("[]");
114 const created = yield* request("POST", "/todos", {
115 id,
116 text: "Ship DSQL guide",
117 });
118 expect(created.status).toBe(201);
119 expect(JSON.parse(created.body)[0]).toMatchObject({
120 id,
121 text: "Ship DSQL guide",
122 done: false,
123 });
124 const updated = yield* request("PATCH", `/todos/${id}`, { done: true });
125 expect(updated.status).toBe(200);
126 expect(JSON.parse(updated.body)[0].done).toBe(true);
127 for (let i = 0; i < 3; i++) {
128 const listed = yield* request("GET", "/todos");
129 expect(listed.status).toBe(200);
130 expect(JSON.parse(listed.body)).toHaveLength(1);
131 expect(JSON.parse(listed.body)[0]).toMatchObject({
132 id,
133 text: "Ship DSQL guide",
134 done: true,
135 });
136 }
137 expect(
138 (yield* request("POST", "/todos", { id: "invalid", text: "" }))
139 .status,
140 ).toBe(400);
141 expect((yield* request("GET", "/setup")).status).toBe(404);
142 expect((yield* request("DELETE", `/todos/${id}`)).status).toBe(204);
143 expect((yield* request("GET", "/todos")).body).toBe("[]");
144 }),
145 { timeout: 120_000 },
146 );
147});