Merge: primary-role mentors assignable on mentor config page
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m51s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m51s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -315,7 +315,10 @@ export const mentorRouter = router({
|
|||||||
|
|
||||||
const mentors = await ctx.prisma.user.findMany({
|
const mentors = await ctx.prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
roles: { has: 'MENTOR' },
|
// MENTOR as primary role OR in the secondary roles[] array. Some legacy
|
||||||
|
// users have role=MENTOR with an empty roles[], so checking roles only
|
||||||
|
// would wrongly exclude them (mirrors userHasRole's fallback).
|
||||||
|
OR: [{ role: 'MENTOR' }, { roles: { has: 'MENTOR' } }],
|
||||||
status: { not: 'SUSPENDED' },
|
status: { not: 'SUSPENDED' },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -728,9 +731,11 @@ export const mentorRouter = router({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const mentors = await ctx.prisma.user.findMany({
|
const mentors = await ctx.prisma.user.findMany({
|
||||||
where: { id: { in: input.mentorIds } },
|
where: { id: { in: input.mentorIds } },
|
||||||
select: { id: true, name: true, email: true, roles: true },
|
select: { id: true, name: true, email: true, role: true, roles: true },
|
||||||
})
|
})
|
||||||
const validMentors = mentors.filter((m) => m.roles.includes('MENTOR'))
|
// Accept MENTOR as primary role OR in roles[] (legacy users may have
|
||||||
|
// role=MENTOR with an empty roles[] array).
|
||||||
|
const validMentors = mentors.filter((m) => m.roles.includes('MENTOR') || m.role === 'MENTOR')
|
||||||
if (validMentors.length === 0) {
|
if (validMentors.length === 0) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
@@ -1574,7 +1579,9 @@ export const mentorRouter = router({
|
|||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const mentors = await ctx.prisma.user.findMany({
|
const mentors = await ctx.prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
roles: { has: 'MENTOR' },
|
// MENTOR as primary role OR in the secondary roles[] array (see
|
||||||
|
// getCandidates: legacy users may have role=MENTOR with empty roles[]).
|
||||||
|
OR: [{ role: 'MENTOR' }, { roles: { has: 'MENTOR' } }],
|
||||||
status: { not: 'SUSPENDED' },
|
status: { not: 'SUSPENDED' },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
98
tests/unit/mentor-primary-role-assignable.test.ts
Normal file
98
tests/unit/mentor-primary-role-assignable.test.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
|
||||||
|
|
||||||
|
vi.mock('@/lib/email', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('@/lib/email')>('@/lib/email')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
sendMentorBulkAssignmentEmail: vi.fn(async () => true),
|
||||||
|
sendTeamMentorIntroductionEmail: vi.fn(async () => true),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
import { prisma, createCaller } from '../setup'
|
||||||
|
import {
|
||||||
|
createTestProgram,
|
||||||
|
createTestCompetition,
|
||||||
|
createTestRound,
|
||||||
|
createTestProject,
|
||||||
|
createTestProjectRoundState,
|
||||||
|
createTestUser,
|
||||||
|
cleanupTestData,
|
||||||
|
uid,
|
||||||
|
} from '../helpers'
|
||||||
|
import { mentorRouter } from '../../src/server/routers/mentor'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression: a user whose MENTOR role is the PRIMARY role but whose roles[]
|
||||||
|
* array is empty (legacy/seeded records, e.g. prod user "Arnaud Blandin") must
|
||||||
|
* still be selectable + assignable on the mentor config page. Previously the
|
||||||
|
* candidate query filtered `roles: { has: 'MENTOR' }`, which excludes an empty
|
||||||
|
* roles[] even when role === 'MENTOR'.
|
||||||
|
*/
|
||||||
|
describe('mentor.getCandidates / bulkAssign — primary-role-only mentors are assignable', () => {
|
||||||
|
let programId = ''
|
||||||
|
let projectId = ''
|
||||||
|
let mentorId = ''
|
||||||
|
let adminId = ''
|
||||||
|
const userIds: string[] = []
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const program = await createTestProgram()
|
||||||
|
programId = program.id
|
||||||
|
const competition = await createTestCompetition(program.id)
|
||||||
|
const round = await createTestRound(competition.id, {
|
||||||
|
roundType: 'MENTORING',
|
||||||
|
status: 'ROUND_DRAFT', // draft → assignment emails are deferred
|
||||||
|
})
|
||||||
|
const project = await createTestProject(program.id)
|
||||||
|
projectId = project.id
|
||||||
|
await createTestProjectRoundState(project.id, round.id)
|
||||||
|
|
||||||
|
// The crux: primary role MENTOR, but roles[] is EMPTY.
|
||||||
|
const mentor = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
id: uid('u'),
|
||||||
|
email: `${uid('arnaud')}@test.local`,
|
||||||
|
name: 'Primary Only Mentor',
|
||||||
|
role: 'MENTOR',
|
||||||
|
roles: [],
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
mentorId = mentor.id
|
||||||
|
|
||||||
|
const admin = await createTestUser('SUPER_ADMIN')
|
||||||
|
adminId = admin.id
|
||||||
|
userIds.push(mentor.id, admin.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanupTestData(programId, userIds)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getCandidates includes a primary-role-only mentor', async () => {
|
||||||
|
const caller = createCaller(mentorRouter, {
|
||||||
|
id: adminId,
|
||||||
|
email: 'admin@test.local',
|
||||||
|
role: 'SUPER_ADMIN',
|
||||||
|
})
|
||||||
|
const res = await caller.getCandidates({ projectId })
|
||||||
|
const ids = res.candidates.map((c: { id: string }) => c.id)
|
||||||
|
expect(ids).toContain(mentorId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bulkAssign accepts a primary-role-only mentor and creates the assignment', async () => {
|
||||||
|
const caller = createCaller(mentorRouter, {
|
||||||
|
id: adminId,
|
||||||
|
email: 'admin@test.local',
|
||||||
|
role: 'SUPER_ADMIN',
|
||||||
|
})
|
||||||
|
// Must not throw "None of the selected users have the MENTOR role".
|
||||||
|
await caller.bulkAssign({ mentorIds: [mentorId], projectIds: [projectId] })
|
||||||
|
|
||||||
|
const assignment = await prisma.mentorAssignment.findFirst({
|
||||||
|
where: { projectId, mentorId },
|
||||||
|
})
|
||||||
|
expect(assignment).not.toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user