Files
MOPC-Portal/tests/unit/final-documents.test.ts

212 lines
9.9 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, afterAll } from 'vitest'
import { TRPCError } from '@trpc/server'
import { prisma } from '../setup'
import {
createTestProgram,
createTestCompetition,
createTestRound,
createTestProject,
createTestProjectRoundState,
createTestUser,
cleanupTestData,
uid,
} from '../helpers'
import {
getFinalDocumentStatusForProject,
sendManualFinalDocReminders,
} from '@/server/services/final-documents'
import * as applicantRouter from '@/server/routers/applicant'
import * as finalistRouter from '@/server/routers/finalist'
import { createCaller } from '../setup'
const programIds: string[] = []
async function makeFinaleProgram(
opts: { roundStatus?: 'ROUND_ACTIVE' | 'ROUND_DRAFT'; closeAt?: Date; skipRequirements?: boolean } = {},
) {
const program = await createTestProgram()
programIds.push(program.id)
const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
const round = await createTestRound(comp.id, {
roundType: 'LIVE_FINAL',
status: opts.roundStatus ?? 'ROUND_ACTIVE',
sortOrder: 6,
windowCloseAt: opts.closeAt ?? new Date(Date.now() + 86_400_000),
})
if (opts.skipRequirements) {
return { program, comp, round, reqPlan: undefined, reqVideo: undefined }
}
const reqPlan = await prisma.fileRequirement.create({
data: { id: uid('req'), roundId: round.id, name: 'Final Business Plan', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 },
})
const reqVideo = await prisma.fileRequirement.create({
data: { id: uid('req'), roundId: round.id, name: '1-minute Video', acceptedMimeTypes: ['video/*'], isRequired: true, sortOrder: 2 },
})
return { program, comp, round, reqPlan, reqVideo }
}
describe('getFinalDocumentStatusForProject', () => {
afterAll(async () => {
for (const id of programIds) await cleanupTestData(id)
})
it('returns null when the project is not enrolled in the active LIVE_FINAL round', async () => {
const { program } = await makeFinaleProgram()
const orphan = await createTestProject(program.id)
const status = await getFinalDocumentStatusForProject(prisma, orphan.id)
expect(status).toBeNull()
})
it('returns per-requirement status with none uploaded', async () => {
const { program, round } = await makeFinaleProgram()
const project = await createTestProject(program.id)
await createTestProjectRoundState(project.id, round.id)
const status = await getFinalDocumentStatusForProject(prisma, project.id)
expect(status).not.toBeNull()
expect(status!.requirements).toHaveLength(2)
expect(status!.requirements.every((r) => !r.uploaded)).toBe(true)
expect(status!.allRequiredUploaded).toBe(false)
expect(status!.deadline?.toISOString()).toBe(round.windowCloseAt!.toISOString())
})
it('marks a requirement uploaded and flips allRequiredUploaded when all present', async () => {
const { program, round, reqPlan, reqVideo } = await makeFinaleProgram()
const project = await createTestProject(program.id)
await createTestProjectRoundState(project.id, round.id)
for (const [req, type, mime] of [
[reqPlan!, 'BUSINESS_PLAN', 'application/pdf'],
[reqVideo!, 'VIDEO', 'video/mp4'],
] as const) {
await prisma.projectFile.create({
data: {
id: uid('file'), projectId: project.id, roundId: round.id, requirementId: req.id,
fileType: type as any, fileName: `f-${req.id}`, mimeType: mime, size: 10,
bucket: 'b', objectKey: uid('key'),
},
})
}
const status = await getFinalDocumentStatusForProject(prisma, project.id)
expect(status!.requirements.every((r) => r.uploaded)).toBe(true)
expect(status!.allRequiredUploaded).toBe(true)
})
it('returns null when the LIVE_FINAL round is not active', async () => {
const { program, round } = await makeFinaleProgram({ roundStatus: 'ROUND_DRAFT' })
const project = await createTestProject(program.id)
await createTestProjectRoundState(project.id, round.id)
const status = await getFinalDocumentStatusForProject(prisma, project.id)
expect(status).toBeNull()
})
it('reports allRequiredUploaded false when the round has no required requirements', async () => {
const { program, round } = await makeFinaleProgram({ skipRequirements: true })
const project = await createTestProject(program.id)
await createTestProjectRoundState(project.id, round.id)
const status = await getFinalDocumentStatusForProject(prisma, project.id)
expect(status).not.toBeNull()
expect(status!.requirements).toHaveLength(0)
expect(status!.allRequiredUploaded).toBe(false)
})
})
describe('applicant.getFinalDocumentStatus', () => {
const localPrograms: string[] = []
const localUsers: string[] = []
afterAll(async () => {
for (const id of localPrograms) await cleanupTestData(id, localUsers)
})
it('returns the status for the caller\'s enrolled finalist project', async () => {
const program = await createTestProgram()
localPrograms.push(program.id)
const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000) })
await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } })
const project = await createTestProject(program.id)
await createTestProjectRoundState(project.id, round.id)
const user = await createTestUser('APPLICANT')
localUsers.push(user.id)
await prisma.teamMember.create({ data: { projectId: project.id, userId: user.id, role: 'LEAD' } })
const caller = createCaller(applicantRouter.applicantRouter, user)
const status = await caller.getFinalDocumentStatus()
expect(status?.roundId).toBe(round.id)
expect(status?.requirements).toHaveLength(1)
})
it('returns null when the caller has no project', async () => {
const user = await createTestUser('APPLICANT')
localUsers.push(user.id)
const caller = createCaller(applicantRouter.applicantRouter, user)
expect(await caller.getFinalDocumentStatus()).toBeNull()
})
})
describe('sendManualFinalDocReminders', () => {
const localPrograms: string[] = []
const localUsers: string[] = []
afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) })
it('sends a reminder only to finalist teams with missing required docs', async () => {
const program = await createTestProgram()
localPrograms.push(program.id)
const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000) })
await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } })
const project = await createTestProject(program.id)
await createTestProjectRoundState(project.id, round.id)
const lead = await createTestUser('APPLICANT')
localUsers.push(lead.id)
await prisma.teamMember.create({ data: { projectId: project.id, userId: lead.id, role: 'LEAD' } })
const result = await sendManualFinalDocReminders(prisma, { programId: program.id, actorId: lead.id })
expect(result.sent).toBe(1)
const notif = await prisma.inAppNotification.findFirst({ where: { userId: lead.id, type: 'GRAND_FINAL_DOCS_REMINDER' } })
expect(notif).not.toBeNull()
})
})
describe('finalist.listReviewDocuments', () => {
const localPrograms: string[] = []
const localUsers: string[] = []
afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) })
async function setup() {
const program = await createTestProgram()
localPrograms.push(program.id)
const comp = await createTestCompetition(program.id, { status: 'ACTIVE' })
const jg = await prisma.juryGroup.create({ data: { id: uid('jg'), competitionId: comp.id, name: 'Finals Jury', slug: uid('jg') } })
const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000) })
await prisma.round.update({ where: { id: round.id }, data: { juryGroupId: jg.id } })
await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } })
const project = await createTestProject(program.id, { competitionCategory: 'STARTUP' })
await createTestProjectRoundState(project.id, round.id)
return { program, comp, jg, round, project }
}
it('admin sees all finalist teams', async () => {
const { program } = await setup()
const admin = await createTestUser('PROGRAM_ADMIN'); localUsers.push(admin.id)
const caller = createCaller(finalistRouter.finalistRouter, admin)
const result = await caller.listReviewDocuments({ programId: program.id })
expect(result.teams).toHaveLength(1)
expect(result.totalCount).toBe(1)
})
it('a finals jury-group member is allowed', async () => {
const { program, jg } = await setup()
const juror = await createTestUser('JURY_MEMBER'); localUsers.push(juror.id)
await prisma.juryGroupMember.create({ data: { juryGroupId: jg.id, userId: juror.id, role: 'MEMBER' } })
const caller = createCaller(finalistRouter.finalistRouter, juror)
const result = await caller.listReviewDocuments({ programId: program.id })
expect(result.teams).toHaveLength(1)
})
it('a non-finals jury member is forbidden', async () => {
const { program } = await setup()
const juror = await createTestUser('JURY_MEMBER'); localUsers.push(juror.id)
const caller = createCaller(finalistRouter.finalistRouter, juror)
await expect(caller.listReviewDocuments({ programId: program.id })).rejects.toThrow(TRPCError)
})
})