Add comprehensive testing covering 420 unit/handler tests across 24 Vitest files, 9 Playwright E2E specs, accessibility scans, and visual regression. Includes GitHub Actions CI, Husky pre-push hook, and TESTING.md docs.
165 lines
5 KiB
JavaScript
165 lines
5 KiB
JavaScript
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 }
|
|
)
|
|
})
|
|
})
|
|
})
|