Skip to content

Commit 0d3893c

Browse files
committed
add unit tests for sqs handler
1 parent 25f5bf9 commit 0d3893c

File tree

1 file changed

+363
-0
lines changed

1 file changed

+363
-0
lines changed
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import { mockClient } from "aws-sdk-client-mock";
3+
import {
4+
DynamoDBClient,
5+
GetItemCommand,
6+
TransactWriteItemsCommand,
7+
} from "@aws-sdk/client-dynamodb";
8+
import { marshall } from "@aws-sdk/util-dynamodb";
9+
import { createOrgGithubTeamHandler } from "../../../../src/api/sqs/handlers/createOrgGithubTeam.js";
10+
import { genericConfig } from "../../../../src/common/config.js";
11+
import { InternalServerError } from "../../../../src/common/errors/index.js";
12+
import { SKIP_EXTERNAL_ORG_LEAD_UPDATE } from "../../../../src/common/overrides.js";
13+
14+
// Mock dependencies
15+
const ddbMock = mockClient(DynamoDBClient);
16+
17+
vi.mock("../../../../src/api/functions/github.js", () => ({
18+
createGithubTeam: vi.fn(),
19+
assignIdpGroupsToTeam: vi.fn(),
20+
}));
21+
22+
vi.mock("../../../../src/api/utils.js", () => ({
23+
retryDynamoTransactionWithBackoff: vi.fn((operation) => operation()),
24+
}));
25+
26+
vi.mock("ioredis", () => import("ioredis-mock"));
27+
28+
let mockLockAborted = false;
29+
30+
vi.mock("redlock-universal", () => ({
31+
createLock: vi.fn(() => ({
32+
using: vi.fn((callback) => callback({ aborted: mockLockAborted })),
33+
})),
34+
IoredisAdapter: vi.fn(),
35+
}));
36+
37+
import {
38+
createGithubTeam,
39+
assignIdpGroupsToTeam,
40+
} from "../../../../src/api/functions/github.js";
41+
42+
describe("createOrgGithubTeamHandler", () => {
43+
const mockLogger = {
44+
info: vi.fn(),
45+
warn: vi.fn(),
46+
error: vi.fn(),
47+
debug: vi.fn(),
48+
trace: vi.fn(),
49+
fatal: vi.fn(),
50+
};
51+
52+
// Use "Social Committee" - a real org that's NOT in the skip list
53+
const basePayload = {
54+
orgName: "Social Committee",
55+
githubTeamName: "social-leads",
56+
githubTeamDescription: "Social Committee Leadership Team",
57+
};
58+
59+
const baseMetadata = {
60+
reqId: "test-req-123",
61+
initiator: "test@illinois.edu",
62+
};
63+
64+
beforeEach(() => {
65+
vi.clearAllMocks();
66+
ddbMock.reset();
67+
mockLockAborted = false;
68+
});
69+
70+
afterEach(() => {
71+
vi.restoreAllMocks();
72+
});
73+
74+
it("should skip organizations with external updates disabled", async () => {
75+
// Use ACM which IS in the skip list
76+
const disabledOrgPayload = {
77+
...basePayload,
78+
orgName: "ACM",
79+
githubTeamName: "acm-leads",
80+
githubTeamDescription: "ACM Leadership Team",
81+
};
82+
83+
await createOrgGithubTeamHandler(
84+
disabledOrgPayload,
85+
baseMetadata,
86+
mockLogger as any,
87+
);
88+
89+
expect(mockLogger.info).toHaveBeenCalledWith(
90+
expect.stringContaining("external updates disabled"),
91+
);
92+
expect(ddbMock.commandCalls(GetItemCommand)).toHaveLength(0);
93+
});
94+
95+
it("should throw error if org entry not found", async () => {
96+
ddbMock.on(GetItemCommand).resolves({});
97+
98+
await expect(
99+
createOrgGithubTeamHandler(basePayload, baseMetadata, mockLogger as any),
100+
).rejects.toThrow(InternalServerError);
101+
});
102+
103+
it("should skip if org does not have an Entra group", async () => {
104+
ddbMock.on(GetItemCommand).resolves({
105+
Item: marshall({
106+
primaryKey: "DEFINE#Social Committee",
107+
entryId: "0",
108+
}),
109+
});
110+
111+
await createOrgGithubTeamHandler(
112+
basePayload,
113+
baseMetadata,
114+
mockLogger as any,
115+
);
116+
117+
expect(mockLogger.info).toHaveBeenCalledWith(
118+
expect.stringContaining("does not have an Entra group"),
119+
);
120+
expect(createGithubTeam).not.toHaveBeenCalled();
121+
});
122+
123+
it("should skip if org already has a GitHub team", async () => {
124+
ddbMock.on(GetItemCommand).resolves({
125+
Item: marshall({
126+
primaryKey: "DEFINE#Social Committee",
127+
entryId: "0",
128+
leadsEntraGroupId: "entra-group-123",
129+
leadsGithubTeamId: 456,
130+
}),
131+
});
132+
133+
await createOrgGithubTeamHandler(
134+
basePayload,
135+
baseMetadata,
136+
mockLogger as any,
137+
);
138+
139+
expect(mockLogger.info).toHaveBeenCalledWith(
140+
"This org already has a GitHub team, skipping",
141+
);
142+
expect(createGithubTeam).not.toHaveBeenCalled();
143+
});
144+
145+
it("should create GitHub team and store team ID", async () => {
146+
const newTeamId = 789;
147+
148+
ddbMock.on(GetItemCommand).resolves({
149+
Item: marshall({
150+
primaryKey: "DEFINE#Social Committee",
151+
entryId: "0",
152+
leadsEntraGroupId: "entra-group-123",
153+
}),
154+
});
155+
156+
(createGithubTeam as any).mockResolvedValue({
157+
updated: true,
158+
id: newTeamId,
159+
});
160+
161+
ddbMock.on(TransactWriteItemsCommand).resolves({});
162+
163+
await createOrgGithubTeamHandler(
164+
basePayload,
165+
baseMetadata,
166+
mockLogger as any,
167+
);
168+
169+
expect(createGithubTeam).toHaveBeenCalledWith(
170+
expect.objectContaining({
171+
name: expect.stringContaining("social-leads"),
172+
description: "Social Committee Leadership Team",
173+
}),
174+
);
175+
176+
expect(ddbMock.commandCalls(TransactWriteItemsCommand)).toHaveLength(1);
177+
expect(mockLogger.info).toHaveBeenCalledWith(
178+
expect.stringContaining(`created with team ID "${newTeamId}"`),
179+
);
180+
});
181+
182+
it("should handle existing team and skip IDP sync setup", async () => {
183+
const existingTeamId = 999;
184+
185+
ddbMock.on(GetItemCommand).resolves({
186+
Item: marshall({
187+
primaryKey: "DEFINE#Social Committee",
188+
entryId: "0",
189+
leadsEntraGroupId: "entra-group-123",
190+
}),
191+
});
192+
193+
(createGithubTeam as any).mockResolvedValue({
194+
updated: false,
195+
id: existingTeamId,
196+
});
197+
198+
ddbMock.on(TransactWriteItemsCommand).resolves({});
199+
200+
await createOrgGithubTeamHandler(
201+
basePayload,
202+
baseMetadata,
203+
mockLogger as any,
204+
);
205+
206+
expect(mockLogger.info).toHaveBeenCalledWith(
207+
expect.stringContaining("already existed"),
208+
);
209+
expect(assignIdpGroupsToTeam).not.toHaveBeenCalled();
210+
});
211+
212+
it("should set up IDP sync when enabled and team is newly created", async () => {
213+
const newTeamId = 789;
214+
215+
ddbMock.on(GetItemCommand).resolves({
216+
Item: marshall({
217+
primaryKey: "DEFINE#Social Committee",
218+
entryId: "0",
219+
leadsEntraGroupId: "entra-group-123",
220+
}),
221+
});
222+
223+
(createGithubTeam as any).mockResolvedValue({
224+
updated: true,
225+
id: newTeamId,
226+
});
227+
228+
ddbMock.on(TransactWriteItemsCommand).resolves({});
229+
230+
await createOrgGithubTeamHandler(
231+
basePayload,
232+
baseMetadata,
233+
mockLogger as any,
234+
);
235+
236+
// assignIdpGroupsToTeam would be called if GithubIdpSyncEnabled is true
237+
// We can't easily mock currentEnvironmentConfig, so we'll just check
238+
// that the function completed successfully
239+
expect(ddbMock.commandCalls(TransactWriteItemsCommand)).toHaveLength(1);
240+
});
241+
242+
it("should include audit log entry in transaction for new teams", async () => {
243+
const newTeamId = 789;
244+
245+
ddbMock.on(GetItemCommand).resolves({
246+
Item: marshall({
247+
primaryKey: "DEFINE#Social Committee",
248+
entryId: "0",
249+
leadsEntraGroupId: "entra-group-123",
250+
}),
251+
});
252+
253+
(createGithubTeam as any).mockResolvedValue({
254+
updated: true,
255+
id: newTeamId,
256+
});
257+
258+
ddbMock.on(TransactWriteItemsCommand).callsFake((input) => {
259+
expect(input.TransactItems).toHaveLength(2);
260+
261+
const hasAuditLog = input.TransactItems.some(
262+
(item: any) => item.Put?.TableName === genericConfig.AuditLogTable,
263+
);
264+
expect(hasAuditLog).toBe(true);
265+
266+
const hasUpdate = input.TransactItems.some(
267+
(item: any) => item.Update?.TableName === genericConfig.SigInfoTableName,
268+
);
269+
expect(hasUpdate).toBe(true);
270+
271+
return Promise.resolve({});
272+
});
273+
274+
await createOrgGithubTeamHandler(
275+
basePayload,
276+
baseMetadata,
277+
mockLogger as any,
278+
);
279+
280+
expect(mockLogger.info).toHaveBeenCalledWith("Adding updates to audit log");
281+
});
282+
283+
it("should not include audit log for existing teams", async () => {
284+
const existingTeamId = 999;
285+
286+
ddbMock.on(GetItemCommand).resolves({
287+
Item: marshall({
288+
primaryKey: "DEFINE#Social Committee",
289+
entryId: "0",
290+
leadsEntraGroupId: "entra-group-123",
291+
}),
292+
});
293+
294+
(createGithubTeam as any).mockResolvedValue({
295+
updated: false,
296+
id: existingTeamId,
297+
});
298+
299+
ddbMock.on(TransactWriteItemsCommand).callsFake((input) => {
300+
expect(input.TransactItems).toHaveLength(1);
301+
302+
const hasAuditLog = input.TransactItems.some(
303+
(item: any) => item.Put?.TableName === genericConfig.AuditLogTable,
304+
);
305+
expect(hasAuditLog).toBe(false);
306+
307+
return Promise.resolve({});
308+
});
309+
310+
await createOrgGithubTeamHandler(
311+
basePayload,
312+
baseMetadata,
313+
mockLogger as any,
314+
);
315+
});
316+
317+
it("should append suffix to team name when configured", async () => {
318+
const newTeamId = 789;
319+
320+
ddbMock.on(GetItemCommand).resolves({
321+
Item: marshall({
322+
primaryKey: "DEFINE#Social Committee",
323+
entryId: "0",
324+
leadsEntraGroupId: "entra-group-123",
325+
}),
326+
});
327+
328+
(createGithubTeam as any).mockResolvedValue({
329+
updated: true,
330+
id: newTeamId,
331+
});
332+
333+
ddbMock.on(TransactWriteItemsCommand).resolves({});
334+
335+
await createOrgGithubTeamHandler(
336+
basePayload,
337+
baseMetadata,
338+
mockLogger as any,
339+
);
340+
341+
expect(createGithubTeam).toHaveBeenCalledWith(
342+
expect.objectContaining({
343+
name: expect.stringContaining("social-leads"),
344+
}),
345+
);
346+
});
347+
348+
it("should throw error if lock is lost before creating team", async () => {
349+
mockLockAborted = true;
350+
351+
ddbMock.on(GetItemCommand).resolves({
352+
Item: marshall({
353+
primaryKey: "DEFINE#Social Committee",
354+
entryId: "0",
355+
leadsEntraGroupId: "entra-group-123",
356+
}),
357+
});
358+
359+
await expect(
360+
createOrgGithubTeamHandler(basePayload, baseMetadata, mockLogger as any),
361+
).rejects.toThrow(InternalServerError);
362+
});
363+
});

0 commit comments

Comments
 (0)