1import { Credentials } from "@distilled.cloud/aws/Credentials";
2import { sign } from "@distilled.cloud/aws/SigV4";
3import * as Alchemy from "alchemy";
4import * as AWS from "alchemy/AWS";
5import * as Test from "alchemy/Test/Bun";
6import { describe, expect } from "bun:test";
7import * as Effect from "effect/Effect";
8import * as Redacted from "effect/Redacted";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientRequest from "effect/http/HttpClientRequest";
11import Stack from "../alchemy.run.ts";
12
13// Aurora provisioning/deletion is opt-in, as in the RDSData integration suite.
14describe.skipIf(!process.env.AWS_TEST_SLOW)(
15 "Drizzle + Aurora PostgreSQL",
16 () => {
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 const stack = beforeAll(
23 Effect.gen(function* () {
24 yield* destroy(Stack);
25 const outputs = yield* deploy(Stack);
26 yield* deploy(Stack);
27 return outputs;
28 }),
29 { timeout: 1_500_000 },
30 );
31 afterAll(destroy(Stack), { timeout: 1_500_000 });
32
33 test.provider(
34 "CRUD, TLS, least privilege, and repeated invocations",
35 () =>
36 Effect.gen(function* () {
37 const { url } = yield* stack;
38 if (!url)
39 return yield* Effect.fail(new Error("Missing Function URL"));
40 const client = yield* HttpClient.HttpClient;
41 const getCredentials = yield* Credentials;
42 expect((yield* client.get(`${url}health`)).status).toBe(403);
43 const request = Effect.fn(function* (
44 method: "GET" | "POST" | "PATCH" | "DELETE",
45 path: string,
46 body?: object,
47 ) {
48 const credentials = yield* getCredentials;
49 const payload =
50 body === undefined ? undefined : JSON.stringify(body);
51 const signed = yield* sign({
52 method,
53 url: new URL(path, url).toString(),
54 body: payload,
55 service: "lambda",
56 region: credentials.region,
57 accessKeyId: Redacted.value(credentials.accessKeyId),
58 secretAccessKey: credentials.secretAccessKey,
59 sessionToken: credentials.sessionToken,
60 });
61 const base = HttpClientRequest.make(method)(signed.url).pipe(
62 HttpClientRequest.setHeaders(signed.headers),
63 );
64 const response = yield* client.execute(
65 payload === undefined
66 ? base
67 : base.pipe(
68 HttpClientRequest.bodyText(payload, "application/json"),
69 ),
70 );
71 return { statusCode: response.status, body: yield* response.text };
72 });
73 const health = yield* request("GET", "/health");
74 expect(health.statusCode).toBe(200);
75 expect(JSON.parse(health.body).rows).toEqual([
76 {
77 username: "app_iam",
78 tls: true,
79 can_insert: true,
80 can_create: false,
81 },
82 ]);
83 const id = "aaaaaaaa-0000-4000-8000-000000000001";
84 yield* request("DELETE", `/todos/${id}`);
85 expect((yield* request("GET", "/todos")).body).toBe("[]");
86 const created = yield* request("POST", "/todos", {
87 id,
88 title: "Ship Aurora guide",
89 });
90 expect(created.statusCode).toBe(201);
91 expect(JSON.parse(created.body)).toEqual([
92 { id, title: "Ship Aurora guide", done: false },
93 ]);
94 const updated = yield* request("PATCH", `/todos/${id}`, {
95 done: true,
96 });
97 expect(updated.statusCode).toBe(200);
98 expect(JSON.parse(updated.body)[0].done).toBe(true);
99 for (let i = 0; i < 3; i++) {
100 const listed = yield* request("GET", "/todos");
101 expect(listed.statusCode).toBe(200);
102 expect(JSON.parse(listed.body)).toEqual([
103 { id, title: "Ship Aurora guide", done: true },
104 ]);
105 }
106 expect(
107 (yield* request("POST", "/todos", { id: "invalid", title: "" }))
108 .statusCode,
109 ).toBe(400);
110 expect((yield* request("GET", "/setup")).statusCode).toBe(404);
111 expect((yield* request("DELETE", `/todos/${id}`)).statusCode).toBe(
112 204,
113 );
114 expect((yield* request("GET", "/todos")).body).toBe("[]");
115 }),
116 { timeout: 120_000 },
117 );
118 },
119);