1import * as AWS from "@/AWS";
2import { deleteRestApiAndWait } from "@/AWS/ApiGateway/common.ts";
3import * as Provider from "@/Provider";
4import * as Test from "./Test.ts";
5import * as ag from "@distilled.cloud/aws/api-gateway";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Stream from "effect/Stream";
9import { assertRestApiDeleted } from "./assertions.ts";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// `deleteRestApi` is governed by a hard account-wide throttle (~1 request per
14// 30s). During a fleet-wide test wave the teardown of this suite can race
15// other suites' REST API deletes; if the vitest process is killed before the
16// provider's throttle retry wins, the REST API (and its ~21 auto-created
17// GatewayResponses) is stranded. The physical name is deterministic
18// (`{stack}-{logicalId}-{stage}-{suffix}`), so reap out-of-band by the
19// `-{logicalId}-test-` marker: run as a pre-clean before deploy (covers
20// strays from a previous killed run even when the state store was wiped) and
21// as an `Effect.ensuring` finalizer (covers mid-test failures).
22const reapRestApis = (logicalId: string) =>
23 ag.getRestApis.pages({}).pipe(
24 Stream.runCollect,
25 Effect.map((chunk) =>
26 Array.from(chunk).flatMap((page) =>
27 (page.items ?? []).filter(
28 (api): api is ag.RestApi & { id: string } =>
29 api.id != null &&
30 (api.name?.includes(`-${logicalId}-test-`) ?? false),
31 ),
32 ),
33 ),
34 Effect.flatMap(Effect.forEach((api) => deleteRestApiAndWait(api.id))),
35 Effect.asVoid,
36 // Finalizer contract: the error channel must be `never`. Any unexpected
37 // error here is a genuinely stuck delete and should surface as a defect.
38 Effect.orDie,
39 );
40
41test.provider.skipIf(!!process.env.FAST)(
42 "create and delete deployment",
43 (stack) =>
44 Effect.gen(function* () {
45 yield* stack.destroy();
46 yield* reapRestApis("AgDepApi");
47
48 const { api, deployment } = yield* stack.deploy(
49 Effect.gen(function* () {
50 const api = yield* AWS.ApiGateway.RestApi("AgDepApi", {
51 endpointConfiguration: { types: ["REGIONAL"] },
52 });
53 yield* AWS.ApiGateway.Method("AgDepMock", {
54 restApi: api,
55 httpMethod: "GET",
56 authorizationType: "NONE",
57 integration: { type: "MOCK" },
58 });
59 const deployment = yield* AWS.ApiGateway.Deployment("AgDep", {
60 restApi: api,
61 description: "alchemy-test-deployment",
62 });
63 return { api, deployment };
64 }),
65 );
66
67 expect(deployment.deploymentId).toBeDefined();
68
69 yield* stack.destroy();
70 yield* assertRestApiDeleted(api.restApiId);
71 }).pipe(Effect.ensuring(reapRestApis("AgDepApi"))),
72 { tags: ["provider:aws", "provider:aws:apigateway", "live"] },
73);
74
75test.provider.skipIf(!!process.env.FAST)(
76 "deployment trigger change creates new deployment",
77 (stack) =>
78 Effect.gen(function* () {
79 yield* stack.destroy();
80 yield* reapRestApis("AgTrigApi");
81
82 const { api, d1 } = yield* stack.deploy(
83 Effect.gen(function* () {
84 const api = yield* AWS.ApiGateway.RestApi("AgTrigApi", {
85 endpointConfiguration: { types: ["REGIONAL"] },
86 });
87 yield* AWS.ApiGateway.Method("AgTrigMock", {
88 restApi: api,
89 httpMethod: "GET",
90 authorizationType: "NONE",
91 integration: { type: "MOCK" },
92 });
93 const deployment = yield* AWS.ApiGateway.Deployment("AgTrigDep", {
94 restApi: api,
95 description: "v1",
96 triggers: { t: "a" },
97 });
98 return { api, d1: deployment };
99 }),
100 );
101
102 const { d2 } = yield* stack.deploy(
103 Effect.gen(function* () {
104 const api = yield* AWS.ApiGateway.RestApi("AgTrigApi", {
105 endpointConfiguration: { types: ["REGIONAL"] },
106 });
107 yield* AWS.ApiGateway.Method("AgTrigMock", {
108 restApi: api,
109 httpMethod: "GET",
110 authorizationType: "NONE",
111 integration: { type: "MOCK" },
112 });
113 const deployment = yield* AWS.ApiGateway.Deployment("AgTrigDep", {
114 restApi: api,
115 description: "v1",
116 triggers: { t: "b" },
117 });
118 return { d2: deployment };
119 }),
120 );
121
122 expect(d2.deploymentId).not.toEqual(d1.deploymentId);
123
124 yield* stack.destroy();
125 yield* assertRestApiDeleted(api.restApiId);
126 }).pipe(Effect.ensuring(reapRestApis("AgTrigApi"))),
127 { tags: ["provider:aws", "provider:aws:apigateway", "live"] },
128);
129
130test.provider.skipIf(!!process.env.FAST)(
131 "list enumerates the deployed deployment",
132 (stack) =>
133 Effect.gen(function* () {
134 yield* stack.destroy();
135 yield* reapRestApis("AgListApi");
136
137 const { api, deployment } = yield* stack.deploy(
138 Effect.gen(function* () {
139 const api = yield* AWS.ApiGateway.RestApi("AgListApi", {
140 endpointConfiguration: { types: ["REGIONAL"] },
141 });
142 yield* AWS.ApiGateway.Method("AgListMock", {
143 restApi: api,
144 httpMethod: "GET",
145 authorizationType: "NONE",
146 integration: { type: "MOCK" },
147 });
148 const deployment = yield* AWS.ApiGateway.Deployment("AgListDep", {
149 restApi: api,
150 description: "alchemy-test-list-deployment",
151 });
152 return { api, deployment };
153 }),
154 );
155
156 const provider = yield* Provider.findProvider(
157 AWS.ApiGateway.DeploymentResource,
158 );
159 const all = yield* provider.list();
160
161 expect(all.some((d) => d.deploymentId === deployment.deploymentId)).toBe(
162 true,
163 );
164
165 yield* stack.destroy();
166 yield* assertRestApiDeleted(api.restApiId);
167 }).pipe(Effect.ensuring(reapRestApis("AgListApi"))),
168 { tags: ["provider:aws", "provider:aws:apigateway", "live"] },
169);