1import * as Lambda from "@/AWS/Lambda";
2import * as Notifications from "@/AWS/Notifications";
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/** A Date `days` days in the past (UTC). */
13const daysAgo = (days: number) =>
14 Effect.sync(() => new Date(Date.now() - days * 24 * 3600 * 1000));
15
16/**
17 * Fabricate a well-formed same-account notification-event ARN that cannot
18 * exist — drives the typed not-found path for GetNotificationEvent /
19 * GetManagedNotificationChildEvent, proving the grant reaches the API (an
20 * IAM gap would surface AccessDeniedException instead).
21 */
22const fakeEventArn = (accountId: string) =>
23 `arn:aws:notifications::${accountId}:configuration/a01000000000000000000000000/event/a01000000000000000000000000`;
24
25export class NotificationsTestFunction extends Lambda.Function<Lambda.Function>()(
26 "NotificationsTestFunction",
27) {}
28
29export default NotificationsTestFunction.make(
30 {
31 main,
32 functionUrl: true,
33 timeout: Duration.seconds(30),
34 },
35 Effect.gen(function* () {
36 // A configuration owned by the fixture — the ListChannels binding is
37 // scoped to it (proving ARN injection + the configuration-scoped grant).
38 const config = yield* Notifications.NotificationConfiguration(
39 "BindingsConfig",
40 { description: "notifications bindings fixture" },
41 );
42
43 // --- account-level bindings ---
44 const getNotificationEvent = yield* Notifications.GetNotificationEvent();
45 const listNotificationEvents =
46 yield* Notifications.ListNotificationEvents();
47 const getManagedNotificationEvent =
48 yield* Notifications.GetManagedNotificationEvent();
49 const listManagedNotificationEvents =
50 yield* Notifications.ListManagedNotificationEvents();
51 const getManagedNotificationChildEvent =
52 yield* Notifications.GetManagedNotificationChildEvent();
53 const listManagedNotificationChildEvents =
54 yield* Notifications.ListManagedNotificationChildEvents();
55 const getManagedNotificationConfiguration =
56 yield* Notifications.GetManagedNotificationConfiguration();
57 const listManagedNotificationConfigurations =
58 yield* Notifications.ListManagedNotificationConfigurations();
59 const listManagedNotificationChannelAssociations =
60 yield* Notifications.ListManagedNotificationChannelAssociations();
61
62 // --- configuration-scoped bindings ---
63 const listChannels = yield* Notifications.ListChannels(config);
64
65 const bound = {
66 getNotificationEvent,
67 listNotificationEvents,
68 getManagedNotificationEvent,
69 listManagedNotificationEvents,
70 getManagedNotificationChildEvent,
71 listManagedNotificationChildEvents,
72 getManagedNotificationConfiguration,
73 listManagedNotificationConfigurations,
74 listManagedNotificationChannelAssociations,
75 listChannels,
76 };
77
78 // The first managed configuration ARN doubles as the account-id source
79 // for fabricated ARNs (every account has the AWS Health categories).
80 const firstManagedConfigArn = listManagedNotificationConfigurations().pipe(
81 Effect.map((r) => r.managedNotificationConfigurations[0]?.arn),
82 );
83
84 return {
85 fetch: Effect.gen(function* () {
86 const request = yield* HttpServerRequest;
87 const url = new URL(request.originalUrl);
88 const pathname = url.pathname;
89
90 if (request.method === "GET" && pathname === "/bindings") {
91 return yield* HttpServerResponse.json({
92 bound: Object.keys(bound),
93 });
94 }
95
96 if (request.method === "GET" && pathname === "/events") {
97 const result = yield* listNotificationEvents({
98 startTime: yield* daysAgo(7),
99 });
100 return yield* HttpServerResponse.json({
101 count: result.notificationEvents.length,
102 });
103 }
104
105 if (request.method === "GET" && pathname === "/event-nonexistent") {
106 // Typed not-found path — proves the GetNotificationEvent grant.
107 const managedArn = yield* firstManagedConfigArn;
108 const accountId = managedArn?.split(":")[4] ?? "000000000000";
109 const result = yield* getNotificationEvent({
110 arn: fakeEventArn(accountId),
111 }).pipe(
112 Effect.map(() => "Ok"),
113 Effect.catchTag(
114 ["ResourceNotFoundException", "ValidationException"],
115 (e) => Effect.succeed(e._tag),
116 ),
117 );
118 return yield* HttpServerResponse.json({ tag: result });
119 }
120
121 if (request.method === "GET" && pathname === "/managed-configs") {
122 const result = yield* listManagedNotificationConfigurations();
123 return yield* HttpServerResponse.json({
124 count: result.managedNotificationConfigurations.length,
125 names: result.managedNotificationConfigurations.map((c) => c.name),
126 });
127 }
128
129 if (request.method === "GET" && pathname === "/managed-config") {
130 const arn = yield* firstManagedConfigArn;
131 if (arn === undefined) {
132 return yield* HttpServerResponse.json({ tag: "NoConfigs" });
133 }
134 const result = yield* getManagedNotificationConfiguration({ arn });
135 return yield* HttpServerResponse.json({
136 tag: "Ok",
137 name: result.name,
138 });
139 }
140
141 if (request.method === "GET" && pathname === "/managed-events") {
142 const result = yield* listManagedNotificationEvents({
143 startTime: yield* daysAgo(7),
144 });
145 return yield* HttpServerResponse.json({
146 count: result.managedNotificationEvents.length,
147 });
148 }
149
150 if (request.method === "GET" && pathname === "/managed-event") {
151 const listed = yield* listManagedNotificationEvents({
152 startTime: yield* daysAgo(7),
153 maxResults: 5,
154 });
155 const first = listed.managedNotificationEvents[0];
156 if (first === undefined) {
157 return yield* HttpServerResponse.json({ tag: "NoEvents" });
158 }
159 const result = yield* getManagedNotificationEvent({
160 arn: first.arn,
161 });
162 return yield* HttpServerResponse.json({
163 tag: "Ok",
164 configurationArn: result.managedNotificationConfigurationArn,
165 });
166 }
167
168 if (request.method === "GET" && pathname === "/managed-child-events") {
169 const listed = yield* listManagedNotificationEvents({
170 startTime: yield* daysAgo(7),
171 maxResults: 5,
172 });
173 const first = listed.managedNotificationEvents[0];
174 if (first === undefined) {
175 return yield* HttpServerResponse.json({ tag: "NoEvents" });
176 }
177 // Most managed events are not aggregates — a ValidationException
178 // for a non-aggregate parent still proves the grant + typed union.
179 const result = yield* listManagedNotificationChildEvents({
180 aggregateManagedNotificationEventArn: first.arn,
181 }).pipe(
182 Effect.map((r) => ({
183 tag: "Ok",
184 count: r.managedNotificationChildEvents.length,
185 })),
186 Effect.catchTag(
187 ["ValidationException", "ResourceNotFoundException"],
188 (e) => Effect.succeed({ tag: e._tag, count: 0 }),
189 ),
190 );
191 return yield* HttpServerResponse.json(result);
192 }
193
194 if (
195 request.method === "GET" &&
196 pathname === "/managed-child-event-nonexistent"
197 ) {
198 // Typed not-found path — proves the grant reaches the API.
199 const managedArn = yield* firstManagedConfigArn;
200 const accountId = managedArn?.split(":")[4] ?? "000000000000";
201 const result = yield* getManagedNotificationChildEvent({
202 arn: fakeEventArn(accountId),
203 }).pipe(
204 Effect.map(() => "Ok"),
205 Effect.catchTag(
206 ["ResourceNotFoundException", "ValidationException"],
207 (e) => Effect.succeed(e._tag),
208 ),
209 );
210 return yield* HttpServerResponse.json({ tag: result });
211 }
212
213 if (
214 request.method === "GET" &&
215 pathname === "/managed-channel-associations"
216 ) {
217 const arn = yield* firstManagedConfigArn;
218 if (arn === undefined) {
219 return yield* HttpServerResponse.json({ tag: "NoConfigs" });
220 }
221 const result = yield* listManagedNotificationChannelAssociations({
222 managedNotificationConfigurationArn: arn,
223 });
224 return yield* HttpServerResponse.json({
225 tag: "Ok",
226 count: result.channelAssociations.length,
227 });
228 }
229
230 if (request.method === "GET" && pathname === "/channels") {
231 const result = yield* listChannels();
232 return yield* HttpServerResponse.json({
233 count: result.channels.length,
234 });
235 }
236
237 return yield* HttpServerResponse.json(
238 { error: "Not found", method: request.method, pathname },
239 { status: 404 },
240 );
241 }).pipe(Effect.orDie),
242 };
243 }).pipe(
244 Effect.provide(
245 Layer.mergeAll(
246 Lambda.EventSource,
247 Notifications.GetNotificationEventHttp,
248 Notifications.ListNotificationEventsHttp,
249 Notifications.GetManagedNotificationEventHttp,
250 Notifications.ListManagedNotificationEventsHttp,
251 Notifications.GetManagedNotificationChildEventHttp,
252 Notifications.ListManagedNotificationChildEventsHttp,
253 Notifications.GetManagedNotificationConfigurationHttp,
254 Notifications.ListManagedNotificationConfigurationsHttp,
255 Notifications.ListManagedNotificationChannelAssociationsHttp,
256 Notifications.ListChannelsHttp,
257 ),
258 ),
259 ),
260);