1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import * as iam from "@distilled.cloud/aws/iam";
5import * as licensemanager from "@distilled.cloud/aws/license-manager";
6import { describe, expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10import * as HttpClient from "effect/http/HttpClient";
11import * as HttpClientRequest from "effect/http/HttpClientRequest";
12import LicenseManagerTestFunctionLive, {
13 LicenseManagerTestFunction,
14} from "./handler";
15
16const testOptions = { providers: AWS.providers() };
17const { test, beforeAll, afterAll } = Test.make(testOptions);
18const sharedStack = Core.scratchStack(testOptions, "LicenseManagerBindings");
19const RUN_CREATE_LIFECYCLE =
20 process.env.AWS_TEST_LICENSE_MANAGER_CREATE === "1";
21
22// NOTE: CreateLicenseConfiguration has a small DAILY account quota (~10
23// creates/day; deletes do NOT refund it — soft-deleted configurations still
24// count). This suite's fixture consumes 1 create per run, so keep the whole
25// shared fixture behind the same explicit gate as LicenseConfiguration.test.
26// Otherwise a quota rejection in beforeAll cascades into 16 misleading test
27// failures. Enable with AWS_TEST_LICENSE_MANAGER_CREATE=1 on a fresh quota
28// day to exercise the live binding fixture.
29
30// License Manager requires one-time account onboarding (see
31// LicenseConfiguration.test.ts): create the service-linked role
32// idempotently, then probe with a bounded typed retry through IAM
33// propagation.
34const ensureOnboarded = Effect.gen(function* () {
35 yield* iam
36 .createServiceLinkedRole({
37 AWSServiceName: "license-manager.amazonaws.com",
38 })
39 .pipe(Effect.catchTag("InvalidInputException", () => Effect.void));
40 yield* licensemanager.listLicenseConfigurations({ MaxResults: 1 }).pipe(
41 Effect.retry({
42 while: (e) => e._tag === "AccessDeniedException",
43 schedule: Schedule.exponential("1 second"),
44 times: 8,
45 }),
46 );
47});
48
49// Lambda function URL cold-start (DNS, IAM propagation, init) can take well
50// over 60s on a fresh deploy.
51const readinessPolicy = Schedule.max([
52 Schedule.fixed("2 seconds"),
53 Schedule.recurs(75),
54]);
55
56let baseUrl: string;
57
58class TransientUpstream extends Data.TaggedError("TransientUpstream")<{
59 readonly status: number;
60 readonly body: string;
61}> {}
62
63// The shared Lambda fixture occasionally answers a transient 5xx under load
64// (cold re-init, IAM propagation on the freshly attached policy that the
65// handler's `Effect.orDie` surfaces as a 500). Retry only 5xx; a genuine
66// 4xx/assertion failure surfaces immediately.
67const send = (request: HttpClientRequest.HttpClientRequest) =>
68 HttpClient.execute(request).pipe(
69 Effect.flatMap((response) =>
70 response.status >= 500
71 ? response.text.pipe(
72 Effect.flatMap((body) =>
73 Effect.fail(
74 new TransientUpstream({ status: response.status, body }),
75 ),
76 ),
77 )
78 : Effect.succeed(response),
79 ),
80 Effect.retry({
81 while: (e) => e._tag === "TransientUpstream",
82 schedule: Schedule.max([
83 Schedule.exponential("500 millis"),
84 Schedule.recurs(6),
85 ]),
86 }),
87 );
88
89const getJson = (path: string) =>
90 send(HttpClientRequest.get(`${baseUrl}${path}`)).pipe(
91 Effect.flatMap((r) => r.json),
92 );
93
94const describeCreateLifecycle = describe.skipIf(!RUN_CREATE_LIFECYCLE);
95
96describeCreateLifecycle.sequential("LicenseManager Bindings", () => {
97 beforeAll(
98 Effect.gen(function* () {
99 // beforeAll runs outside the provider context — provide the AWS
100 // client environment explicitly for the raw distilled calls.
101 yield* Core.withProviders(
102 ensureOnboarded,
103 testOptions,
104 "LicenseManagerBindings",
105 );
106
107 yield* Effect.logInfo(
108 "LicenseManager test setup: destroying previous resources",
109 );
110 yield* sharedStack.destroy();
111
112 yield* Effect.logInfo("LicenseManager test setup: deploying fixture");
113 const { functionUrl } = yield* sharedStack.deploy(
114 Effect.gen(function* () {
115 return yield* LicenseManagerTestFunction;
116 }).pipe(Effect.provide(LicenseManagerTestFunctionLive)),
117 );
118
119 expect(functionUrl).toBeTruthy();
120 baseUrl = functionUrl!.replace(/\/+$/, "");
121
122 const readinessUrl = `${baseUrl}/bindings`;
123 yield* Effect.logInfo(
124 `LicenseManager test setup: probing readiness at ${readinessUrl}`,
125 );
126 yield* HttpClient.get(readinessUrl).pipe(
127 Effect.flatMap((response) =>
128 response.status === 200
129 ? Effect.succeed(response)
130 : Effect.fail(new Error(`Function not ready: ${response.status}`)),
131 ),
132 Effect.tapError((error) =>
133 Effect.logWarning(
134 `LicenseManager test setup: fixture not ready yet (${String(error)})`,
135 ),
136 ),
137 Effect.retry({ schedule: readinessPolicy }),
138 );
139 }),
140 { timeout: 240_000 },
141 );
142
143 afterAll(sharedStack.destroy(), { timeout: 120_000 });
144
145 describe(
146 "binding registration",
147 {
148 tags: [
149 "provider:aws",
150 "provider:aws:iam",
151 "provider:aws:lambda",
152 "provider:aws:licensemanager",
153 "live",
154 ],
155 },
156 () => {
157 test.provider("all 23 capabilities initialize in the runtime", (_stack) =>
158 Effect.gen(function* () {
159 const response = yield* getJson("/bindings");
160 expect((response as any).bound).toHaveLength(23);
161 }),
162 );
163 },
164 );
165
166 describe(
167 "GetLicenseConfiguration",
168 {
169 tags: [
170 "provider:aws",
171 "provider:aws:iam",
172 "provider:aws:lambda",
173 "provider:aws:licensemanager",
174 "live",
175 ],
176 },
177 () => {
178 test.provider(
179 "reads the bound configuration (proving ARN injection + the scoped grant)",
180 (_stack) =>
181 Effect.gen(function* () {
182 const response = (yield* getJson("/configuration")) as any;
183 expect(response.countingType).toBe("vCPU");
184 expect(response.licenseCount).toBe(5);
185 expect(response.name).toBeTruthy();
186 }),
187 { timeout: 60_000 },
188 );
189 },
190 );
191
192 describe(
193 "ListAssociationsForLicenseConfiguration",
194 {
195 tags: [
196 "provider:aws",
197 "provider:aws:iam",
198 "provider:aws:lambda",
199 "provider:aws:licensemanager",
200 "live",
201 ],
202 },
203 () => {
204 test.provider(
205 "lists the configuration's associations",
206 (_stack) =>
207 Effect.gen(function* () {
208 const response = (yield* getJson("/associations")) as any;
209 expect(response.count).toBe(0);
210 }),
211 { timeout: 60_000 },
212 );
213 },
214 );
215
216 describe(
217 "ListUsageForLicenseConfiguration",
218 {
219 tags: [
220 "provider:aws",
221 "provider:aws:iam",
222 "provider:aws:lambda",
223 "provider:aws:licensemanager",
224 "live",
225 ],
226 },
227 () => {
228 test.provider(
229 "lists per-resource usage",
230 (_stack) =>
231 Effect.gen(function* () {
232 const response = (yield* getJson("/usage")) as any;
233 expect(response.count).toBe(0);
234 }),
235 { timeout: 60_000 },
236 );
237 },
238 );
239
240 describe(
241 "ListFailuresForLicenseConfigurationOperations",
242 {
243 tags: [
244 "provider:aws",
245 "provider:aws:iam",
246 "provider:aws:lambda",
247 "provider:aws:licensemanager",
248 "live",
249 ],
250 },
251 () => {
252 test.provider(
253 "lists failed license operations",
254 (_stack) =>
255 Effect.gen(function* () {
256 const response = (yield* getJson("/failures")) as any;
257 expect(response.count).toBe(0);
258 }),
259 { timeout: 60_000 },
260 );
261 },
262 );
263
264 describe(
265 "ListLicenses",
266 {
267 tags: [
268 "provider:aws",
269 "provider:aws:iam",
270 "provider:aws:lambda",
271 "provider:aws:licensemanager",
272 "live",
273 ],
274 },
275 () => {
276 test.provider(
277 "lists owned seller licenses",
278 (_stack) =>
279 Effect.gen(function* () {
280 const response = (yield* getJson("/licenses")) as any;
281 expect(response.count).toBeGreaterThanOrEqual(0);
282 }),
283 { timeout: 60_000 },
284 );
285 },
286 );
287
288 describe(
289 "ListReceivedLicenses",
290 {
291 tags: [
292 "provider:aws",
293 "provider:aws:iam",
294 "provider:aws:lambda",
295 "provider:aws:licensemanager",
296 "live",
297 ],
298 },
299 () => {
300 test.provider(
301 "lists received licenses",
302 (_stack) =>
303 Effect.gen(function* () {
304 const response = (yield* getJson("/received-licenses")) as any;
305 expect(response.count).toBeGreaterThanOrEqual(0);
306 }),
307 { timeout: 60_000 },
308 );
309 },
310 );
311
312 describe(
313 "ListReceivedGrants",
314 {
315 tags: [
316 "provider:aws",
317 "provider:aws:iam",
318 "provider:aws:lambda",
319 "provider:aws:licensemanager",
320 "live",
321 ],
322 },
323 () => {
324 test.provider(
325 "lists received grants",
326 (_stack) =>
327 Effect.gen(function* () {
328 const response = (yield* getJson("/received-grants")) as any;
329 expect(response.count).toBeGreaterThanOrEqual(0);
330 }),
331 { timeout: 60_000 },
332 );
333 },
334 );
335
336 describe(
337 "ListDistributedGrants",
338 {
339 tags: [
340 "provider:aws",
341 "provider:aws:iam",
342 "provider:aws:lambda",
343 "provider:aws:licensemanager",
344 "live",
345 ],
346 },
347 () => {
348 test.provider(
349 "lists distributed grants",
350 (_stack) =>
351 Effect.gen(function* () {
352 const response = (yield* getJson("/distributed-grants")) as any;
353 expect(response.count).toBeGreaterThanOrEqual(0);
354 }),
355 { timeout: 60_000 },
356 );
357 },
358 );
359
360 describe(
361 "ListResourceInventory",
362 {
363 tags: [
364 "provider:aws",
365 "provider:aws:iam",
366 "provider:aws:lambda",
367 "provider:aws:licensemanager",
368 "live",
369 ],
370 },
371 () => {
372 test.provider(
373 "lists the resource inventory (or the typed SSM dependency rejection)",
374 (_stack) =>
375 Effect.gen(function* () {
376 const response = (yield* getJson("/inventory")) as any;
377 expect(["Ok", "FailedDependencyException"]).toContain(response.tag);
378 expect(response.count).toBeGreaterThanOrEqual(0);
379 }),
380 { timeout: 60_000 },
381 );
382 },
383 );
384
385 describe(
386 "GetServiceSettings",
387 {
388 tags: [
389 "provider:aws",
390 "provider:aws:iam",
391 "provider:aws:lambda",
392 "provider:aws:licensemanager",
393 "live",
394 ],
395 },
396 () => {
397 test.provider(
398 "reads the account's service settings",
399 (_stack) =>
400 Effect.gen(function* () {
401 const response = (yield* getJson("/service-settings")) as any;
402 expect(response.tag).toBe("Ok");
403 expect(typeof response.hasSnsTopic).toBe("boolean");
404 }),
405 { timeout: 60_000 },
406 );
407 },
408 );
409
410 describe(
411 "CheckoutLicense",
412 {
413 tags: [
414 "provider:aws",
415 "provider:aws:iam",
416 "provider:aws:lambda",
417 "provider:aws:licensemanager",
418 "live",
419 ],
420 },
421 () => {
422 test.provider(
423 "surfaces a typed rejection for a nonexistent product SKU (proving the grant)",
424 (_stack) =>
425 Effect.gen(function* () {
426 const response = (yield* getJson("/checkout-invalid")) as any;
427 expect([
428 "ResourceNotFoundException",
429 "NoEntitlementsAllowedException",
430 "InvalidParameterValueException",
431 "ValidationException",
432 ]).toContain(response.tag);
433 }),
434 { timeout: 60_000 },
435 );
436 },
437 );
438
439 describe(
440 "GetAccessToken",
441 {
442 tags: [
443 "provider:aws",
444 "provider:aws:iam",
445 "provider:aws:lambda",
446 "provider:aws:licensemanager",
447 "live",
448 ],
449 },
450 () => {
451 test.provider(
452 "surfaces a typed rejection for a malformed refresh token (proving the grant)",
453 (_stack) =>
454 Effect.gen(function* () {
455 const response = (yield* getJson("/access-token-invalid")) as any;
456 expect([
457 "InvalidParameterValueException",
458 "ValidationException",
459 "AuthorizationException",
460 ]).toContain(response.tag);
461 }),
462 { timeout: 60_000 },
463 );
464 },
465 );
466
467 describe(
468 "GetLicense",
469 {
470 tags: [
471 "provider:aws",
472 "provider:aws:iam",
473 "provider:aws:lambda",
474 "provider:aws:licensemanager",
475 "live",
476 ],
477 },
478 () => {
479 test.provider(
480 "surfaces a typed rejection for a nonexistent license ARN (proving the grant)",
481 (_stack) =>
482 Effect.gen(function* () {
483 const response = (yield* getJson("/license-invalid")) as any;
484 expect([
485 "ResourceNotFoundException",
486 "InvalidParameterValueException",
487 "ValidationException",
488 ]).toContain(response.tag);
489 }),
490 { timeout: 60_000 },
491 );
492 },
493 );
494
495 describe(
496 "GetGrant",
497 {
498 tags: [
499 "provider:aws",
500 "provider:aws:iam",
501 "provider:aws:lambda",
502 "provider:aws:licensemanager",
503 "live",
504 ],
505 },
506 () => {
507 test.provider(
508 "surfaces a typed rejection for a nonexistent grant ARN (proving the grant)",
509 (_stack) =>
510 Effect.gen(function* () {
511 const response = (yield* getJson("/grant-invalid")) as any;
512 expect([
513 "ResourceNotFoundException",
514 "InvalidParameterValueException",
515 "ValidationException",
516 ]).toContain(response.tag);
517 }),
518 { timeout: 60_000 },
519 );
520 },
521 );
522
523 describe(
524 "ListLicenseSpecificationsForResource",
525 {
526 tags: [
527 "provider:aws",
528 "provider:aws:iam",
529 "provider:aws:lambda",
530 "provider:aws:licensemanager",
531 "live",
532 ],
533 },
534 () => {
535 test.provider(
536 "surfaces an empty list or the typed rejection for a bogus resource ARN",
537 (_stack) =>
538 Effect.gen(function* () {
539 const response = (yield* getJson("/specifications-invalid")) as any;
540 expect(["Ok", "InvalidParameterValueException"]).toContain(
541 response.tag,
542 );
543 }),
544 { timeout: 60_000 },
545 );
546 },
547 );
548});