1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import * as servicequotas from "@distilled.cloud/aws/service-quotas";
5import { describe, expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientRequest from "effect/http/HttpClientRequest";
11import ServiceQuotasTestFunctionLive, {
12 ServiceQuotasTestFunction,
13} from "./handler";
14
15const testOptions = { providers: AWS.providers() };
16const { test, beforeAll, afterAll } = Test.make(testOptions);
17const sharedStack = Core.scratchStack(testOptions, "ServiceQuotasBindings");
18
19// Lambda function URL cold-start (DNS, IAM propagation, init) can take well
20// over 60s on a fresh deploy under parallel-suite load.
21const readinessPolicy = Schedule.max([
22 Schedule.fixed("2 seconds"),
23 Schedule.recurs(75),
24]);
25
26let baseUrl: string;
27
28class TransientUpstream extends Data.TaggedError("TransientUpstream")<{
29 readonly status: number;
30 readonly body: string;
31}> {}
32
33// Retry 5xx only — cold re-inits under parallel load surface as transient
34// 500s; a genuine 4xx/assertion failure surfaces immediately.
35const send = (request: HttpClientRequest.HttpClientRequest) =>
36 HttpClient.execute(request).pipe(
37 Effect.flatMap((response) =>
38 response.status >= 500
39 ? response.text.pipe(
40 Effect.flatMap((body) =>
41 Effect.fail(
42 new TransientUpstream({ status: response.status, body }),
43 ),
44 ),
45 )
46 : Effect.succeed(response),
47 ),
48 Effect.retry({
49 while: (e) => e._tag === "TransientUpstream",
50 schedule: Schedule.max([
51 Schedule.exponential("1 second"),
52 Schedule.recurs(5),
53 ]),
54 }),
55 );
56
57describe(
58 "ServiceQuotas Bindings",
59 {
60 tags: [
61 "provider:aws",
62 "provider:aws:lambda",
63 "provider:aws:servicequotas",
64 "live",
65 ],
66 },
67 () => {
68 beforeAll(
69 Effect.gen(function* () {
70 yield* Effect.logInfo(
71 "ServiceQuotas test setup: destroying previous resources",
72 );
73 yield* sharedStack.destroy();
74
75 yield* Effect.logInfo("ServiceQuotas test setup: deploying fixture");
76 const { functionUrl } = yield* sharedStack.deploy(
77 Effect.gen(function* () {
78 return yield* ServiceQuotasTestFunction;
79 }).pipe(Effect.provide(ServiceQuotasTestFunctionLive)),
80 );
81
82 expect(functionUrl).toBeTruthy();
83 baseUrl = functionUrl!.replace(/\/+$/, "");
84 const readinessUrl = `${baseUrl}/ping`;
85
86 yield* Effect.logInfo(
87 `ServiceQuotas test setup: probing readiness at ${readinessUrl}`,
88 );
89 yield* HttpClient.get(readinessUrl).pipe(
90 Effect.flatMap((response) =>
91 response.status === 200
92 ? Effect.succeed(response)
93 : Effect.fail(
94 new Error(`Function not ready: ${response.status}`),
95 ),
96 ),
97 Effect.tapError((error) =>
98 Effect.logWarning(
99 `ServiceQuotas test setup: fixture not ready yet (${String(error)})`,
100 ),
101 ),
102 Effect.retry({ schedule: readinessPolicy }),
103 );
104 }),
105 { timeout: 240_000 },
106 );
107
108 afterAll(sharedStack.destroy(), { timeout: 120_000 });
109
110 describe("GetServiceQuota", () => {
111 test.provider(
112 "surfaces the typed NoSuchResourceException for a bogus quota",
113 (_stack) =>
114 Effect.gen(function* () {
115 const response = yield* send(
116 HttpClientRequest.get(
117 `${baseUrl}/quota?service=vpc&quota=L-00000000`,
118 ),
119 );
120 expect(response.status).toBe(404);
121 const body = (yield* response.json) as { tag: string };
122 expect(body.tag).toBe("NoSuchResourceException");
123 }),
124 { timeout: 120_000 },
125 );
126
127 test.provider(
128 "reads the applied quota value through the deployed Lambda",
129 (_stack) =>
130 Effect.gen(function* () {
131 // Same account/region as the Lambda — establish the expected
132 // behavior out-of-band (some quotas have no applied value and
133 // return the typed not-found).
134 const expected = yield* servicequotas
135 .getServiceQuota({
136 ServiceCode: "lambda",
137 QuotaCode: "L-B99A9384",
138 })
139 .pipe(
140 Effect.map((r) => r.Quota?.Value),
141 Effect.catchTag("NoSuchResourceException", () =>
142 Effect.succeed(undefined),
143 ),
144 );
145
146 const response = yield* send(
147 HttpClientRequest.get(
148 `${baseUrl}/quota?service=lambda&quota=L-B99A9384`,
149 ),
150 );
151 if (expected === undefined) {
152 expect(response.status).toBe(404);
153 const body = (yield* response.json) as { tag: string };
154 expect(body.tag).toBe("NoSuchResourceException");
155 } else {
156 expect(response.status).toBe(200);
157 const body = (yield* response.json) as {
158 quotaCode: string;
159 value: number;
160 };
161 expect(body.quotaCode).toBe("L-B99A9384");
162 expect(body.value).toBe(expected);
163 }
164 }),
165 { timeout: 120_000 },
166 );
167 });
168
169 describe("GetAWSDefaultServiceQuota", () => {
170 test.provider(
171 "reads the AWS default quota value through the deployed Lambda",
172 (_stack) =>
173 Effect.gen(function* () {
174 // Out-of-band expectation from the same account/region.
175 const expected = yield* servicequotas
176 .getAWSDefaultServiceQuota({
177 ServiceCode: "vpc",
178 QuotaCode: "L-F678F1CE",
179 })
180 .pipe(Effect.map((r) => r.Quota?.Value));
181
182 const response = yield* send(
183 HttpClientRequest.get(
184 `${baseUrl}/default-quota?service=vpc&quota=L-F678F1CE`,
185 ),
186 );
187 expect(response.status).toBe(200);
188 const body = (yield* response.json) as {
189 quotaCode: string;
190 value: number;
191 };
192 expect(body.quotaCode).toBe("L-F678F1CE");
193 expect(body.value).toBe(expected);
194 }),
195 { timeout: 120_000 },
196 );
197 });
198
199 describe("ListServices", () => {
200 test.provider(
201 "lists Service Quotas service codes through the deployed Lambda",
202 (_stack) =>
203 Effect.gen(function* () {
204 const response = yield* send(
205 HttpClientRequest.get(`${baseUrl}/services`),
206 );
207 expect(response.status).toBe(200);
208 const body = (yield* response.json) as { serviceCodes: string[] };
209 expect(body.serviceCodes.length).toBeGreaterThan(0);
210 }),
211 { timeout: 120_000 },
212 );
213 });
214
215 describe("ListServiceQuotas", () => {
216 test.provider(
217 "lists a service's applied quotas through the deployed Lambda",
218 (_stack) =>
219 Effect.gen(function* () {
220 const response = yield* send(
221 HttpClientRequest.get(`${baseUrl}/quotas?service=vpc`),
222 );
223 expect(response.status).toBe(200);
224 const body = (yield* response.json) as { quotaCodes: string[] };
225 expect(body.quotaCodes.length).toBeGreaterThan(0);
226 }),
227 { timeout: 120_000 },
228 );
229 });
230
231 describe("ListRequestedServiceQuotaChangeHistoryByQuota", () => {
232 test.provider(
233 "lists a quota's request history through the deployed Lambda",
234 (_stack) =>
235 Effect.gen(function* () {
236 const response = yield* send(
237 HttpClientRequest.get(
238 `${baseUrl}/history?service=vpc&quota=L-F678F1CE`,
239 ),
240 );
241 expect(response.status).toBe(200);
242 const body = (yield* response.json) as { count: number };
243 expect(body.count).toBeGreaterThanOrEqual(0);
244 }),
245 { timeout: 120_000 },
246 );
247 });
248
249 describe("RequestServiceQuotaIncrease", () => {
250 test.provider(
251 "surfaces the typed NoSuchResourceException for a bogus quota (write-path IAM verified, no request submitted)",
252 (_stack) =>
253 Effect.gen(function* () {
254 const response = yield* send(
255 HttpClientRequest.post(
256 `${baseUrl}/request-increase?service=vpc&quota=L-00000000`,
257 ),
258 );
259 expect(response.status).toBe(404);
260 const body = (yield* response.json) as { tag: string };
261 expect(body.tag).toBe("NoSuchResourceException");
262 }),
263 { timeout: 120_000 },
264 );
265 });
266 },
267);