1import * as AWS from "@/AWS";
2import { AWSEnvironment } from "@/AWS/Environment";
3import * as Output from "@/Output";
4import * as Test from "./Test.ts";
5import { expect } from "alchemy-test";
6import * as Duration from "effect/Duration";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import { TestFunction, TestFunctionLive } from "../Lambda/handler.ts";
11import { assertRestApiDeleted } from "./assertions.ts";
12
13const { test } = Test.make({ providers: AWS.providers() });
14
15test.provider.skipIf(!!process.env.FAST)(
16 "REST API proxies to Lambda (primitives)",
17 (stack) =>
18 Effect.gen(function* () {
19 yield* stack.destroy();
20
21 const { region, accountId } = yield* AWSEnvironment.current;
22
23 const out = yield* stack.deploy(
24 Effect.gen(function* () {
25 const fn = yield* TestFunction.pipe(Effect.provide(TestFunctionLive));
26
27 const api = yield* AWS.ApiGateway.RestApi("AgSmokeApi", {
28 endpointConfiguration: { types: ["REGIONAL"] },
29 });
30
31 const proxyResource = yield* AWS.ApiGateway.Resource("AgSmokeProxy", {
32 restApi: api,
33 parentId: api.rootResourceId,
34 pathPart: "{proxy+}",
35 });
36
37 const invokeUri = Output.map(
38 fn.functionArn,
39 (arn: string) =>
40 `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${arn}/invocations`,
41 );
42
43 yield* AWS.ApiGateway.Method("AgSmokeRootAny", {
44 restApi: api,
45 httpMethod: "ANY",
46 authorizationType: "NONE",
47 integration: {
48 type: "AWS_PROXY",
49 integrationHttpMethod: "POST",
50 uri: invokeUri,
51 },
52 });
53
54 yield* AWS.ApiGateway.Method("AgSmokeProxyAny", {
55 restApi: api,
56 resourceId: proxyResource.resourceId,
57 httpMethod: "ANY",
58 authorizationType: "NONE",
59 integration: {
60 type: "AWS_PROXY",
61 integrationHttpMethod: "POST",
62 uri: invokeUri,
63 },
64 });
65
66 const deployment = yield* AWS.ApiGateway.Deployment("AgSmokeDep", {
67 restApi: api,
68 description: "smoke",
69 });
70
71 const stage = yield* AWS.ApiGateway.Stage("AgSmokeStage", {
72 restApi: api,
73 stageName: "test",
74 deploymentId: deployment.deploymentId,
75 });
76
77 yield* AWS.Lambda.Permission("AgSmokePerm", {
78 action: "lambda:InvokeFunction",
79 functionName: fn.functionName,
80 principal: "apigateway.amazonaws.com",
81 sourceArn: Output.map(
82 api.restApiId,
83 (id: string) =>
84 `arn:aws:execute-api:${region}:${accountId}:${id}/*/*/*`,
85 ),
86 });
87
88 const invokeUrl = Output.map(
89 Output.all(api.restApiId, stage.stageName),
90 ([id, sn]: [string, string]) =>
91 `https://${id}.execute-api.${region}.amazonaws.com/${sn}/`,
92 );
93
94 return { invokeUrl, restApiId: api.restApiId };
95 }),
96 );
97
98 const response = yield* HttpClient.get(out.invokeUrl).pipe(
99 Effect.flatMap((response) =>
100 response.status === 200
101 ? Effect.succeed(response)
102 : Effect.fail(new Error(`invoke URL returned ${response.status}`)),
103 ),
104 Effect.retry({
105 // Stage propagation + Lambda permission can take 30–90s after the
106 // last create. Cap exponential at 10s so we keep polling at a
107 // steady cadence instead of doubling out to multi-minute waits.
108 schedule: Schedule.max([
109 Schedule.exponential(500).pipe(
110 Schedule.modifyDelay(({ duration: d }) =>
111 Effect.succeed(
112 Duration.isGreaterThan(d, Duration.seconds(10))
113 ? Duration.seconds(10)
114 : d,
115 ),
116 ),
117 ),
118 Schedule.recurs(20),
119 ]),
120 }),
121 );
122
123 expect(response.status).toBe(200);
124 expect(yield* response.text).toBe("Hello, world!");
125
126 yield* stack.destroy();
127 yield* assertRestApiDeleted(out.restApiId);
128 }),
129 {
130 tags: [
131 "provider:aws",
132 "provider:aws:apigateway",
133 "provider:aws:lambda",
134 "live",
135 ],
136 timeout: 600_000,
137 },
138);