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