feat: add testing infrastructure — Vitest, Playwright, CI, git hooks
Some checks are pending
Test / vitest (push) Waiting to run
Test / playwright (push) Blocked by required conditions
Test / visual (push) Blocked by required conditions

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:
Jennie Robinson Faber 2026-04-04 16:07:21 +01:00
parent 036af95e00
commit 1e30ba23cd
35 changed files with 3637 additions and 5 deletions

View 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 }
)
})
})
})