1import * as LicenseManager from "@/AWS/LicenseManager";
2import * as Lambda from "@/AWS/Lambda";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "handler.ts");
11
12// Well-formed-but-nonexistent identifiers — drive the typed error paths,
13// proving the account-level grants reach the API (an IAM gap would surface
14// AccessDeniedException and 500 the route instead).
15const BOGUS_LICENSE_ARN =
16 "arn:aws:license-manager::111111111111:license:l-00000000000000000000000000000000";
17const BOGUS_GRANT_ARN =
18 "arn:aws:license-manager::111111111111:grant:g-00000000000000000000000000000000";
19const BOGUS_RESOURCE_ARN = "arn:aws:ec2:us-east-1::image/ami-00000000000000000";
20
21export class LicenseManagerTestFunction extends Lambda.Function<Lambda.Function>()(
22 "LicenseManagerTestFunction",
23) {}
24
25export default LicenseManagerTestFunction.make(
26 {
27 main,
28 functionUrl: true,
29 timeout: Duration.seconds(30),
30 },
31 Effect.gen(function* () {
32 const licenses = yield* LicenseManager.LicenseConfiguration(
33 "BindingsLicenses",
34 {
35 licenseCountingType: "vCPU",
36 licenseCount: 5,
37 description: "alchemy license-manager bindings fixture",
38 },
39 );
40
41 // --- configuration-scoped bindings ---
42 const getConfiguration =
43 yield* LicenseManager.GetLicenseConfiguration(licenses);
44 const listAssociations =
45 yield* LicenseManager.ListAssociationsForLicenseConfiguration(licenses);
46 const listUsage =
47 yield* LicenseManager.ListUsageForLicenseConfiguration(licenses);
48 const listFailures =
49 yield* LicenseManager.ListFailuresForLicenseConfigurationOperations(
50 licenses,
51 );
52
53 // --- checkout data plane (account-level) ---
54 const checkoutLicense = yield* LicenseManager.CheckoutLicense();
55 const checkInLicense = yield* LicenseManager.CheckInLicense();
56 const checkoutBorrowLicense = yield* LicenseManager.CheckoutBorrowLicense();
57 const extendLicenseConsumption =
58 yield* LicenseManager.ExtendLicenseConsumption();
59 const getAccessToken = yield* LicenseManager.GetAccessToken();
60
61 // --- license/grant reads (account-level) ---
62 const getLicense = yield* LicenseManager.GetLicense();
63 const getLicenseUsage = yield* LicenseManager.GetLicenseUsage();
64 const listLicenses = yield* LicenseManager.ListLicenses();
65 const listLicenseVersions = yield* LicenseManager.ListLicenseVersions();
66 const listReceivedLicenses = yield* LicenseManager.ListReceivedLicenses();
67 const getGrant = yield* LicenseManager.GetGrant();
68 const acceptGrant = yield* LicenseManager.AcceptGrant();
69 const rejectGrant = yield* LicenseManager.RejectGrant();
70 const listReceivedGrants = yield* LicenseManager.ListReceivedGrants();
71 const listDistributedGrants = yield* LicenseManager.ListDistributedGrants();
72
73 // --- inventory + specifications + settings (account-level) ---
74 const listResourceInventory = yield* LicenseManager.ListResourceInventory();
75 const listLicenseSpecificationsForResource =
76 yield* LicenseManager.ListLicenseSpecificationsForResource();
77 const updateLicenseSpecificationsForResource =
78 yield* LicenseManager.UpdateLicenseSpecificationsForResource();
79 const getServiceSettings = yield* LicenseManager.GetServiceSettings();
80
81 const bound = {
82 getConfiguration,
83 listAssociations,
84 listUsage,
85 listFailures,
86 checkoutLicense,
87 checkInLicense,
88 checkoutBorrowLicense,
89 extendLicenseConsumption,
90 getAccessToken,
91 getLicense,
92 getLicenseUsage,
93 listLicenses,
94 listLicenseVersions,
95 listReceivedLicenses,
96 getGrant,
97 acceptGrant,
98 rejectGrant,
99 listReceivedGrants,
100 listDistributedGrants,
101 listResourceInventory,
102 listLicenseSpecificationsForResource,
103 updateLicenseSpecificationsForResource,
104 getServiceSettings,
105 };
106
107 return {
108 fetch: Effect.gen(function* () {
109 const request = yield* HttpServerRequest;
110 const url = new URL(request.originalUrl);
111 const pathname = url.pathname;
112
113 if (request.method === "GET" && pathname === "/bindings") {
114 return yield* HttpServerResponse.json({
115 bound: Object.keys(bound),
116 });
117 }
118
119 if (request.method === "GET" && pathname === "/configuration") {
120 const config = yield* getConfiguration();
121 return yield* HttpServerResponse.json({
122 name: config.Name ?? null,
123 countingType: config.LicenseCountingType ?? null,
124 licenseCount: config.LicenseCount ?? null,
125 });
126 }
127
128 if (request.method === "GET" && pathname === "/associations") {
129 const result = yield* listAssociations();
130 return yield* HttpServerResponse.json({
131 count: (result.LicenseConfigurationAssociations ?? []).length,
132 });
133 }
134
135 if (request.method === "GET" && pathname === "/usage") {
136 const result = yield* listUsage();
137 return yield* HttpServerResponse.json({
138 count: (result.LicenseConfigurationUsageList ?? []).length,
139 });
140 }
141
142 if (request.method === "GET" && pathname === "/failures") {
143 const result = yield* listFailures();
144 return yield* HttpServerResponse.json({
145 count: (result.LicenseOperationFailureList ?? []).length,
146 });
147 }
148
149 if (request.method === "GET" && pathname === "/licenses") {
150 const result = yield* listLicenses();
151 return yield* HttpServerResponse.json({
152 count: (result.Licenses ?? []).length,
153 });
154 }
155
156 if (request.method === "GET" && pathname === "/received-licenses") {
157 const result = yield* listReceivedLicenses();
158 return yield* HttpServerResponse.json({
159 count: (result.Licenses ?? []).length,
160 });
161 }
162
163 if (request.method === "GET" && pathname === "/received-grants") {
164 const result = yield* listReceivedGrants();
165 return yield* HttpServerResponse.json({
166 count: (result.Grants ?? []).length,
167 });
168 }
169
170 if (request.method === "GET" && pathname === "/distributed-grants") {
171 const result = yield* listDistributedGrants();
172 return yield* HttpServerResponse.json({
173 count: (result.Grants ?? []).length,
174 });
175 }
176
177 if (request.method === "GET" && pathname === "/inventory") {
178 // Resource inventory requires Systems Manager inventory; an account
179 // without it rejects with the typed FailedDependencyException —
180 // either outcome proves the binding + grant.
181 const result = yield* listResourceInventory().pipe(
182 Effect.map((r) => ({
183 tag: "Ok",
184 count: (r.ResourceInventoryList ?? []).length,
185 })),
186 Effect.catchTag("FailedDependencyException", (e) =>
187 Effect.succeed({ tag: e._tag, count: 0 }),
188 ),
189 );
190 return yield* HttpServerResponse.json(result);
191 }
192
193 if (request.method === "GET" && pathname === "/service-settings") {
194 const result = yield* getServiceSettings();
195 return yield* HttpServerResponse.json({
196 tag: "Ok",
197 hasSnsTopic: result.SnsTopicArn !== undefined,
198 });
199 }
200
201 if (request.method === "GET" && pathname === "/checkout-invalid") {
202 // A nonexistent product SKU must surface a typed rejection — a
203 // genuine IAM gap surfaces AccessDeniedException and 500s instead.
204 const result = yield* checkoutLicense({
205 ProductSKU: "00000000-0000-0000-0000-000000000000",
206 CheckoutType: "PROVISIONAL",
207 KeyFingerprint: "aws:294406891311:AWS/KeyManagement:v1",
208 Entitlements: [{ Name: "seats", Value: "1", Unit: "Count" }],
209 ClientToken: "00000000-0000-4000-8000-000000000000",
210 }).pipe(
211 Effect.map(() => "Ok"),
212 Effect.catchTag(
213 [
214 "ResourceNotFoundException",
215 "NoEntitlementsAllowedException",
216 "InvalidParameterValueException",
217 "ValidationException",
218 ],
219 (e) => Effect.succeed(e._tag),
220 ),
221 );
222 return yield* HttpServerResponse.json({ tag: result });
223 }
224
225 if (request.method === "GET" && pathname === "/access-token-invalid") {
226 // A malformed refresh token must surface a typed rejection —
227 // "Invalid token." arrives as InvalidParameterValueException
228 // (verified by probe; patched into distilled's GetAccessToken
229 // union, which the Smithy model omits).
230 const result = yield* getAccessToken({
231 Token: "not-a-valid-refresh-token",
232 }).pipe(
233 Effect.map(() => "Ok"),
234 Effect.catchTag(
235 [
236 "InvalidParameterValueException",
237 "ValidationException",
238 "AuthorizationException",
239 ],
240 (e) => Effect.succeed(e._tag),
241 ),
242 );
243 return yield* HttpServerResponse.json({ tag: result });
244 }
245
246 if (request.method === "GET" && pathname === "/license-invalid") {
247 const result = yield* getLicense({
248 LicenseArn: BOGUS_LICENSE_ARN,
249 }).pipe(
250 Effect.map(() => "Ok"),
251 Effect.catchTag(
252 [
253 "ResourceNotFoundException",
254 "InvalidParameterValueException",
255 "ValidationException",
256 ],
257 (e) => Effect.succeed(e._tag),
258 ),
259 );
260 return yield* HttpServerResponse.json({ tag: result });
261 }
262
263 if (request.method === "GET" && pathname === "/grant-invalid") {
264 const result = yield* getGrant({ GrantArn: BOGUS_GRANT_ARN }).pipe(
265 Effect.map(() => "Ok"),
266 Effect.catchTag(
267 [
268 "ResourceNotFoundException",
269 "InvalidParameterValueException",
270 "ValidationException",
271 ],
272 (e) => Effect.succeed(e._tag),
273 ),
274 );
275 return yield* HttpServerResponse.json({ tag: result });
276 }
277
278 if (
279 request.method === "GET" &&
280 pathname === "/specifications-invalid"
281 ) {
282 const result = yield* listLicenseSpecificationsForResource({
283 ResourceArn: BOGUS_RESOURCE_ARN,
284 }).pipe(
285 Effect.map((r) => ({
286 tag: "Ok",
287 count: (r.LicenseSpecifications ?? []).length,
288 })),
289 Effect.catchTag("InvalidParameterValueException", (e) =>
290 Effect.succeed({ tag: e._tag, count: 0 }),
291 ),
292 );
293 return yield* HttpServerResponse.json(result);
294 }
295
296 return yield* HttpServerResponse.json(
297 { error: "Not found", method: request.method, pathname },
298 { status: 404 },
299 );
300 }).pipe(Effect.orDie),
301 };
302 }).pipe(
303 Effect.provide(
304 Layer.mergeAll(
305 Lambda.EventSource,
306 LicenseManager.GetLicenseConfigurationHttp,
307 LicenseManager.ListAssociationsForLicenseConfigurationHttp,
308 LicenseManager.ListUsageForLicenseConfigurationHttp,
309 LicenseManager.ListFailuresForLicenseConfigurationOperationsHttp,
310 LicenseManager.CheckoutLicenseHttp,
311 LicenseManager.CheckInLicenseHttp,
312 LicenseManager.CheckoutBorrowLicenseHttp,
313 LicenseManager.ExtendLicenseConsumptionHttp,
314 LicenseManager.GetAccessTokenHttp,
315 LicenseManager.GetLicenseHttp,
316 LicenseManager.GetLicenseUsageHttp,
317 LicenseManager.ListLicensesHttp,
318 LicenseManager.ListLicenseVersionsHttp,
319 LicenseManager.ListReceivedLicensesHttp,
320 LicenseManager.GetGrantHttp,
321 LicenseManager.AcceptGrantHttp,
322 LicenseManager.RejectGrantHttp,
323 LicenseManager.ListReceivedGrantsHttp,
324 LicenseManager.ListDistributedGrantsHttp,
325 LicenseManager.ListResourceInventoryHttp,
326 LicenseManager.ListLicenseSpecificationsForResourceHttp,
327 LicenseManager.UpdateLicenseSpecificationsForResourceHttp,
328 LicenseManager.GetServiceSettingsHttp,
329 ),
330 ),
331 ),
332);