1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as HttpClient from "effect/http/HttpClient";
8import * as HttpClientRequest from "effect/http/HttpClientRequest";
9import NotificationsContactsTestFunctionLive, {
10 NotificationsContactsTestFunction,
11} from "./handler.ts";
12
13const testOptions = { providers: AWS.providers() };
14const { test, beforeAll, afterAll } = Test.make(testOptions);
15const sharedStack = Core.scratchStack(
16 testOptions,
17 "NotificationsContactsBindings",
18);
19
20const readinessPolicy = Schedule.max([
21 Schedule.fixed("2 seconds"),
22 Schedule.recurs(75),
23]);
24
25let baseUrl: string;
26
27describe(
28 "NotificationsContacts Bindings",
29 {
30 tags: [
31 "provider:aws",
32 "provider:aws:lambda",
33 "provider:aws:notificationscontacts",
34 "live",
35 ],
36 },
37 () => {
38 beforeAll(
39 Effect.gen(function* () {
40 yield* Effect.logInfo(
41 "NotificationsContacts test setup: destroying previous resources",
42 );
43 yield* sharedStack.destroy();
44
45 yield* Effect.logInfo(
46 "NotificationsContacts test setup: deploying fixture",
47 );
48 const { functionUrl } = yield* sharedStack.deploy(
49 Effect.gen(function* () {
50 return yield* NotificationsContactsTestFunction;
51 }).pipe(Effect.provide(NotificationsContactsTestFunctionLive)),
52 );
53
54 expect(functionUrl).toBeTruthy();
55 baseUrl = functionUrl!.replace(/\/+$/, "");
56 const readinessUrl = `${baseUrl}/ping`;
57
58 yield* Effect.logInfo(
59 `NotificationsContacts test setup: probing readiness at ${readinessUrl}`,
60 );
61 yield* HttpClient.get(readinessUrl).pipe(
62 Effect.flatMap((response) =>
63 response.status === 200
64 ? Effect.succeed(response)
65 : Effect.fail(
66 new Error(`Function not ready: ${response.status}`),
67 ),
68 ),
69 Effect.tapError((error) =>
70 Effect.logWarning(
71 `NotificationsContacts test setup: fixture not ready yet (${String(error)})`,
72 ),
73 ),
74 Effect.retry({ schedule: readinessPolicy }),
75 );
76 }),
77 { timeout: 240_000 },
78 );
79
80 afterAll(sharedStack.destroy(), { timeout: 120_000 });
81
82 describe("NotificationsContacts.GetEmailContact", () => {
83 test.provider(
84 "reads the bound contact's activation status",
85 (_stack) =>
86 Effect.gen(function* () {
87 const response = (yield* HttpClient.get(`${baseUrl}/contact`).pipe(
88 Effect.flatMap((r) => r.json),
89 )) as { arn: string; status: string };
90
91 expect(response.arn).toContain(":emailcontact/");
92 // Activation is a human email-confirmation loop; a fresh fixture
93 // contact stays `inactive`.
94 expect(["inactive", "active"]).toContain(response.status);
95 }),
96 { timeout: 120_000 },
97 );
98 });
99
100 describe("NotificationsContacts.SendActivationCode", () => {
101 test.provider(
102 "sends the activation email to the bound contact",
103 (_stack) =>
104 Effect.gen(function* () {
105 const response = (yield* HttpClient.execute(
106 HttpClientRequest.post(`${baseUrl}/send-code`),
107 ).pipe(Effect.flatMap((r) => r.json))) as {
108 sent: boolean;
109 conflict: boolean;
110 };
111
112 // A successful send, or the typed ConflictException for an
113 // already-active contact — either outcome proves the binding.
114 expect(response.sent || response.conflict).toBe(true);
115 }),
116 { timeout: 120_000 },
117 );
118 });
119
120 describe("NotificationsContacts.ActivateEmailContact", () => {
121 test.provider(
122 "rejects a bogus activation code with a typed error",
123 (_stack) =>
124 Effect.gen(function* () {
125 const response = (yield* HttpClient.execute(
126 HttpClientRequest.post(`${baseUrl}/activate-bogus`),
127 ).pipe(Effect.flatMap((r) => r.json))) as {
128 activated: boolean;
129 errorTag?: string;
130 };
131
132 // The code from the activation email is a human loop — a bogus
133 // code must surface one of the operation's TYPED error tags,
134 // proving the IAM grant and request wiring end-to-end.
135 expect(response.activated).toBe(false);
136 expect(["ValidationException", "ConflictException"]).toContain(
137 response.errorTag,
138 );
139 }),
140 { timeout: 120_000 },
141 );
142 });
143 },
144);