1import * as Lambda from "@/AWS/Lambda";
2import * as RAM from "@/AWS/RAM";
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/**
13 * Fabricate a well-formed same-account invitation ARN that cannot exist —
14 * drives the typed not-found path for the invitation bindings, proving the
15 * grant reaches the API (an IAM gap would surface AccessDeniedException
16 * instead).
17 */
18const fakeInvitationArn = (shareArn: string) => {
19 const [, , , region, accountId] = shareArn.split(":");
20 return `arn:aws:ram:${region}:${accountId}:resource-share-invitation/00000000-0000-4000-8000-000000000000`;
21};
22
23/** A well-formed same-account subnet ARN that cannot exist. */
24const fakeSubnetArn = (shareArn: string) => {
25 const [, , , region, accountId] = shareArn.split(":");
26 return `arn:aws:ec2:${region}:${accountId}:subnet/subnet-00000000000000000`;
27};
28
29export class RAMTestFunction extends Lambda.Function<Lambda.Function>()(
30 "RAMTestFunction",
31) {}
32
33export default RAMTestFunction.make(
34 {
35 main,
36 functionUrl: true,
37 timeout: Duration.seconds(30),
38 },
39 Effect.gen(function* () {
40 // A resource share owned by the fixture — guarantees that
41 // GetResourceShares(SELF) is non-empty and provides the region/account
42 // for the fabricated not-found ARNs.
43 yield* RAM.ResourceShare("BindingsShare", {
44 allowExternalPrincipals: true,
45 });
46
47 const acceptInvitation = yield* RAM.AcceptResourceShareInvitation();
48 const rejectInvitation = yield* RAM.RejectResourceShareInvitation();
49 const getInvitations = yield* RAM.GetResourceShareInvitations();
50 const listPendingResources = yield* RAM.ListPendingInvitationResources();
51 const getResourceShares = yield* RAM.GetResourceShares();
52 const getAssociations = yield* RAM.GetResourceShareAssociations();
53 const listResources = yield* RAM.ListResources();
54 const listPrincipals = yield* RAM.ListPrincipals();
55 const getResourcePolicies = yield* RAM.GetResourcePolicies();
56 const getPermission = yield* RAM.GetPermission();
57 const listPermissions = yield* RAM.ListPermissions();
58
59 const bound = {
60 acceptInvitation,
61 rejectInvitation,
62 getInvitations,
63 listPendingResources,
64 getResourceShares,
65 getAssociations,
66 listResources,
67 listPrincipals,
68 getResourcePolicies,
69 getPermission,
70 listPermissions,
71 };
72
73 // The fixture's own share doubles as the region/account source for
74 // fabricated ARNs.
75 const firstShareArn = getResourceShares({ resourceOwner: "SELF" }).pipe(
76 Effect.map((r) => r.resourceShares?.[0]?.resourceShareArn),
77 );
78
79 return {
80 fetch: Effect.gen(function* () {
81 const request = yield* HttpServerRequest;
82 const url = new URL(request.originalUrl);
83 const pathname = url.pathname;
84
85 if (request.method === "GET" && pathname === "/bindings") {
86 return yield* HttpServerResponse.json({
87 bound: Object.keys(bound),
88 });
89 }
90
91 if (request.method === "GET" && pathname === "/shares") {
92 const result = yield* getResourceShares({ resourceOwner: "SELF" });
93 return yield* HttpServerResponse.json({
94 count: (result.resourceShares ?? []).length,
95 });
96 }
97
98 if (request.method === "GET" && pathname === "/associations") {
99 const shareArn = yield* firstShareArn;
100 if (shareArn === undefined) {
101 return yield* HttpServerResponse.json({ tag: "NoShares" });
102 }
103 const result = yield* getAssociations({
104 associationType: "PRINCIPAL",
105 resourceShareArns: [shareArn],
106 });
107 return yield* HttpServerResponse.json({
108 tag: "Ok",
109 count: (result.resourceShareAssociations ?? []).length,
110 });
111 }
112
113 if (request.method === "GET" && pathname === "/invitations") {
114 const result = yield* getInvitations();
115 return yield* HttpServerResponse.json({
116 count: (result.resourceShareInvitations ?? []).length,
117 });
118 }
119
120 if (request.method === "GET" && pathname === "/accept-nonexistent") {
121 const shareArn = yield* firstShareArn;
122 if (shareArn === undefined) {
123 return yield* HttpServerResponse.json({ tag: "NoShares" });
124 }
125 // Typed not-found path — proves the AcceptResourceShareInvitation
126 // grant reaches the API.
127 const result = yield* acceptInvitation({
128 resourceShareInvitationArn: fakeInvitationArn(shareArn),
129 }).pipe(
130 Effect.map(() => "Ok"),
131 Effect.catchTag(
132 [
133 "ResourceShareInvitationArnNotFoundException",
134 "MalformedArnException",
135 ],
136 (e) => Effect.succeed(e._tag),
137 ),
138 );
139 return yield* HttpServerResponse.json({ tag: result });
140 }
141
142 if (request.method === "GET" && pathname === "/reject-nonexistent") {
143 const shareArn = yield* firstShareArn;
144 if (shareArn === undefined) {
145 return yield* HttpServerResponse.json({ tag: "NoShares" });
146 }
147 const result = yield* rejectInvitation({
148 resourceShareInvitationArn: fakeInvitationArn(shareArn),
149 }).pipe(
150 Effect.map(() => "Ok"),
151 Effect.catchTag(
152 [
153 "ResourceShareInvitationArnNotFoundException",
154 "MalformedArnException",
155 ],
156 (e) => Effect.succeed(e._tag),
157 ),
158 );
159 return yield* HttpServerResponse.json({ tag: result });
160 }
161
162 if (
163 request.method === "GET" &&
164 pathname === "/pending-resources-nonexistent"
165 ) {
166 const shareArn = yield* firstShareArn;
167 if (shareArn === undefined) {
168 return yield* HttpServerResponse.json({ tag: "NoShares" });
169 }
170 const result = yield* listPendingResources({
171 resourceShareInvitationArn: fakeInvitationArn(shareArn),
172 }).pipe(
173 Effect.map(() => "Ok"),
174 Effect.catchTag(
175 [
176 "ResourceShareInvitationArnNotFoundException",
177 "MalformedArnException",
178 "InvalidParameterException",
179 ],
180 (e) => Effect.succeed(e._tag),
181 ),
182 );
183 return yield* HttpServerResponse.json({ tag: result });
184 }
185
186 if (request.method === "GET" && pathname === "/resources") {
187 const result = yield* listResources({ resourceOwner: "SELF" });
188 return yield* HttpServerResponse.json({
189 count: (result.resources ?? []).length,
190 });
191 }
192
193 if (request.method === "GET" && pathname === "/principals") {
194 const result = yield* listPrincipals({ resourceOwner: "SELF" });
195 return yield* HttpServerResponse.json({
196 count: (result.principals ?? []).length,
197 });
198 }
199
200 if (
201 request.method === "GET" &&
202 pathname === "/resource-policies-nonexistent"
203 ) {
204 const shareArn = yield* firstShareArn;
205 if (shareArn === undefined) {
206 return yield* HttpServerResponse.json({ tag: "NoShares" });
207 }
208 // A fabricated subnet ARN — either the typed not-found or an empty
209 // policy list proves the grant.
210 const result = yield* getResourcePolicies({
211 resourceArns: [fakeSubnetArn(shareArn)],
212 }).pipe(
213 Effect.map((r) => ({
214 tag: "Ok" as string,
215 count: (r.policies ?? []).length,
216 })),
217 Effect.catchTag(
218 ["ResourceArnNotFoundException", "MalformedArnException"],
219 (e) => Effect.succeed({ tag: e._tag as string, count: 0 }),
220 ),
221 );
222 return yield* HttpServerResponse.json(result);
223 }
224
225 if (request.method === "GET" && pathname === "/permissions") {
226 const result = yield* listPermissions();
227 return yield* HttpServerResponse.json({
228 count: (result.permissions ?? []).length,
229 });
230 }
231
232 if (request.method === "GET" && pathname === "/permission") {
233 const listed = yield* listPermissions({ maxResults: 5 });
234 const first = (listed.permissions ?? [])[0];
235 if (first?.arn === undefined) {
236 return yield* HttpServerResponse.json({ tag: "NoPermissions" });
237 }
238 const result = yield* getPermission({ permissionArn: first.arn });
239 return yield* HttpServerResponse.json({
240 tag: "Ok",
241 name: result.permission?.name,
242 });
243 }
244
245 return yield* HttpServerResponse.json(
246 { error: "Not found", method: request.method, pathname },
247 { status: 404 },
248 );
249 }).pipe(Effect.orDie),
250 };
251 }).pipe(
252 Effect.provide(
253 Layer.mergeAll(
254 Lambda.EventSource,
255 RAM.AcceptResourceShareInvitationHttp,
256 RAM.RejectResourceShareInvitationHttp,
257 RAM.GetResourceShareInvitationsHttp,
258 RAM.ListPendingInvitationResourcesHttp,
259 RAM.GetResourceSharesHttp,
260 RAM.GetResourceShareAssociationsHttp,
261 RAM.ListResourcesHttp,
262 RAM.ListPrincipalsHttp,
263 RAM.GetResourcePoliciesHttp,
264 RAM.GetPermissionHttp,
265 RAM.ListPermissionsHttp,
266 ),
267 ),
268 ),
269);