import { describe, it, expect, vi, beforeEach } from 'vitest' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' vi.mock('../../../server/models/member.js', () => ({ default: { findByIdAndUpdate: vi.fn() } })) vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) vi.mock('../../../server/utils/validateBody.js', () => ({ validateBody: vi.fn() })) vi.mock('../../../server/utils/schemas.js', () => ({ adminRoleUpdateSchema: {} })) import handler from '../../../server/api/admin/members/[id]/role.patch.js' import Member from '../../../server/models/member.js' import { validateBody } from '../../../server/utils/validateBody.js' import { createMockEvent } from '../helpers/createMockEvent.js' describe('admin role PATCH endpoint', () => { beforeEach(() => { vi.clearAllMocks() requireAdmin.mockResolvedValue({ _id: { toString: () => 'admin-123' } }) validateBody.mockResolvedValue({ role: 'member' }) vi.stubGlobal('getRouterParam', vi.fn().mockReturnValue('target-member-id')) }) describe('source inspection', () => { const source = readFileSync( resolve(import.meta.dirname, '../../../server/api/admin/members/[id]/role.patch.js'), 'utf-8' ) it('calls requireAdmin before validateBody', () => { const adminIndex = source.indexOf('requireAdmin(event)') const validateIndex = source.indexOf('validateBody(event') expect(adminIndex).toBeGreaterThan(-1) expect(validateIndex).toBeGreaterThan(-1) expect(adminIndex).toBeLessThan(validateIndex) }) }) describe('auth', () => { it('rejects non-admin users', async () => { requireAdmin.mockRejectedValue( createError({ statusCode: 403, statusMessage: 'Forbidden' }) ) const event = createMockEvent({ method: 'PATCH', path: '/api/admin/members/target-member-id/role', body: { role: 'member' } }) await expect(handler(event)).rejects.toMatchObject({ statusCode: 403 }) }) }) describe('self-demotion', () => { it('returns 400 when admin tries to remove their own admin role', async () => { requireAdmin.mockResolvedValue({ _id: { toString: () => 'admin-123' } }) validateBody.mockResolvedValue({ role: 'member' }) vi.stubGlobal('getRouterParam', vi.fn().mockReturnValue('admin-123')) const event = createMockEvent({ method: 'PATCH', path: '/api/admin/members/admin-123/role', body: { role: 'member' } }) await expect(handler(event)).rejects.toMatchObject({ statusCode: 400, statusMessage: 'You cannot remove your own admin role.' }) }) }) describe('validation', () => { it('rejects invalid role via validateBody', async () => { validateBody.mockRejectedValue( createError({ statusCode: 400, statusMessage: 'Invalid role' }) ) const event = createMockEvent({ method: 'PATCH', path: '/api/admin/members/target-member-id/role', body: { role: 'superadmin' } }) await expect(handler(event)).rejects.toMatchObject({ statusCode: 400 }) }) }) describe('member not found', () => { it('returns 404 when member does not exist', async () => { Member.findByIdAndUpdate.mockResolvedValue(null) const event = createMockEvent({ method: 'PATCH', path: '/api/admin/members/nonexistent-id/role', body: { role: 'member' } }) await expect(handler(event)).rejects.toMatchObject({ statusCode: 404, statusMessage: 'Member not found.' }) }) }) describe('successful role changes', () => { it('promotes a member to admin', async () => { validateBody.mockResolvedValue({ role: 'admin' }) const updatedMember = { _id: 'target-member-id', role: 'admin', name: 'Test User' } Member.findByIdAndUpdate.mockResolvedValue(updatedMember) const event = createMockEvent({ method: 'PATCH', path: '/api/admin/members/target-member-id/role', body: { role: 'admin' } }) const result = await handler(event) expect(result).toEqual({ success: true, member: updatedMember }) expect(Member.findByIdAndUpdate).toHaveBeenCalledWith( 'target-member-id', { role: 'admin' }, { new: true } ) }) it('demotes a member to regular role', async () => { validateBody.mockResolvedValue({ role: 'member' }) const updatedMember = { _id: 'target-member-id', role: 'member', name: 'Test User' } Member.findByIdAndUpdate.mockResolvedValue(updatedMember) const event = createMockEvent({ method: 'PATCH', path: '/api/admin/members/target-member-id/role', body: { role: 'member' } }) const result = await handler(event) expect(result).toEqual({ success: true, member: updatedMember }) expect(Member.findByIdAndUpdate).toHaveBeenCalledWith( 'target-member-id', { role: 'member' }, { new: true } ) }) }) })