1import * as Lambda from "@/AWS/Lambda";
2import * as NotificationsContacts from "@/AWS/NotificationsContacts";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Redacted from "effect/Redacted";
7import * as Result from "effect/Result";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import path from "pathe";
11
12const main = path.resolve(import.meta.dirname, "handler.ts");
13
14/** Deterministic identity for the fixture's contact. */
15export const CONTACT_NAME = "alchemy-test-contact-bindings";
16export const CONTACT_EMAIL = "sam+alchemy-test-contact-bindings@alchemy.run";
17
18export class NotificationsContactsTestFunction extends Lambda.Function<Lambda.Function>()(
19 "NotificationsContactsTestFunction",
20) {}
21
22export default NotificationsContactsTestFunction.make(
23 {
24 main,
25 functionUrl: true,
26 timeout: Duration.seconds(30),
27 },
28 Effect.gen(function* () {
29 const contact = yield* NotificationsContacts.EmailContact(
30 "BindingsContact",
31 {
32 name: CONTACT_NAME,
33 emailAddress: CONTACT_EMAIL,
34 tags: { fixture: "notificationscontacts-bindings" },
35 },
36 );
37
38 const getContact = yield* NotificationsContacts.GetEmailContact(contact);
39 const sendActivationCode =
40 yield* NotificationsContacts.SendActivationCode(contact);
41 const activate = yield* NotificationsContacts.ActivateEmailContact(contact);
42
43 return {
44 fetch: Effect.gen(function* () {
45 const request = yield* HttpServerRequest;
46 const pathname = new URL(request.originalUrl).pathname;
47
48 // Cheap readiness route — no AWS call.
49 if (request.method === "GET" && pathname === "/ping") {
50 return yield* HttpServerResponse.json({ ok: true });
51 }
52
53 if (request.method === "GET" && pathname === "/contact") {
54 const result = yield* getContact();
55 // name/address are sensitive — assert on shape/status only.
56 return yield* HttpServerResponse.json({
57 arn: result.emailContact.arn,
58 status: result.emailContact.status,
59 });
60 }
61
62 if (request.method === "POST" && pathname === "/send-code") {
63 const sent = yield* sendActivationCode().pipe(
64 Effect.map(() => ({ sent: true, conflict: false })),
65 // Already-active contacts (or a recent resend) surface as the
66 // typed ConflictException — either outcome proves the binding.
67 Effect.catchTag("ConflictException", () =>
68 Effect.succeed({ sent: false, conflict: true }),
69 ),
70 );
71 return yield* HttpServerResponse.json(sent);
72 }
73
74 if (request.method === "POST" && pathname === "/activate-bogus") {
75 // Activation needs the code from the activation email (a human
76 // loop) — a bogus code must surface a TYPED error, which proves
77 // both the IAM grant and the request wiring.
78 const result = yield* Effect.result(
79 activate({ code: Redacted.make("000000") }),
80 );
81 if (Result.isFailure(result)) {
82 return yield* HttpServerResponse.json({
83 activated: false,
84 errorTag: result.failure._tag,
85 });
86 }
87 return yield* HttpServerResponse.json({ activated: true });
88 }
89
90 return yield* HttpServerResponse.json(
91 { error: "Not found", method: request.method, pathname },
92 { status: 404 },
93 );
94 }).pipe(Effect.orDie),
95 };
96 }).pipe(
97 Effect.provide(
98 Layer.mergeAll(
99 NotificationsContacts.GetEmailContactHttp,
100 NotificationsContacts.SendActivationCodeHttp,
101 NotificationsContacts.ActivateEmailContactHttp,
102 ),
103 ),
104 ),
105);