fix(members): role tabs/filter include users with secondary roles

user.list and user.listInvitableIds filtered on the singular User.role column,
so the type tabs (Jury/Mentor/…) omitted users holding that role as a secondary
role (User.roles[]). Match the role as primary OR secondary (roles hasSome),
combined with search via AND, mirroring userHasRole / hasRole middleware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-01 18:51:11 +02:00
parent 040e5ff9a9
commit d4a77f63d3
2 changed files with 117 additions and 12 deletions

View File

@@ -0,0 +1,93 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { prisma, createCaller } from '../setup'
import { createTestUser, uid } from '../helpers'
import { userRouter } from '../../src/server/routers/user'
/**
* Regression: the admin Members tabs filter by type (Jury, Mentor, …) must
* include users who hold that type as a SECONDARY role (User.roles[]), not only
* users whose PRIMARY role (User.role) matches. Previously the query filtered on
* `where.role` alone, so multi-role users were missing from the relevant tab.
*/
describe('user.list / listInvitableIds — role filter includes secondary roles', () => {
const token = uid('secrole')
const ids: string[] = []
let adminId = ''
let aId = '' // primary JURY_MEMBER + secondary MENTOR
let bId = '' // primary MENTOR
let cId = '' // JURY only (control)
beforeAll(async () => {
const admin = await createTestUser('SUPER_ADMIN')
adminId = admin.id
ids.push(admin.id)
const a = await prisma.user.create({
data: {
id: uid('u'),
email: `${token}-a@test.local`,
name: `${token} A`,
role: 'JURY_MEMBER',
roles: ['JURY_MEMBER', 'MENTOR'],
status: 'NONE',
},
})
const b = await prisma.user.create({
data: {
id: uid('u'),
email: `${token}-b@test.local`,
name: `${token} B`,
role: 'MENTOR',
roles: ['MENTOR'],
status: 'NONE',
},
})
const c = await prisma.user.create({
data: {
id: uid('u'),
email: `${token}-c@test.local`,
name: `${token} C`,
role: 'JURY_MEMBER',
roles: ['JURY_MEMBER'],
status: 'NONE',
},
})
aId = a.id
bId = b.id
cId = c.id
ids.push(a.id, b.id, c.id)
})
afterAll(async () => {
await prisma.user.deleteMany({ where: { id: { in: ids } } })
})
it('list: the Mentor tab includes a user whose MENTOR role is secondary', async () => {
const caller = createCaller(userRouter, {
id: adminId,
email: 'admin@test.local',
role: 'SUPER_ADMIN',
})
const res = await caller.list({ roles: ['MENTOR'], search: token, perPage: 100 })
const returnedIds = res.users.map((u: { id: string }) => u.id)
expect(returnedIds).toContain(bId) // primary mentor (already worked)
expect(returnedIds).toContain(aId) // secondary mentor (the bug)
expect(returnedIds).not.toContain(cId) // jury-only must NOT appear
})
it('listInvitableIds: the Mentor tab includes a user whose MENTOR role is secondary', async () => {
const caller = createCaller(userRouter, {
id: adminId,
email: 'admin@test.local',
role: 'SUPER_ADMIN',
})
const res = await caller.listInvitableIds({ roles: ['MENTOR'], search: token })
expect(res.userIds).toContain(bId)
expect(res.userIds).toContain(aId)
expect(res.userIds).not.toContain(cId)
})
})