test(board): unit + e2e tests for board posts and channels
This commit is contained in:
parent
f3df1945bd
commit
5fb069a80e
4 changed files with 707 additions and 0 deletions
239
tests/server/api/board-channels.test.js
Normal file
239
tests/server/api/board-channels.test.js
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { setResponseStatus } from 'h3'
|
||||
|
||||
vi.stubGlobal('setResponseStatus', setResponseStatus)
|
||||
|
||||
const { mockFind, mockCreate, mockFindByIdAndUpdate, mockFindByIdAndDelete } = vi.hoisted(() => ({
|
||||
mockFind: vi.fn(),
|
||||
mockCreate: vi.fn(),
|
||||
mockFindByIdAndUpdate: vi.fn(),
|
||||
mockFindByIdAndDelete: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../../server/models/boardChannel.js', () => ({
|
||||
default: {
|
||||
find: mockFind,
|
||||
create: mockCreate,
|
||||
findByIdAndUpdate: mockFindByIdAndUpdate,
|
||||
findByIdAndDelete: mockFindByIdAndDelete,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../../server/utils/auth.js', () => ({
|
||||
requireAuth: vi.fn(),
|
||||
requireAdmin: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../../server/utils/validateBody.js', () => ({
|
||||
validateBody: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../../server/utils/schemas.js', () => ({
|
||||
boardChannelCreateSchema: {},
|
||||
boardChannelUpdateSchema: {},
|
||||
}))
|
||||
|
||||
vi.mock('../../../server/utils/mongoose.js', () => ({
|
||||
connectDB: vi.fn(),
|
||||
}))
|
||||
|
||||
import { requireAuth, requireAdmin } from '../../../server/utils/auth.js'
|
||||
import { validateBody } from '../../../server/utils/validateBody.js'
|
||||
import getHandler from '../../../server/api/board/channels.get.js'
|
||||
import postHandler from '../../../server/api/admin/board-channels.post.js'
|
||||
import patchHandler from '../../../server/api/admin/board-channels/[id].patch.js'
|
||||
import deleteHandler from '../../../server/api/admin/board-channels/[id].delete.js'
|
||||
import { createMockEvent } from '../helpers/createMockEvent.js'
|
||||
|
||||
describe('GET /api/board/channels', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
requireAuth.mockResolvedValue({ _id: 'member-1' })
|
||||
})
|
||||
|
||||
it('returns channels for authenticated member', async () => {
|
||||
const channels = [{ _id: 'c1', name: 'coop' }]
|
||||
const chain = {
|
||||
sort: vi.fn().mockReturnThis(),
|
||||
lean: vi.fn().mockResolvedValue(channels),
|
||||
}
|
||||
mockFind.mockReturnValue(chain)
|
||||
|
||||
const event = createMockEvent({ method: 'GET', path: '/api/board/channels' })
|
||||
const result = await getHandler(event)
|
||||
|
||||
expect(mockFind).toHaveBeenCalledWith({})
|
||||
expect(chain.sort).toHaveBeenCalledWith({ name: 1 })
|
||||
expect(result).toEqual({ channels })
|
||||
})
|
||||
|
||||
it('requires auth (401)', async () => {
|
||||
requireAuth.mockRejectedValue(
|
||||
createError({ statusCode: 401, statusMessage: 'Unauthorized' }),
|
||||
)
|
||||
const event = createMockEvent({ method: 'GET', path: '/api/board/channels' })
|
||||
await expect(getHandler(event)).rejects.toMatchObject({ statusCode: 401 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/admin/board-channels', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' })
|
||||
})
|
||||
|
||||
it('creates channel when admin sends valid data', async () => {
|
||||
validateBody.mockResolvedValue({
|
||||
name: 'coop-formation',
|
||||
slackChannelId: 'C01234ABC',
|
||||
tagSlugs: ['coop-formation'],
|
||||
})
|
||||
const created = {
|
||||
_id: 'new-channel',
|
||||
name: 'coop-formation',
|
||||
slackChannelId: 'C01234ABC',
|
||||
tagSlugs: ['coop-formation'],
|
||||
toObject() {
|
||||
return {
|
||||
_id: this._id,
|
||||
name: this.name,
|
||||
slackChannelId: this.slackChannelId,
|
||||
tagSlugs: this.tagSlugs,
|
||||
}
|
||||
},
|
||||
}
|
||||
mockCreate.mockResolvedValue(created)
|
||||
|
||||
const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
|
||||
const result = await postHandler(event)
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith({
|
||||
name: 'coop-formation',
|
||||
slackChannelId: 'C01234ABC',
|
||||
tagSlugs: ['coop-formation'],
|
||||
})
|
||||
expect(result.channel.slackChannelId).toBe('C01234ABC')
|
||||
})
|
||||
|
||||
it('rejects missing required fields with 400', async () => {
|
||||
validateBody.mockRejectedValue(
|
||||
createError({ statusCode: 400, statusMessage: 'Validation failed' }),
|
||||
)
|
||||
|
||||
const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
|
||||
await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 400 })
|
||||
})
|
||||
|
||||
it('rejects non-admin with 403', async () => {
|
||||
requireAdmin.mockRejectedValue(
|
||||
createError({ statusCode: 403, statusMessage: 'Forbidden' }),
|
||||
)
|
||||
|
||||
const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
|
||||
await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 403 })
|
||||
})
|
||||
|
||||
it('returns 409 on duplicate slackChannelId', async () => {
|
||||
validateBody.mockResolvedValue({
|
||||
name: 'x',
|
||||
slackChannelId: 'C01234ABC',
|
||||
tagSlugs: [],
|
||||
})
|
||||
const dupErr = Object.assign(new Error('dup'), { code: 11000 })
|
||||
mockCreate.mockRejectedValue(dupErr)
|
||||
|
||||
const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
|
||||
await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 409 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATCH /api/admin/board-channels/[id]', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' })
|
||||
})
|
||||
|
||||
it('updates a channel', async () => {
|
||||
validateBody.mockResolvedValue({ name: 'renamed' })
|
||||
const updated = {
|
||||
_id: 'c1',
|
||||
name: 'renamed',
|
||||
toObject() {
|
||||
return { _id: this._id, name: this.name }
|
||||
},
|
||||
}
|
||||
mockFindByIdAndUpdate.mockResolvedValue(updated)
|
||||
|
||||
const event = createMockEvent({ method: 'PATCH', path: '/api/admin/board-channels/c1' })
|
||||
event.context = { params: { id: 'c1' } }
|
||||
|
||||
const result = await patchHandler(event)
|
||||
|
||||
expect(mockFindByIdAndUpdate).toHaveBeenCalledWith(
|
||||
'c1',
|
||||
{ $set: { name: 'renamed' } },
|
||||
{ new: true, runValidators: true },
|
||||
)
|
||||
expect(result.channel.name).toBe('renamed')
|
||||
})
|
||||
|
||||
it('returns 404 when channel not found', async () => {
|
||||
validateBody.mockResolvedValue({ name: 'x' })
|
||||
mockFindByIdAndUpdate.mockResolvedValue(null)
|
||||
|
||||
const event = createMockEvent({ method: 'PATCH', path: '/api/admin/board-channels/missing' })
|
||||
event.context = { params: { id: 'missing' } }
|
||||
|
||||
await expect(patchHandler(event)).rejects.toMatchObject({ statusCode: 404 })
|
||||
})
|
||||
|
||||
it('rejects non-admin with 403', async () => {
|
||||
requireAdmin.mockRejectedValue(
|
||||
createError({ statusCode: 403, statusMessage: 'Forbidden' }),
|
||||
)
|
||||
|
||||
const event = createMockEvent({ method: 'PATCH', path: '/api/admin/board-channels/c1' })
|
||||
event.context = { params: { id: 'c1' } }
|
||||
|
||||
await expect(patchHandler(event)).rejects.toMatchObject({ statusCode: 403 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /api/admin/board-channels/[id]', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' })
|
||||
})
|
||||
|
||||
it('deletes a channel', async () => {
|
||||
mockFindByIdAndDelete.mockResolvedValue({ _id: 'c1' })
|
||||
|
||||
const event = createMockEvent({ method: 'DELETE', path: '/api/admin/board-channels/c1' })
|
||||
event.context = { params: { id: 'c1' } }
|
||||
|
||||
const result = await deleteHandler(event)
|
||||
|
||||
expect(mockFindByIdAndDelete).toHaveBeenCalledWith('c1')
|
||||
expect(result).toEqual({ success: true })
|
||||
})
|
||||
|
||||
it('returns 404 when channel not found', async () => {
|
||||
mockFindByIdAndDelete.mockResolvedValue(null)
|
||||
|
||||
const event = createMockEvent({ method: 'DELETE', path: '/api/admin/board-channels/missing' })
|
||||
event.context = { params: { id: 'missing' } }
|
||||
|
||||
await expect(deleteHandler(event)).rejects.toMatchObject({ statusCode: 404 })
|
||||
})
|
||||
|
||||
it('rejects non-admin with 403', async () => {
|
||||
requireAdmin.mockRejectedValue(
|
||||
createError({ statusCode: 403, statusMessage: 'Forbidden' }),
|
||||
)
|
||||
|
||||
const event = createMockEvent({ method: 'DELETE', path: '/api/admin/board-channels/c1' })
|
||||
event.context = { params: { id: 'c1' } }
|
||||
|
||||
await expect(deleteHandler(event)).rejects.toMatchObject({ statusCode: 403 })
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue