1import * as AppRunner from "@/AWS/AppRunner";
2import * as Lambda from "@/AWS/Lambda";
3import * as Cause from "effect/Cause";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import path from "pathe";
10
11const main = path.resolve(import.meta.dirname, "handler.ts");
12
13/**
14 * Bindings fixture: a Lambda that manages an App Runner service through the
15 * ListOperations / PauseService / ResumeService / StartDeployment bindings.
16 * The service itself is the cheap public-ECR hello image (no access role, no
17 * Docker build).
18 */
19export class AppRunnerTestFunction extends Lambda.Function<Lambda.Function>()(
20 "AppRunnerTestFunction",
21) {}
22
23export default AppRunnerTestFunction.make(
24 {
25 main,
26 functionUrl: true,
27 // Pause/resume calls fan out SDK calls; AWS's 3s default intermittently
28 // times out on a cold start.
29 timeout: Duration.seconds(30),
30 },
31 Effect.gen(function* () {
32 const service = yield* AppRunner.Service("BindingsService", {
33 serviceName: "alchemy-test-apprunner-bind",
34 imageRepository: {
35 imageIdentifier:
36 "public.ecr.aws/aws-containers/hello-app-runner:latest",
37 imageRepositoryType: "ECR_PUBLIC",
38 port: "8000",
39 },
40 instanceConfiguration: { cpu: "256", memory: "512" },
41 });
42
43 const listOperations = yield* AppRunner.ListOperations(service);
44 const pauseService = yield* AppRunner.PauseService(service);
45 const resumeService = yield* AppRunner.ResumeService(service);
46 const startDeployment = yield* AppRunner.StartDeployment(service);
47 const describeCustomDomains =
48 yield* AppRunner.DescribeCustomDomains(service);
49
50 return {
51 fetch: Effect.gen(function* () {
52 const request = yield* HttpServerRequest;
53 const pathname = new URL(request.originalUrl).pathname;
54
55 if (request.method === "GET" && pathname === "/operations") {
56 const result = yield* listOperations({ MaxResults: 20 });
57 return yield* HttpServerResponse.json({
58 operations: (result.OperationSummaryList ?? []).map((op) => ({
59 id: op.Id,
60 type: op.Type,
61 status: op.Status,
62 })),
63 });
64 }
65
66 if (request.method === "POST" && pathname === "/pause") {
67 const result = yield* pauseService();
68 return yield* HttpServerResponse.json({
69 serviceArn: result.Service.ServiceArn,
70 status: result.Service.Status,
71 operationId: result.OperationId,
72 });
73 }
74
75 if (request.method === "POST" && pathname === "/resume") {
76 const result = yield* resumeService();
77 return yield* HttpServerResponse.json({
78 serviceArn: result.Service.ServiceArn,
79 status: result.Service.Status,
80 operationId: result.OperationId,
81 });
82 }
83
84 if (request.method === "POST" && pathname === "/deploy") {
85 const result = yield* startDeployment();
86 return yield* HttpServerResponse.json({
87 operationId: result.OperationId,
88 });
89 }
90
91 if (request.method === "GET" && pathname === "/custom-domains") {
92 const result = yield* describeCustomDomains();
93 return yield* HttpServerResponse.json({
94 dnsTarget: result.DNSTarget,
95 customDomains: (result.CustomDomains ?? []).map((domain) => ({
96 domainName: domain.DomainName,
97 status: domain.Status,
98 })),
99 });
100 }
101
102 if (request.method === "GET" && pathname === "/ping") {
103 return yield* HttpServerResponse.json({ ok: true });
104 }
105
106 return yield* HttpServerResponse.json(
107 { error: "Not found", method: request.method, pathname },
108 { status: 404 },
109 );
110 }).pipe(
111 // Render the failure into the response instead of dying: a binding
112 // that is rejected (an IAM grant that has not propagated yet, an
113 // InvalidStateException) otherwise reaches the test as an opaque
114 // Lambda 502, and the assertion blows up on a missing field rather
115 // than saying what App Runner actually refused.
116 Effect.catchCause((cause) =>
117 HttpServerResponse.json(
118 { error: Cause.pretty(cause) },
119 { status: 500 },
120 ),
121 ),
122 Effect.orDie,
123 ),
124 };
125 }).pipe(
126 Effect.provide(
127 Layer.mergeAll(
128 AppRunner.DescribeCustomDomainsHttp,
129 AppRunner.ListOperationsHttp,
130 AppRunner.PauseServiceHttp,
131 AppRunner.ResumeServiceHttp,
132 AppRunner.StartDeploymentHttp,
133 ),
134 ),
135 ),
136);