import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import { prisma, createCaller } from '../setup' import { createTestUser, cleanupTestData, uid } from '../helpers' import { userRouter } from '../../src/server/routers/user' import * as emailModule from '../../src/lib/email' import type { UserRole } from '@prisma/client' async function createUserWithRoles(primaryRole: UserRole, rolesArray: UserRole[]) { const id = uid('user') return prisma.user.create({ data: { id, email: `${id}@test.local`, name: `Test ${primaryRole}`, role: primaryRole, roles: rolesArray, status: 'ACTIVE', }, }) } describe('user.bulkUpdateRoles', () => { const userIds: string[] = [] afterAll(async () => { if (userIds.length > 0) { await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } }) await prisma.decisionAuditLog.deleteMany({ where: { actorId: { in: userIds } } }) await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) beforeEach(() => { vi.restoreAllMocks() }) it('adds MENTOR role to multiple users and sends one onboarding email each', async () => { const admin = await createTestUser('SUPER_ADMIN') const j1 = await createUserWithRoles('JURY_MEMBER', ['JURY_MEMBER']) const j2 = await createUserWithRoles('JURY_MEMBER', ['JURY_MEMBER']) userIds.push(admin.id, j1.id, j2.id) const sendSpy = vi.spyOn(emailModule, 'sendMentorOnboardingEmail').mockResolvedValue() const caller = createCaller(userRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) const result = await caller.bulkUpdateRoles({ userIds: [j1.id, j2.id], addRole: 'MENTOR', }) expect(result.updated).toBe(2) expect(result.alreadyHadRole).toBe(0) expect(sendSpy).toHaveBeenCalledTimes(2) const updatedJ1 = await prisma.user.findUniqueOrThrow({ where: { id: j1.id } }) expect(updatedJ1.roles).toContain('MENTOR') expect(updatedJ1.roles).toContain('JURY_MEMBER') expect(updatedJ1.mentorOnboardingSentAt).not.toBeNull() }) it('is idempotent — second call with same input does NOT resend onboarding email', async () => { const admin = await createTestUser('SUPER_ADMIN') const j = await createUserWithRoles('JURY_MEMBER', ['JURY_MEMBER']) userIds.push(admin.id, j.id) const sendSpy = vi.spyOn(emailModule, 'sendMentorOnboardingEmail').mockResolvedValue() const caller = createCaller(userRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) await caller.bulkUpdateRoles({ userIds: [j.id], addRole: 'MENTOR' }) expect(sendSpy).toHaveBeenCalledTimes(1) const second = await caller.bulkUpdateRoles({ userIds: [j.id], addRole: 'MENTOR' }) expect(sendSpy).toHaveBeenCalledTimes(1) // still 1 expect(second.updated).toBe(0) expect(second.alreadyHadRole).toBe(1) }) it('removeRole strips the role; does not affect onboarding stamp', async () => { const admin = await createTestUser('SUPER_ADMIN') const m = await createUserWithRoles('MENTOR', ['MENTOR', 'JURY_MEMBER']) userIds.push(admin.id, m.id) const caller = createCaller(userRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) const result = await caller.bulkUpdateRoles({ userIds: [m.id], removeRole: 'JURY_MEMBER' }) expect(result.updated).toBe(1) const updated = await prisma.user.findUniqueOrThrow({ where: { id: m.id } }) expect(updated.roles).toEqual(['MENTOR']) }) it('refuses to remove SUPER_ADMIN from self', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) await prisma.user.update({ where: { id: admin.id }, data: { roles: ['SUPER_ADMIN'] } }) const caller = createCaller(userRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) await expect( caller.bulkUpdateRoles({ userIds: [admin.id], removeRole: 'SUPER_ADMIN' }), ).rejects.toThrow(/cannot remove SUPER_ADMIN from self|self-demote/i) }) })