import { describe, it, expect, afterAll, vi } from 'vitest' vi.mock('@/lib/minio', async (importOriginal) => { const actual = await importOriginal() return { ...actual, getPresignedUrl: vi.fn(async () => 'https://example.test/presigned') } }) import { prisma } from '../setup' import { createTestProgram, createTestCompetition, createTestRound, createTestProject, createTestProjectRoundState, createTestUser, cleanupTestData, uid, } from '../helpers' import { listFinalistDocumentsForReview, listReviewVisibilityOptions } from '@/server/services/final-documents' const programIds: string[] = [] afterAll(async () => { for (const id of programIds) await cleanupTestData(id) }) /** * One finalist team with 4 files: * - Business Plan (prior SUBMISSION round, via requirement reqBP) * - Pitch Deck (prior SUBMISSION round, via requirement reqDeck) * - loose.pdf (prior SUBMISSION round, NO requirement) * - final.mp4 (uploaded directly to the LIVE_FINAL round, via reqFinal) */ async function setupCuration() { const program = await createTestProgram() programIds.push(program.id) const comp = await createTestCompetition(program.id, { status: 'ACTIVE' }) const priorRound = await createTestRound(comp.id, { roundType: 'SUBMISSION', status: 'ROUND_CLOSED', sortOrder: 2 }) const reqBP = await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: priorRound.id, name: 'Business Plan', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 }, }) const reqDeck = await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: priorRound.id, name: 'Pitch Deck', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 2 }, }) const finale = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000), configJson: { allowFinalistRevisedUploads: true }, }) const reqFinal = await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: finale.id, name: '1-minute Video', acceptedMimeTypes: ['video/*'], isRequired: false, sortOrder: 1 }, }) const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, finale.id) const mkFile = (roundId: string, requirementId: string | null, fileName: string) => prisma.projectFile.create({ data: { id: uid('file'), projectId: project.id, roundId, requirementId, fileType: 'SUPPORTING_DOC', fileName, mimeType: 'application/pdf', size: 10, bucket: 'b', objectKey: uid('key'), }, }) await mkFile(priorRound.id, reqBP.id, 'bp.pdf') await mkFile(priorRound.id, reqDeck.id, 'deck.pdf') await mkFile(priorRound.id, null, 'loose.pdf') await mkFile(finale.id, reqFinal.id, 'final.mp4') return { program, priorRound, finale, reqBP, reqDeck, reqFinal, project } } async function setSelection(roundId: string, ids: string[] | null) { const round = await prisma.round.findUniqueOrThrow({ where: { id: roundId }, select: { configJson: true } }) const cfg = (round.configJson ?? {}) as Record if (ids === null) delete cfg.reviewVisibleRequirementIds else cfg.reviewVisibleRequirementIds = ids await prisma.round.update({ where: { id: roundId }, data: { configJson: cfg as object } }) } describe('listFinalistDocumentsForReview curation', () => { it('no selection key → all files visible (current behavior)', async () => { const { program } = await setupCuration() const result = await listFinalistDocumentsForReview(prisma, program.id) expect(result.teams).toHaveLength(1) expect(result.teams[0].files).toHaveLength(4) }) it('selection → only matching prior files, finale uploads always visible', async () => { const { program, finale, reqBP } = await setupCuration() await setSelection(finale.id, [reqBP.id]) const result = await listFinalistDocumentsForReview(prisma, program.id) const names = result.teams[0].files.map((f) => f.fileName).sort() expect(names).toEqual(['bp.pdf', 'final.mp4']) // deck.pdf and loose.pdf hidden }) it('empty selection → only finale uploads visible', async () => { const { program, finale } = await setupCuration() await setSelection(finale.id, []) const result = await listFinalistDocumentsForReview(prisma, program.id) expect(result.teams[0].files.map((f) => f.fileName)).toEqual(['final.mp4']) }) it('prior file without a requirement is excluded under any selection', async () => { const { program, finale, reqBP, reqDeck } = await setupCuration() await setSelection(finale.id, [reqBP.id, reqDeck.id]) const result = await listFinalistDocumentsForReview(prisma, program.id) expect(result.teams[0].files.map((f) => f.fileName)).not.toContain('loose.pdf') }) }) describe('listReviewVisibilityOptions', () => { it('lists distinct prior-round slots with counts; excludes finale-round slots and requirement-less files', async () => { const { program, reqBP, reqDeck } = await setupCuration() const options = await listReviewVisibilityOptions(prisma, program.id) expect(options.map((o) => o.requirementId).sort()).toEqual([reqBP.id, reqDeck.id].sort()) const bp = options.find((o) => o.requirementId === reqBP.id)! expect(bp.name).toBe('Business Plan') expect(bp.fileCount).toBe(1) expect(bp.roundName).toBeTruthy() }) it('returns [] when there is no open finale round', async () => { const program = await createTestProgram() programIds.push(program.id) expect(await listReviewVisibilityOptions(prisma, program.id)).toEqual([]) }) }) describe('finalist review-doc settings procedures', () => { const userIds: string[] = [] afterAll(async () => { for (const id of programIds) await cleanupTestData(id, userIds) }) it('round-trips a selection and preserves sibling configJson keys', async () => { const { program, finale, reqBP } = await setupCuration() const admin = await createTestUser('PROGRAM_ADMIN') userIds.push(admin.id) const { finalistRouter } = await import('@/server/routers/finalist') const { createCaller } = await import('../setup') const caller = createCaller(finalistRouter, admin) const initial = await caller.getReviewDocSettings({ programId: program.id, roundId: finale.id }) expect(initial.selectedIds).toBeNull() expect(initial.options.length).toBe(2) await caller.setReviewVisibleRequirements({ roundId: finale.id, requirementIds: [reqBP.id] }) const curated = await caller.getReviewDocSettings({ programId: program.id, roundId: finale.id }) expect(curated.selectedIds).toEqual([reqBP.id]) // sibling key from setupCuration must survive const round = await prisma.round.findUniqueOrThrow({ where: { id: finale.id }, select: { configJson: true } }) expect((round.configJson as Record).allowFinalistRevisedUploads).toBe(true) await caller.setReviewVisibleRequirements({ roundId: finale.id, requirementIds: null }) const cleared = await caller.getReviewDocSettings({ programId: program.id, roundId: finale.id }) expect(cleared.selectedIds).toBeNull() }) it('rejects a non-LIVE_FINAL round', async () => { const { program, priorRound } = await setupCuration() const admin = await createTestUser('PROGRAM_ADMIN') userIds.push(admin.id) const { finalistRouter } = await import('@/server/routers/finalist') const { createCaller } = await import('../setup') const caller = createCaller(finalistRouter, admin) await expect( caller.setReviewVisibleRequirements({ roundId: priorRound.id, requirementIds: [] }), ).rejects.toThrow() void program }) })