1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import { expect } from "alchemy-test";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as HttpClient from "effect/http/HttpClient";
8import * as HttpClientRequest from "effect/http/HttpClientRequest";
9
10import ApiGatewayV2BindingsFunctionLive, {
11 ApiGatewayV2BindingsFunction,
12} from "./bindings-handler.ts";
13
14const { test } = Test.make({ providers: AWS.providers() });
15
16// Fresh Lambda function URLs return transient errors for the first
17// 30–90s (DNS, IAM propagation, cold init). Poll at a steady cadence.
18const readinessSchedule = Schedule.max([
19 Schedule.exponential(500).pipe(
20 Schedule.modifyDelay(({ duration: d }) =>
21 Effect.succeed(
22 Duration.isGreaterThan(d, Duration.seconds(10))
23 ? Duration.seconds(10)
24 : d,
25 ),
26 ),
27 ),
28 Schedule.recurs(20),
29]);
30
31const getJson = (url: string) =>
32 HttpClient.get(url).pipe(
33 Effect.flatMap((response) =>
34 response.status === 200
35 ? response.json
36 : Effect.fail(new Error(`GET ${url} returned ${response.status}`)),
37 ),
38 );
39
40const postJson = (url: string) =>
41 HttpClient.execute(HttpClientRequest.post(url)).pipe(
42 Effect.flatMap((response) =>
43 response.status === 200
44 ? response.json
45 : Effect.fail(new Error(`POST ${url} returned ${response.status}`)),
46 ),
47 );
48
49test.provider.skipIf(!!process.env.FAST)(
50 "ApiGatewayV2 bindings export the API and reset authorizer caches from a Lambda",
51 (stack) =>
52 Effect.gen(function* () {
53 yield* stack.destroy();
54
55 const { functionUrl } = yield* stack.deploy(
56 Effect.gen(function* () {
57 return yield* ApiGatewayV2BindingsFunction;
58 }).pipe(Effect.provide(ApiGatewayV2BindingsFunctionLive)),
59 );
60 expect(functionUrl).toBeTruthy();
61 const baseUrl = functionUrl!.replace(/\/+$/, "");
62
63 // ExportApi — the live OpenAPI 3.0 document includes the fixture's
64 // route (retried through cold start).
65 const exported = (yield* getJson(`${baseUrl}/export`).pipe(
66 Effect.retry({ schedule: readinessSchedule }),
67 )) as { openapi?: string; paths: string[] };
68 expect(exported.openapi ?? "").toContain("3.0");
69 expect(exported.paths).toContain("/ping");
70
71 // ResetAuthorizersCache — the stage has no authorizer cache, so the
72 // API answers 200 (or a typed NotFound the route reports as
73 // flushed:false). A missing IAM grant would 500 the route instead.
74 const reset = (yield* postJson(`${baseUrl}/reset-authorizers`)) as {
75 flushed: boolean;
76 };
77 expect(typeof reset.flushed).toBe("boolean");
78
79 yield* stack.destroy();
80 }),
81 {
82 tags: [
83 "provider:aws",
84 "provider:aws:apigatewayv2",
85 "provider:aws:lambda",
86 "live",
87 ],
88 timeout: 600_000,
89 },
90);