Skip to content

Commit 3a5cca3

Browse files
committed
yoooo
1 parent 426738f commit 3a5cca3

9 files changed

Lines changed: 326 additions & 16 deletions

File tree

packages/api/src/.internal-tests/routers.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const mockDelete = vi.fn();
1616
vi.mock('@query/db', () => {
1717
return {
1818
db: {
19+
transaction: vi.fn().mockImplementation((callback) => callback(db)),
1920
query: {
2021
admins: {
2122
findFirst: (...args: any[]) => mockFindFirst('admins', ...args),
@@ -44,6 +45,18 @@ vi.mock('@query/db', () => {
4445
events: {
4546
findFirst: (...args: any[]) => mockFindFirst('events', ...args),
4647
findMany: (...args: any[]) => mockFindMany('events', ...args),
48+
},
49+
judges: {
50+
findFirst: (...args: any[]) => mockFindFirst('judges', ...args),
51+
findMany: (...args: any[]) => mockFindMany('judges', ...args),
52+
},
53+
judgeAssignments: {
54+
findFirst: (...args: any[]) => mockFindFirst('judgeAssignments', ...args),
55+
findMany: (...args: any[]) => mockFindMany('judgeAssignments', ...args),
56+
},
57+
hackathonProjects: {
58+
findFirst: (...args: any[]) => mockFindFirst('hackathonProjects', ...args),
59+
findMany: (...args: any[]) => mockFindMany('hackathonProjects', ...args),
4760
}
4861
},
4962
insert: () => ({
@@ -82,6 +95,7 @@ vi.mock('@query/db', () => {
8295
status: 'status',
8396
isPublic: 'is_public',
8497
startDate: 'start_date',
98+
endDate: 'end_date',
8599
},
86100
hackathonParticipants: {
87101
id: 'id',
@@ -103,6 +117,18 @@ vi.mock('@query/db', () => {
103117
qrCode: 'qr_code',
104118
checkInEnabled: 'check_in_enabled',
105119
eventDate: 'event_date',
120+
},
121+
judges: {
122+
id: 'id',
123+
userId: 'user_id',
124+
},
125+
judgeAssignments: {
126+
judgeId: 'judge_id',
127+
hackathonId: 'hackathon_id',
128+
},
129+
hackathonProjects: {
130+
id: 'id',
131+
hackathonId: 'hackathon_id',
106132
}
107133
};
108134
});
@@ -401,4 +427,134 @@ describe('Router Integration and Access Control Verification Suite', () => {
401427
});
402428
});
403429

430+
describe('7. Hackathon Teams, Judge and Project Submission Restrictions', () => {
431+
it('should reject team creation if maxMembers is greater than 4', async () => {
432+
const ctx = createMockCtx('user_id');
433+
const caller = appRouter.createCaller(ctx);
434+
await expect(
435+
caller.team.createTeam({
436+
hackathonId: '00000000-0000-0000-0000-000000000000',
437+
name: 'Super Team',
438+
maxMembers: 5,
439+
})
440+
).rejects.toThrow();
441+
});
442+
443+
it('should prevent registered participants from applying to be a judge', async () => {
444+
const ctx = createMockCtx('participant_user_id');
445+
const hackathonId = '00000000-0000-0000-0000-000000000001';
446+
mockFindFirst.mockImplementation((table) => {
447+
if (table === 'hackathonParticipants') {
448+
return { id: 'participant_1', userId: 'participant_user_id', hackathonId };
449+
}
450+
return null;
451+
});
452+
453+
const caller = appRouter.createCaller(ctx);
454+
await expect(
455+
caller.judge.register({
456+
hackathonId,
457+
name: 'John Doe',
458+
email: 'john@example.com',
459+
})
460+
).rejects.toThrowError('You cannot apply to be a judge because you are registered as a participant for this hackathon.');
461+
});
462+
463+
it('should prevent project submissions before 12 hours after the hacking begins', async () => {
464+
const ctx = createMockCtx('captain_user_id');
465+
const hackathonId = '00000000-0000-0000-0000-000000000001';
466+
const teamId = '00000000-0000-0000-0000-000000000002';
467+
468+
const recentStartDate = new Date(Date.now() - 11 * 60 * 60 * 1000); // 11 hours ago
469+
470+
mockFindFirst.mockImplementation((table) => {
471+
if (table === 'hackathonParticipants') {
472+
return { id: 'participant_1', userId: 'captain_user_id', hackathonId, teamId };
473+
}
474+
if (table === 'hackathons') {
475+
return { id: hackathonId, startDate: recentStartDate, hackingStartTime: null };
476+
}
477+
if (table === 'hackathonTeams') {
478+
return { id: teamId, captainId: 'captain_user_id', hackathonId };
479+
}
480+
return null;
481+
});
482+
483+
const caller = appRouter.createCaller(ctx);
484+
await expect(
485+
caller.team.submitProject({
486+
hackathonId,
487+
teamId,
488+
name: 'Awesome Project',
489+
description: 'This is a long description of the awesome project.',
490+
})
491+
).rejects.toThrowError('Project submission is not open yet. It starts 12 hours after the hacking begins.');
492+
});
493+
494+
it('should prevent project edits (existing project) after 34 hours of starting hacking', async () => {
495+
const ctx = createMockCtx('captain_user_id');
496+
const hackathonId = '00000000-0000-0000-0000-000000000001';
497+
const teamId = '00000000-0000-0000-0000-000000000002';
498+
499+
const startDate35hAgo = new Date(Date.now() - 35 * 60 * 60 * 1000); // 35 hours ago
500+
501+
mockFindFirst.mockImplementation((table) => {
502+
if (table === 'hackathonParticipants') {
503+
return { id: 'participant_1', userId: 'captain_user_id', hackathonId, teamId };
504+
}
505+
if (table === 'hackathons') {
506+
return { id: hackathonId, startDate: startDate35hAgo, hackingStartTime: null };
507+
}
508+
if (table === 'hackathonTeams') {
509+
return { id: teamId, captainId: 'captain_user_id', hackathonId };
510+
}
511+
if (table === 'hackathonProjects') {
512+
return { id: 'project_1', hackathonId, teamId, name: 'Old Name', description: 'Old Description' };
513+
}
514+
return null;
515+
});
516+
517+
const caller = appRouter.createCaller(ctx);
518+
await expect(
519+
caller.team.submitProject({
520+
hackathonId,
521+
teamId,
522+
name: 'Awesome Project',
523+
description: 'This is a long description of the awesome project.',
524+
})
525+
).rejects.toThrowError('Project edits are closed. Devposts must be final 34 hours after the hacking starts.');
526+
});
527+
528+
it('should prevent project submissions more than 36 hours after hacking starts', async () => {
529+
const ctx = createMockCtx('captain_user_id');
530+
const hackathonId = '00000000-0000-0000-0000-000000000001';
531+
const teamId = '00000000-0000-0000-0000-000000000002';
532+
533+
const pastStartDate = new Date(Date.now() - 37 * 60 * 60 * 1000); // 37 hours ago
534+
535+
mockFindFirst.mockImplementation((table) => {
536+
if (table === 'hackathonParticipants') {
537+
return { id: 'participant_1', userId: 'captain_user_id', hackathonId, teamId };
538+
}
539+
if (table === 'hackathons') {
540+
return { id: hackathonId, startDate: pastStartDate, hackingStartTime: null };
541+
}
542+
if (table === 'hackathonTeams') {
543+
return { id: teamId, captainId: 'captain_user_id', hackathonId };
544+
}
545+
return null;
546+
});
547+
548+
const caller = appRouter.createCaller(ctx);
549+
await expect(
550+
caller.team.submitProject({
551+
hackathonId,
552+
teamId,
553+
name: 'Awesome Project',
554+
description: 'This is a long description of the awesome project.',
555+
})
556+
).rejects.toThrowError('Project submission closed. The submission window ended 36 hours after the hacking started.');
557+
});
558+
});
559+
404560
});

packages/api/src/routers/hackathon.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const hackathonRouter = createTRPCRouter({
109109
startDate: z.date(),
110110
endDate: z.date(),
111111
registrationDeadline: z.date().optional(),
112+
hackingStartTime: z.date().optional(),
112113
maxParticipants: z.number().int().positive().max(10000).optional(),
113114
prizes: z.array(
114115
z.object({
@@ -126,6 +127,8 @@ export const hackathonRouter = createTRPCRouter({
126127
message: "End date must be after start date",
127128
}).refine(data => !data.registrationDeadline || data.registrationDeadline <= data.startDate, {
128129
message: "Registration deadline must be before start date",
130+
}).refine(data => !data.hackingStartTime || data.hackingStartTime >= data.startDate, {
131+
message: "Hacking start time must be after or equal to hackathon start date",
129132
})
130133
)
131134
.mutation(async ({ ctx, input }) => {
@@ -152,6 +155,7 @@ export const hackathonRouter = createTRPCRouter({
152155
startDate: z.date().optional(),
153156
endDate: z.date().optional(),
154157
registrationDeadline: z.date().optional(),
158+
hackingStartTime: z.date().nullable().optional(),
155159
maxParticipants: z.number().int().positive().max(10000).optional(),
156160
status: z.enum(["draft", "open", "closed", "in_progress", "completed", "cancelled"]).optional(),
157161
prizes: z.array(

packages/api/src/routers/judge.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
hackathonMaps,
1111
hackathons,
1212
users,
13+
hackathonParticipants,
1314
} from "@query/db";
1415
import { eq, and, asc, sql } from "drizzle-orm";
1516
import { CacheKeys } from "../middleware/cache";
@@ -111,6 +112,17 @@ function buildCoverageQueues(
111112
}
112113
}
113114

115+
// Shift/rotate the generated queues to stagger sequences and reduce judge bias
116+
for (let i = 0; i < judgeAssignmentsList.length; i++) {
117+
const judgeId = judgeAssignmentsList[i].judgeId;
118+
const q = queues.get(judgeId);
119+
if (q && q.length > 1) {
120+
const offset = i % q.length;
121+
const rotated = [...q.slice(offset), ...q.slice(0, offset)];
122+
queues.set(judgeId, rotated);
123+
}
124+
}
125+
114126
return queues;
115127
}
116128

@@ -1258,6 +1270,7 @@ export const judgeRouter = createTRPCRouter({
12581270
/** When false (default), special-label/sponsor judge projects are randomized.
12591271
* When true, they stay grouped in table order. */
12601272
groupSpecial: z.boolean().default(false),
1273+
autoCalculate: z.boolean().default(true),
12611274
})
12621275
)
12631276
.mutation(async ({ ctx, input }) => {
@@ -1283,6 +1296,37 @@ export const judgeRouter = createTRPCRouter({
12831296
throw new TRPCError({ code: "BAD_REQUEST", message: "No projects found for this hackathon" });
12841297
}
12851298

1299+
let minProjects = input.minProjects;
1300+
let maxProjects = input.maxProjects;
1301+
1302+
if (input.autoCalculate) {
1303+
// Count active registered participants
1304+
const participantCountResult = await tx
1305+
.select({ count: sql<number>`count(*)` })
1306+
.from(hackathonParticipants)
1307+
.where(
1308+
and(
1309+
eq(hackathonParticipants.hackathonId, input.hackathonId),
1310+
sql`${hackathonParticipants.registrationStatus} != 'rejected'`
1311+
)
1312+
);
1313+
const activeRegistrations = Number(participantCountResult[0]?.count || 0);
1314+
1315+
const P = allProjects.length || Math.ceil(activeRegistrations / 4) || 1;
1316+
const mainJudgesCount = allAssignments.filter(
1317+
(a) => !a.track || MAIN_TRACKS.has(a.track)
1318+
).length || 1;
1319+
1320+
// Each project needs to be graded at least 3 times
1321+
const targetCoverage = 3;
1322+
const avgRequired = Math.ceil((P * targetCoverage) / mainJudgesCount);
1323+
1324+
// We assume a 3-hour judging window (180 minutes)
1325+
// With ~9 minutes per project evaluation, a judge can evaluate at most 20 projects.
1326+
minProjects = Math.max(3, Math.min(avgRequired, 20));
1327+
maxProjects = Math.max(minProjects + 2, Math.min(avgRequired + 2, 22));
1328+
}
1329+
12861330
// Clear existing queues
12871331
await tx.delete(judgeQueue).where(eq(judgeQueue.hackathonId, input.hackathonId));
12881332

@@ -1300,8 +1344,8 @@ export const judgeRouter = createTRPCRouter({
13001344
}));
13011345

13021346
const queues = buildCoverageQueues(judgeList, projectList, MAIN_TRACKS, {
1303-
minProjects: input.minProjects,
1304-
maxProjects: input.maxProjects,
1347+
minProjects,
1348+
maxProjects,
13051349
shuffle: input.shuffle,
13061350
groupSpecial: input.groupSpecial,
13071351
});
@@ -1501,6 +1545,21 @@ export const judgeRouter = createTRPCRouter({
15011545
)
15021546
.mutation(async ({ ctx, input }) => {
15031547
return await (ctx.db as DrizzleDB).transaction(async (tx) => {
1548+
// Check if user is registered as a participant for this hackathon
1549+
const participant = await tx.query.hackathonParticipants.findFirst({
1550+
where: and(
1551+
eq(hackathonParticipants.hackathonId, input.hackathonId),
1552+
eq(hackathonParticipants.userId, ctx.userId as string)
1553+
),
1554+
});
1555+
1556+
if (participant) {
1557+
throw new TRPCError({
1558+
code: "BAD_REQUEST",
1559+
message: "You cannot apply to be a judge because you are registered as a participant for this hackathon.",
1560+
});
1561+
}
1562+
15041563
// Find existing judge profile or create one
15051564
let judge = await tx.query.judges.findFirst({
15061565
where: eq(judges.userId, ctx.userId),

0 commit comments

Comments
 (0)