feat(board): redesign classifieds + Slack channel creation
Adds AdminGhost bot token for admin-only Slack channel creation, refreshes BoardPostCard/Form layouts, and expands admin board-channels management.
This commit is contained in:
parent
6f3d088763
commit
9a560f2a3b
14 changed files with 544 additions and 158 deletions
|
|
@ -3,8 +3,9 @@ import { setResponseStatus } from 'h3'
|
|||
|
||||
vi.stubGlobal('setResponseStatus', setResponseStatus)
|
||||
|
||||
const { mockFind, mockCreate, mockFindByIdAndUpdate, mockFindByIdAndDelete } = vi.hoisted(() => ({
|
||||
const { mockFind, mockFindOne, mockCreate, mockFindByIdAndUpdate, mockFindByIdAndDelete } = vi.hoisted(() => ({
|
||||
mockFind: vi.fn(),
|
||||
mockFindOne: vi.fn(),
|
||||
mockCreate: vi.fn(),
|
||||
mockFindByIdAndUpdate: vi.fn(),
|
||||
mockFindByIdAndDelete: vi.fn(),
|
||||
|
|
@ -13,6 +14,7 @@ const { mockFind, mockCreate, mockFindByIdAndUpdate, mockFindByIdAndDelete } = v
|
|||
vi.mock('../../../server/models/boardChannel.js', () => ({
|
||||
default: {
|
||||
find: mockFind,
|
||||
findOne: mockFindOne,
|
||||
create: mockCreate,
|
||||
findByIdAndUpdate: mockFindByIdAndUpdate,
|
||||
findByIdAndDelete: mockFindByIdAndDelete,
|
||||
|
|
@ -37,6 +39,16 @@ vi.mock('../../../server/utils/mongoose.js', () => ({
|
|||
connectDB: vi.fn(),
|
||||
}))
|
||||
|
||||
const { mockCreateSlackChannel } = vi.hoisted(() => ({
|
||||
mockCreateSlackChannel: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../../server/utils/slack.ts', () => ({
|
||||
getSlackServiceNoVetting: () => ({
|
||||
createChannel: mockCreateSlackChannel,
|
||||
}),
|
||||
}))
|
||||
|
||||
import { requireAuth, requireAdmin } from '../../../server/utils/auth.js'
|
||||
import { validateBody } from '../../../server/utils/validateBody.js'
|
||||
import getHandler from '../../../server/api/board/channels.get.js'
|
||||
|
|
@ -80,6 +92,7 @@ describe('POST /api/admin/board-channels', () => {
|
|||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' })
|
||||
mockFindOne.mockReturnValue({ lean: vi.fn().mockResolvedValue(null) })
|
||||
})
|
||||
|
||||
it('creates channel when admin sends valid data', async () => {
|
||||
|
|
@ -145,12 +158,66 @@ describe('POST /api/admin/board-channels', () => {
|
|||
const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
|
||||
await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 409 })
|
||||
})
|
||||
|
||||
it('creates Slack channel via API when slackChannelId not provided', async () => {
|
||||
validateBody.mockResolvedValue({
|
||||
name: 'coop-formation',
|
||||
tagSlugs: [],
|
||||
})
|
||||
mockCreateSlackChannel.mockResolvedValue({ id: 'C_NEW_123', name: 'coop-formation' })
|
||||
const created = {
|
||||
_id: 'new-ch',
|
||||
name: 'coop-formation',
|
||||
slackChannelId: 'C_NEW_123',
|
||||
tagSlugs: [],
|
||||
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(mockCreateSlackChannel).toHaveBeenCalledWith('coop-formation')
|
||||
expect(mockCreate).toHaveBeenCalledWith({
|
||||
name: 'coop-formation',
|
||||
slackChannelId: 'C_NEW_123',
|
||||
tagSlugs: [],
|
||||
})
|
||||
expect(result.channel.slackChannelId).toBe('C_NEW_123')
|
||||
})
|
||||
|
||||
it('returns 409 when a tag is already mapped to another channel', async () => {
|
||||
validateBody.mockResolvedValue({
|
||||
name: 'new-ch',
|
||||
slackChannelId: 'C99999',
|
||||
tagSlugs: ['coop-formation'],
|
||||
})
|
||||
mockFindOne.mockReturnValue({
|
||||
lean: vi.fn().mockResolvedValue({
|
||||
_id: 'existing',
|
||||
name: 'old-ch',
|
||||
tagSlugs: ['coop-formation'],
|
||||
}),
|
||||
})
|
||||
|
||||
const event = createMockEvent({ method: 'POST', path: '/api/admin/board-channels' })
|
||||
await expect(postHandler(event)).rejects.toMatchObject({ statusCode: 409 })
|
||||
expect(mockCreate).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATCH /api/admin/board-channels/[id]', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' })
|
||||
mockFindOne.mockReturnValue({ lean: vi.fn().mockResolvedValue(null) })
|
||||
})
|
||||
|
||||
it('updates a channel', async () => {
|
||||
|
|
@ -187,6 +254,23 @@ describe('PATCH /api/admin/board-channels/[id]', () => {
|
|||
await expect(patchHandler(event)).rejects.toMatchObject({ statusCode: 404 })
|
||||
})
|
||||
|
||||
it('returns 409 when PATCH assigns a tag already owned by another channel', async () => {
|
||||
validateBody.mockResolvedValue({ tagSlugs: ['coop-formation'] })
|
||||
mockFindOne.mockReturnValue({
|
||||
lean: vi.fn().mockResolvedValue({
|
||||
_id: 'other',
|
||||
name: 'other-ch',
|
||||
tagSlugs: ['coop-formation'],
|
||||
}),
|
||||
})
|
||||
|
||||
const event = createMockEvent({ method: 'PATCH', path: '/api/admin/board-channels/c1' })
|
||||
event.context = { params: { id: 'c1' } }
|
||||
|
||||
await expect(patchHandler(event)).rejects.toMatchObject({ statusCode: 409 })
|
||||
expect(mockFindByIdAndUpdate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects non-admin with 403', async () => {
|
||||
requireAdmin.mockRejectedValue(
|
||||
createError({ statusCode: 403, statusMessage: 'Forbidden' }),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue