1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import * as Lambda from "@distilled.cloud/aws/lambda";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import LambdaBindingsTestFunctionLive, {
10 LambdaBindingsTestFunction,
11} from "./bindings-handler.ts";
12
13const testOptions = { providers: AWS.providers() };
14const { test, beforeAll, afterAll } = Test.make(testOptions);
15const sharedStack = Core.scratchStack(testOptions, "LambdaBindings");
16
17let baseUrl: string;
18let deployedFunctionName: string;
19
20// Fresh function URLs take a few seconds to start serving 200s; ride
21// through propagation + cold start with a bounded retry.
22const getJson = (path: string, times = 10) =>
23 HttpClient.get(`${baseUrl}${path}`).pipe(
24 Effect.flatMap((response) =>
25 response.status === 200
26 ? response.json
27 : response.text.pipe(
28 Effect.flatMap((body) =>
29 Effect.fail(
30 new Error(`${path} returned ${response.status}: ${body}`),
31 ),
32 ),
33 ),
34 ),
35 Effect.retry({
36 schedule: Schedule.max([
37 Schedule.exponential("1 second"),
38 Schedule.recurs(times),
39 ]),
40 }),
41 );
42
43describe(
44 "Lambda Bindings",
45 { tags: ["provider:aws", "provider:aws:lambda", "live"] },
46 () => {
47 beforeAll(
48 Effect.gen(function* () {
49 yield* sharedStack.destroy();
50 const { functionName, functionUrl } = yield* sharedStack.deploy(
51 Effect.gen(function* () {
52 return yield* LambdaBindingsTestFunction;
53 }).pipe(Effect.provide(LambdaBindingsTestFunctionLive)),
54 );
55 expect(functionUrl).toBeTruthy();
56 baseUrl = functionUrl!.replace(/\/+$/, "");
57 deployedFunctionName = functionName;
58 }),
59 { timeout: 300_000 },
60 );
61
62 afterAll(
63 Effect.gen(function* () {
64 yield* sharedStack.destroy();
65 // Out-of-band proof the destroy removed the host function. The raw
66 // afterAll context has no providers layer (only `test.provider` bodies
67 // do), so the distilled call must be wrapped in `Core.withProviders`
68 // to satisfy Credentials/HttpClient/Region.
69 if (deployedFunctionName) {
70 yield* Core.withProviders(
71 Lambda.getFunction({
72 FunctionName: deployedFunctionName,
73 }).pipe(
74 Effect.flatMap(() =>
75 Effect.fail(
76 new Error(`Function ${deployedFunctionName} still exists`),
77 ),
78 ),
79 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
80 Effect.retry({
81 schedule: Schedule.max([
82 Schedule.exponential(500),
83 Schedule.recurs(8),
84 ]),
85 }),
86 ),
87 testOptions,
88 "LambdaBindings",
89 );
90 }
91 }),
92 { timeout: 180_000 },
93 );
94
95 test.provider(
96 "InvokeFunction invokes the target function",
97 () =>
98 Effect.gen(function* () {
99 const result = (yield* getJson("/invoke", 30)) as {
100 statusCode: number;
101 };
102 expect(result.statusCode).toBe(200);
103 }),
104 { timeout: 120_000 },
105 );
106
107 test.provider(
108 "InvokeWithResponseStream streams the target's response",
109 () =>
110 Effect.gen(function* () {
111 const result = (yield* getJson("/invoke-stream")) as {
112 statusCode: number;
113 payload: string;
114 complete: boolean;
115 eventShapes: string[][];
116 };
117 expect(result.statusCode).toBe(200);
118 expect(result.complete).toBe(true);
119 expect(
120 result.payload,
121 `event shapes: ${JSON.stringify(result.eventShapes)}`,
122 ).toContain("ok");
123 }),
124 { timeout: 120_000 },
125 );
126
127 test.provider(
128 "GetFunction reads the target's configuration",
129 () =>
130 Effect.gen(function* () {
131 const result = (yield* getJson("/get-function")) as {
132 functionName: string;
133 };
134 expect(result.functionName).toBeTruthy();
135 }),
136 { timeout: 120_000 },
137 );
138
139 test.provider(
140 "GetAccountSettings reads account limits",
141 () =>
142 Effect.gen(function* () {
143 const result = (yield* getJson("/account-settings")) as {
144 concurrentExecutions: number;
145 };
146 expect(result.concurrentExecutions).toBeGreaterThan(0);
147 }),
148 { timeout: 120_000 },
149 );
150
151 test.provider(
152 "ListFunctions lists functions in the region",
153 () =>
154 Effect.gen(function* () {
155 const result = (yield* getJson("/list-functions")) as {
156 count: number;
157 };
158 expect(result.count).toBeGreaterThan(0);
159 }),
160 { timeout: 120_000 },
161 );
162 },
163);