Skip to content

Commit 087904e

Browse files
authored
fix: missing team report system (#492)
* added admin only missing team restore logic and button * changed button typography and added safety guard * added comments and changed function name for readability * added functionality to button to set active true * changed active behavior and added tests * Add generic error message
1 parent 3694081 commit 087904e

6 files changed

Lines changed: 548 additions & 141 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/** @jest-environment node */
2+
3+
import { auth } from '@/auth';
4+
import { GetManySubmissions } from '@datalib/submissions/getSubmissions';
5+
import { UpdateSubmission } from '@datalib/submissions/updateSubmission';
6+
import { GetTeam } from '@datalib/teams/getTeam';
7+
import { UpdateTeam } from '@datalib/teams/updateTeam';
8+
import {
9+
reportMissingProject,
10+
restoreMissingTeam,
11+
} from '@actions/teams/reportMissingTeam';
12+
import Submission from '@typeDefs/submission';
13+
14+
jest.mock('@/auth', () => ({
15+
auth: jest.fn(),
16+
}));
17+
18+
jest.mock('@datalib/submissions/getSubmissions', () => ({
19+
GetManySubmissions: jest.fn(),
20+
}));
21+
22+
jest.mock('@datalib/submissions/updateSubmission', () => ({
23+
UpdateSubmission: jest.fn(),
24+
}));
25+
26+
jest.mock('@datalib/teams/getTeam', () => ({
27+
GetTeam: jest.fn(),
28+
}));
29+
30+
jest.mock('@datalib/teams/updateTeam', () => ({
31+
UpdateTeam: jest.fn(),
32+
}));
33+
34+
const mockAuth = auth as jest.MockedFunction<typeof auth>;
35+
const mockGetManySubmissions = GetManySubmissions as jest.MockedFunction<
36+
typeof GetManySubmissions
37+
>;
38+
const mockUpdateSubmission = UpdateSubmission as jest.MockedFunction<
39+
typeof UpdateSubmission
40+
>;
41+
const mockGetTeam = GetTeam as jest.MockedFunction<typeof GetTeam>;
42+
const mockUpdateTeam = UpdateTeam as jest.MockedFunction<typeof UpdateTeam>;
43+
44+
const makeSubmission = (
45+
judge_id: string,
46+
team_id: string,
47+
queuePosition: number,
48+
is_scored = false
49+
): Submission => ({
50+
judge_id,
51+
team_id,
52+
queuePosition,
53+
is_scored,
54+
social_good: null,
55+
creativity: null,
56+
presentation: null,
57+
scores: [],
58+
});
59+
60+
describe('reportMissingTeam flow', () => {
61+
beforeEach(() => {
62+
jest.clearAllMocks();
63+
});
64+
65+
describe('reportMissingProject', () => {
66+
it('pushes a report and moves the reported team to the end of the judge queue', async () => {
67+
mockUpdateTeam.mockResolvedValue({
68+
ok: true,
69+
body: {},
70+
error: null,
71+
} as any);
72+
mockGetManySubmissions.mockResolvedValue({
73+
ok: true,
74+
body: [
75+
makeSubmission('judge-1', 'team-a', 0),
76+
makeSubmission('judge-1', 'team-b', 1),
77+
makeSubmission('judge-1', 'team-c', 2),
78+
],
79+
error: null,
80+
} as any);
81+
mockUpdateSubmission.mockResolvedValue({
82+
ok: true,
83+
body: {},
84+
error: null,
85+
} as any);
86+
87+
const res = await reportMissingProject('judge-1', 'team-b');
88+
89+
expect(res.ok).toBe(true);
90+
expect(mockUpdateTeam).toHaveBeenCalledWith('team-b', {
91+
$push: {
92+
reports: {
93+
timestamp: expect.any(Number),
94+
judge_id: 'judge-1',
95+
},
96+
},
97+
});
98+
99+
expect(mockUpdateSubmission).toHaveBeenCalledTimes(3);
100+
expect(mockUpdateSubmission).toHaveBeenNthCalledWith(
101+
1,
102+
'judge-1',
103+
'team-a',
104+
{
105+
$set: { queuePosition: 0 },
106+
}
107+
);
108+
expect(mockUpdateSubmission).toHaveBeenNthCalledWith(
109+
2,
110+
'judge-1',
111+
'team-c',
112+
{
113+
$set: { queuePosition: 1 },
114+
}
115+
);
116+
expect(mockUpdateSubmission).toHaveBeenNthCalledWith(
117+
3,
118+
'judge-1',
119+
'team-b',
120+
{
121+
$set: { queuePosition: 2 },
122+
}
123+
);
124+
});
125+
126+
it('returns an error when the reported team is not in that judge submission list', async () => {
127+
mockUpdateTeam.mockResolvedValue({
128+
ok: true,
129+
body: {},
130+
error: null,
131+
} as any);
132+
mockGetManySubmissions.mockResolvedValue({
133+
ok: true,
134+
body: [makeSubmission('judge-1', 'team-x', 0)],
135+
error: null,
136+
} as any);
137+
138+
const res = await reportMissingProject('judge-1', 'team-b');
139+
140+
expect(res.ok).toBe(false);
141+
expect(res.error).toContain(
142+
'Submission from judge: judge-1 and team: team-b not found.'
143+
);
144+
expect(mockUpdateSubmission).not.toHaveBeenCalled();
145+
});
146+
});
147+
148+
describe('restoreMissingTeam', () => {
149+
it('requeues each unique reporting judge, clears reports, and reactivates inactive team', async () => {
150+
mockAuth.mockResolvedValue({ user: { role: 'admin' } } as any);
151+
mockGetTeam.mockResolvedValue({
152+
ok: true,
153+
body: {
154+
_id: 'team-target',
155+
teamNumber: 5,
156+
tableNumber: 'A1',
157+
name: 'Target Team',
158+
tracks: ['General'],
159+
active: false,
160+
reports: [
161+
{ timestamp: 1, judge_id: 'judge-a' },
162+
{ timestamp: 2, judge_id: 'judge-a' },
163+
{ timestamp: 3, judge_id: 'judge-b' },
164+
{ timestamp: 4, judge_id: '' },
165+
],
166+
},
167+
error: null,
168+
} as any);
169+
170+
mockGetManySubmissions
171+
.mockResolvedValueOnce({
172+
ok: true,
173+
body: [
174+
makeSubmission('judge-a', 'team-x', 0),
175+
makeSubmission('judge-a', 'team-target', 1),
176+
makeSubmission('judge-a', 'team-y', 2),
177+
],
178+
error: null,
179+
} as any)
180+
.mockResolvedValueOnce({
181+
ok: true,
182+
body: [
183+
makeSubmission('judge-b', 'team-target', 0, true),
184+
makeSubmission('judge-b', 'team-z', 1),
185+
],
186+
error: null,
187+
} as any);
188+
189+
mockUpdateSubmission.mockResolvedValue({
190+
ok: true,
191+
body: {},
192+
error: null,
193+
} as any);
194+
mockUpdateTeam.mockResolvedValue({
195+
ok: true,
196+
body: {},
197+
error: null,
198+
} as any);
199+
200+
const res = await restoreMissingTeam('team-target');
201+
202+
expect(res.ok).toBe(true);
203+
expect(mockGetManySubmissions).toHaveBeenCalledTimes(2);
204+
expect(mockGetManySubmissions).toHaveBeenNthCalledWith(1, {
205+
judge_id: { '*convertId': { id: 'judge-a' } },
206+
});
207+
expect(mockGetManySubmissions).toHaveBeenNthCalledWith(2, {
208+
judge_id: { '*convertId': { id: 'judge-b' } },
209+
});
210+
211+
// judge-a queue should be reordered, judge-b is skipped because target is already scored
212+
expect(mockUpdateSubmission).toHaveBeenCalledTimes(3);
213+
expect(mockUpdateSubmission).toHaveBeenNthCalledWith(
214+
1,
215+
'judge-a',
216+
'team-x',
217+
{
218+
$set: { queuePosition: 0 },
219+
}
220+
);
221+
expect(mockUpdateSubmission).toHaveBeenNthCalledWith(
222+
2,
223+
'judge-a',
224+
'team-y',
225+
{
226+
$set: { queuePosition: 1 },
227+
}
228+
);
229+
expect(mockUpdateSubmission).toHaveBeenNthCalledWith(
230+
3,
231+
'judge-a',
232+
'team-target',
233+
{
234+
$set: { queuePosition: 2 },
235+
}
236+
);
237+
238+
expect(mockUpdateTeam).toHaveBeenCalledWith('team-target', {
239+
$set: { reports: [], active: true },
240+
});
241+
expect(res.body?.requeueResults).toEqual([
242+
{ judge_id: 'judge-a', reorderResCount: 3 },
243+
]);
244+
});
245+
246+
it('clears reports and keeps the team active when the team is already active', async () => {
247+
mockAuth.mockResolvedValue({ user: { role: 'admin' } } as any);
248+
mockGetTeam.mockResolvedValue({
249+
ok: true,
250+
body: {
251+
_id: 'team-target',
252+
teamNumber: 10,
253+
tableNumber: 'B2',
254+
name: 'Already Active Team',
255+
tracks: ['General'],
256+
active: true,
257+
reports: [],
258+
},
259+
error: null,
260+
} as any);
261+
mockUpdateTeam.mockResolvedValue({
262+
ok: true,
263+
body: {},
264+
error: null,
265+
} as any);
266+
267+
const res = await restoreMissingTeam('team-target');
268+
269+
expect(res.ok).toBe(true);
270+
expect(mockGetManySubmissions).not.toHaveBeenCalled();
271+
expect(mockUpdateTeam).toHaveBeenCalledWith('team-target', {
272+
$set: { reports: [], active: true },
273+
});
274+
});
275+
276+
it('rejects non-admin users', async () => {
277+
mockAuth.mockResolvedValue({ user: { role: 'judge' } } as any);
278+
279+
const res = await restoreMissingTeam('team-target');
280+
281+
expect(res.ok).toBe(false);
282+
expect(res.error).toBe('Access Denied.');
283+
expect(mockGetTeam).not.toHaveBeenCalled();
284+
expect(mockUpdateTeam).not.toHaveBeenCalled();
285+
expect(mockGetManySubmissions).not.toHaveBeenCalled();
286+
expect(mockUpdateSubmission).not.toHaveBeenCalled();
287+
});
288+
});
289+
});

0 commit comments

Comments
 (0)