feat: add testing infrastructure — Vitest, Playwright, CI, git hooks
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.
This commit is contained in:
parent
036af95e00
commit
1e30ba23cd
35 changed files with 3637 additions and 5 deletions
165
tests/server/api/admin-role-patch.test.js
Normal file
165
tests/server/api/admin-role-patch.test.js
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
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 }
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue