1/**
2 * LIVE counterpart to test/dev.test.ts: deploys the exact same fixture to
3 * real AWS (Test.make + deploy, no CLI) and drives the same HTTP routes
4 * against the real Function URL. Together the two suites pin that every
5 * binding AND every event-source glue resource (SNS Subscription, Lambda
6 * invoke Permission, stream/queue EventSourceMappings) behaves identically
7 * in `alchemy dev` (floci) and a live deploy.
8 *
9 * Requires AWS credentials (run under `--profile testing` / test:examples).
10 */
11import * as Alchemy from "alchemy";
12import * as AWS from "alchemy/AWS";
13import * as Test from "alchemy/Test/Bun";
14import { expect } from "bun:test";
15import * as Effect from "effect/Effect";
16import * as Schedule from "effect/Schedule";
17import * as HttpClient from "effect/http/HttpClient";
18import * as HttpClientRequest from "effect/http/HttpClientRequest";
19import Stack from "../alchemy.run.ts";
20
21const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
22 providers: AWS.providers(),
23 state: Alchemy.localState(),
24});
25
26const stack = beforeAll(deploy(Stack));
27afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
28
29const api = Effect.gen(function* () {
30 const out = (yield* stack) as { api: string };
31 return out.api;
32});
33
34/** GET a route, retrying while the fresh Function URL warms up. */
35const getJson = Effect.fn(function* (path: string) {
36 const url = yield* api;
37 const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
38 const res = yield* client.get(new URL(path, url).toString()).pipe(
39 Effect.retry({
40 schedule: Schedule.exponential("1 second"),
41 times: 10,
42 }),
43 );
44 return yield* res.json;
45});
46
47/** POST a JSON body to a route, with the same warm-up retry. */
48const postJson = Effect.fn(function* (path: string, body: unknown) {
49 const url = yield* api;
50 const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
51 const res = yield* client
52 .execute(
53 HttpClientRequest.post(new URL(path, url).toString()).pipe(
54 HttpClientRequest.bodyJsonUnsafe(body),
55 ),
56 )
57 .pipe(
58 Effect.retry({
59 schedule: Schedule.exponential("1 second"),
60 times: 10,
61 }),
62 );
63 return yield* res.json;
64});
65
66/** Poll a `{ body }` read route until the async consumer has recorded. */
67const pollBody = (path: string) =>
68 getJson(path).pipe(
69 Effect.map((json) => (json as { body: string | null }).body),
70 Effect.repeat({
71 schedule: Schedule.spaced("2 seconds"),
72 until: (body) => body !== null,
73 times: 90,
74 }),
75 );
76
77test(
78 "serves marker and env through the live Function URL",
79 Effect.gen(function* () {
80 const home = (yield* getJson("/")) as { marker: string; variable: string };
81 expect(home.marker).toBe("aws-dev-marker-v1");
82 expect(home.variable).toBe("my-variable-abc123");
83 }),
84 { timeout: 120_000 },
85);
86
87test(
88 "S3 binding roundtrips",
89 Effect.gen(function* () {
90 const s3 = (yield* getJson("/s3")) as { text: string };
91 expect(s3.text).toBe("hello from s3");
92 }),
93 { timeout: 120_000 },
94);
95
96test(
97 "DynamoDB binding roundtrips",
98 Effect.gen(function* () {
99 const dynamo = (yield* getJson("/dynamo")) as { text: string | null };
100 expect(dynamo.text).toBe("hello from dynamo");
101 }),
102 { timeout: 120_000 },
103);
104
105test(
106 "SQS produce is delivered to the queue consumer",
107 Effect.gen(function* () {
108 const message = { id: crypto.randomUUID(), text: "hello from live sqs" };
109 yield* postJson("/queue/send", message);
110 const body = yield* pollBody(`/queue/messages?id=${message.id}`);
111 expect(JSON.parse(body!)).toEqual(message);
112 }),
113 { timeout: 240_000 },
114);
115
116test(
117 "SNS publish is delivered through the Subscription + Permission glue",
118 Effect.gen(function* () {
119 const notification = {
120 id: crypto.randomUUID(),
121 text: "hello from live sns",
122 };
123 yield* postJson("/topic/send", notification);
124 const body = yield* pollBody(`/topic/messages?id=${notification.id}`);
125 expect(JSON.parse(body!)).toEqual(notification);
126 }),
127 { timeout: 240_000 },
128);
129
130test(
131 "table writes are delivered through the stream EventSourceMapping",
132 Effect.gen(function* () {
133 const itemId = crypto.randomUUID();
134 yield* postJson("/items", { id: itemId });
135 const body = yield* pollBody(`/changes?id=${itemId}`);
136 expect(body).toBe("INSERT");
137 }),
138 { timeout: 240_000 },
139);