1import * as Chatbot from "@/AWS/Chatbot";
2import * as Lambda from "@/AWS/Lambda";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import { HttpServerRequest } from "effect/http/HttpServerRequest";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7import path from "pathe";
8
9const main = path.resolve(import.meta.dirname, "handler.ts");
10
11// Well-formed-but-nonexistent identifiers — drive the typed error paths of
12// the identity offboarding bindings without touching any real workspace.
13const NONEXISTENT_SLACK_TEAM = "T0000000000";
14const NONEXISTENT_SLACK_USER = "U0000000000";
15const NONEXISTENT_TEAMS_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
16
17// The delete-identity operations validate the configuration ARN against the
18// caller's account, so the test passes its account id as `?account=` and the
19// fixture builds well-formed same-account (but nonexistent) ARNs.
20const nonexistentSlackConfigurationArn = (account: string) =>
21 `arn:aws:chatbot::${account}:chat-configuration/slack-channel/alchemy-probe-nonexistent`;
22const nonexistentTeamsConfigurationArn = (account: string) =>
23 `arn:aws:chatbot::${account}:chat-configuration/microsoft-teams-channel/alchemy-probe-nonexistent`;
24
25export class ChatbotTestFunction extends Lambda.Function<Lambda.Function>()(
26 "ChatbotTestFunction",
27) {}
28
29/**
30 * Account-level Chatbot bindings fixture.
31 *
32 * The testing account has no Slack workspace or Microsoft Teams team
33 * onboarded (onboarding requires the console OAuth flow), so the reads
34 * answer real empty lists — a REAL data-plane success through each
35 * binding's IAM grant — and the identity deletes answer the typed error
36 * tags for nonexistent identities, proving the wiring and typed error
37 * union for each capability.
38 */
39export default ChatbotTestFunction.make(
40 {
41 main,
42 functionUrl: true,
43 },
44 Effect.gen(function* () {
45 const getAccountPreferences = yield* Chatbot.GetAccountPreferences();
46 const updateAccountPreferences = yield* Chatbot.UpdateAccountPreferences();
47 const describeSlackWorkspaces = yield* Chatbot.DescribeSlackWorkspaces();
48 const describeSlackUserIdentities =
49 yield* Chatbot.DescribeSlackUserIdentities();
50 const deleteSlackUserIdentity = yield* Chatbot.DeleteSlackUserIdentity();
51 const deleteSlackWorkspaceAuthorization =
52 yield* Chatbot.DeleteSlackWorkspaceAuthorization();
53 const listMicrosoftTeamsConfiguredTeams =
54 yield* Chatbot.ListMicrosoftTeamsConfiguredTeams();
55 const listMicrosoftTeamsUserIdentities =
56 yield* Chatbot.ListMicrosoftTeamsUserIdentities();
57 const deleteMicrosoftTeamsUserIdentity =
58 yield* Chatbot.DeleteMicrosoftTeamsUserIdentity();
59 const deleteMicrosoftTeamsConfiguredTeam =
60 yield* Chatbot.DeleteMicrosoftTeamsConfiguredTeam();
61
62 const bound = {
63 getAccountPreferences,
64 updateAccountPreferences,
65 describeSlackWorkspaces,
66 describeSlackUserIdentities,
67 deleteSlackUserIdentity,
68 deleteSlackWorkspaceAuthorization,
69 listMicrosoftTeamsConfiguredTeams,
70 listMicrosoftTeamsUserIdentities,
71 deleteMicrosoftTeamsUserIdentity,
72 deleteMicrosoftTeamsConfiguredTeam,
73 };
74
75 return {
76 fetch: Effect.gen(function* () {
77 const request = yield* HttpServerRequest;
78 const url = new URL(request.originalUrl);
79 const pathname = url.pathname;
80
81 if (request.method === "GET" && pathname === "/bindings") {
82 return yield* HttpServerResponse.json({
83 bound: Object.keys(bound),
84 });
85 }
86
87 if (request.method === "GET" && pathname === "/account-preferences") {
88 // Succeeds on every account — a REAL success through the
89 // binding's IAM grant.
90 const result = yield* getAccountPreferences();
91 return yield* HttpServerResponse.json({
92 ok: true,
93 hasPreferences: result.AccountPreferences !== undefined,
94 });
95 }
96
97 if (
98 request.method === "POST" &&
99 pathname === "/account-preferences-roundtrip"
100 ) {
101 // Read the current preferences, then write the identical values
102 // back — a safe no-op write proving the update binding.
103 const current = yield* getAccountPreferences();
104 const result = yield* updateAccountPreferences({
105 UserAuthorizationRequired:
106 current.AccountPreferences?.UserAuthorizationRequired ?? false,
107 TrainingDataCollectionEnabled:
108 current.AccountPreferences?.TrainingDataCollectionEnabled ??
109 false,
110 }).pipe(
111 Effect.map((r) => ({
112 ok: true as const,
113 hasPreferences: r.AccountPreferences !== undefined,
114 })),
115 Effect.catchTag(
116 ["InvalidParameterException", "InvalidRequestException"],
117 (e) => Effect.succeed({ ok: false as const, tag: e._tag }),
118 ),
119 );
120 return yield* HttpServerResponse.json(result);
121 }
122
123 if (request.method === "GET" && pathname === "/slack-workspaces") {
124 const result = yield* describeSlackWorkspaces();
125 return yield* HttpServerResponse.json({
126 ok: true,
127 count: (result.SlackWorkspaces ?? []).length,
128 });
129 }
130
131 if (request.method === "GET" && pathname === "/slack-user-identities") {
132 const result = yield* describeSlackUserIdentities();
133 return yield* HttpServerResponse.json({
134 ok: true,
135 count: (result.SlackUserIdentities ?? []).length,
136 });
137 }
138
139 if (
140 request.method === "GET" &&
141 pathname === "/teams-configured-teams"
142 ) {
143 const result = yield* listMicrosoftTeamsConfiguredTeams();
144 return yield* HttpServerResponse.json({
145 ok: true,
146 count: (result.ConfiguredTeams ?? []).length,
147 });
148 }
149
150 if (request.method === "GET" && pathname === "/teams-user-identities") {
151 const result = yield* listMicrosoftTeamsUserIdentities();
152 return yield* HttpServerResponse.json({
153 ok: true,
154 count: (result.TeamsUserIdentities ?? []).length,
155 });
156 }
157
158 if (
159 request.method === "POST" &&
160 pathname === "/delete-slack-user-identity"
161 ) {
162 const result = yield* deleteSlackUserIdentity({
163 ChatConfigurationArn: nonexistentSlackConfigurationArn(
164 url.searchParams.get("account") ?? "",
165 ),
166 SlackTeamId: NONEXISTENT_SLACK_TEAM,
167 SlackUserId: NONEXISTENT_SLACK_USER,
168 }).pipe(
169 Effect.map(() => ({ ok: true as const })),
170 Effect.catchTag(
171 [
172 "DeleteSlackUserIdentityException",
173 "InvalidParameterException",
174 "ResourceNotFoundException",
175 ],
176 (e) => Effect.succeed({ ok: false as const, tag: e._tag }),
177 ),
178 );
179 return yield* HttpServerResponse.json(result);
180 }
181
182 if (
183 request.method === "POST" &&
184 pathname === "/delete-slack-workspace-authorization"
185 ) {
186 const result = yield* deleteSlackWorkspaceAuthorization({
187 SlackTeamId: NONEXISTENT_SLACK_TEAM,
188 }).pipe(
189 Effect.map(() => ({ ok: true as const })),
190 Effect.catchTag(
191 [
192 "DeleteSlackWorkspaceAuthorizationFault",
193 "InvalidParameterException",
194 ],
195 (e) => Effect.succeed({ ok: false as const, tag: e._tag }),
196 ),
197 );
198 return yield* HttpServerResponse.json(result);
199 }
200
201 if (
202 request.method === "POST" &&
203 pathname === "/delete-teams-user-identity"
204 ) {
205 const result = yield* deleteMicrosoftTeamsUserIdentity({
206 ChatConfigurationArn: nonexistentTeamsConfigurationArn(
207 url.searchParams.get("account") ?? "",
208 ),
209 UserId: NONEXISTENT_TEAMS_ID,
210 }).pipe(
211 Effect.map(() => ({ ok: true as const })),
212 Effect.catchTag(
213 [
214 "DeleteMicrosoftTeamsUserIdentityException",
215 "InvalidParameterException",
216 "ResourceNotFoundException",
217 ],
218 (e) => Effect.succeed({ ok: false as const, tag: e._tag }),
219 ),
220 );
221 return yield* HttpServerResponse.json(result);
222 }
223
224 if (
225 request.method === "POST" &&
226 pathname === "/delete-teams-configured-team"
227 ) {
228 const result = yield* deleteMicrosoftTeamsConfiguredTeam({
229 TeamId: NONEXISTENT_TEAMS_ID,
230 }).pipe(
231 Effect.map(() => ({ ok: true as const })),
232 // ResourceNotFoundException ("No Team found for the team id")
233 // is outside the Smithy model's union for this operation and is
234 // added via patches/chatbot.json.
235 Effect.catchTag(
236 [
237 "DeleteTeamsConfiguredTeamException",
238 "InvalidParameterException",
239 "ResourceNotFoundException",
240 ],
241 (e) => Effect.succeed({ ok: false as const, tag: e._tag }),
242 ),
243 );
244 return yield* HttpServerResponse.json(result);
245 }
246
247 return yield* HttpServerResponse.json(
248 { error: "Not found", method: request.method, pathname },
249 { status: 404 },
250 );
251 }).pipe(Effect.orDie),
252 };
253 }).pipe(
254 Effect.provide(
255 Layer.mergeAll(
256 Chatbot.GetAccountPreferencesHttp,
257 Chatbot.UpdateAccountPreferencesHttp,
258 Chatbot.DescribeSlackWorkspacesHttp,
259 Chatbot.DescribeSlackUserIdentitiesHttp,
260 Chatbot.DeleteSlackUserIdentityHttp,
261 Chatbot.DeleteSlackWorkspaceAuthorizationHttp,
262 Chatbot.ListMicrosoftTeamsConfiguredTeamsHttp,
263 Chatbot.ListMicrosoftTeamsUserIdentitiesHttp,
264 Chatbot.DeleteMicrosoftTeamsUserIdentityHttp,
265 Chatbot.DeleteMicrosoftTeamsConfiguredTeamHttp,
266 ),
267 ),
268 ),
269);