fix(finalist): program-scope guards on enroll/unenroll (code review)

- enrollFinalists: reject a roundId whose competition belongs to a
  different program than input.programId.
- unenroll: reject a project/round pair from different programs before
  any delete.
- Hoist ADMIN_CONFIRM attendee validation to a pre-pass so a bad entry
  in a multi-team batch fails before any project is partially written.
- Add regression tests for both cross-program guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-04 15:49:13 +02:00
parent 8ee517f6ca
commit 34bb2bad57
3 changed files with 108 additions and 29 deletions

View File

@@ -316,6 +316,31 @@ describe('finalist.enrollFinalists', () => {
}),
).rejects.toThrow(/cap/i)
})
it('rejects a roundId that belongs to a different program', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const a = await setupEnrollFixture(`enroll-xprogram-a-${uid()}`)
const b = await setupEnrollFixture(`enroll-xprogram-b-${uid()}`)
programIds.push(a.program.id, b.program.id)
userIds.push(a.lead.id, a.member.id, b.lead.id, b.member.id)
const caller = createCaller(finalistRouter, {
id: admin.id,
email: admin.email,
role: 'SUPER_ADMIN',
})
// Program A's project + Program B's LIVE_FINAL round → must be rejected.
await expect(
caller.enrollFinalists({
programId: a.program.id,
roundId: b.liveFinalRound.id,
enrollments: [{ projectId: a.project.id, mode: 'EMAIL' }],
}),
).rejects.toThrow(/does not belong to this program/i)
})
})
// ─── finalist.listEnrollmentCandidates ────────────────────────────────────