refactor(board): atomic delete + query limit + composable cleanup
Some checks failed
Test / vitest (push) Failing after 7m17s
Test / playwright (push) Has been skipped
Test / visual (push) Has been skipped
Test / Notify on failure (push) Successful in 1s

Delete uses findOneAndDelete with author match (no TOCTOU window);
existence check only runs on miss to distinguish 403 vs 404. Posts
list capped at 200. Drop unused resolveTagChannel and refreshParams;
route slack URL building through the composable's slackUrl helper.
This commit is contained in:
Jennie Robinson Faber 2026-04-15 12:47:53 +01:00
parent d1a1484daf
commit 28040f44f4
7 changed files with 30 additions and 54 deletions

View file

@ -4,9 +4,11 @@ import { setResponseStatus } from 'h3'
vi.stubGlobal('setResponseStatus', setResponseStatus)
// --- Mocks ---
const { mockFind, mockFindById, mockSaveInstance } = vi.hoisted(() => ({
const { mockFind, mockFindById, mockFindOneAndDelete, mockExists, mockSaveInstance } = vi.hoisted(() => ({
mockFind: vi.fn(),
mockFindById: vi.fn(),
mockFindOneAndDelete: vi.fn(),
mockExists: vi.fn(),
mockSaveInstance: vi.fn(),
}))
@ -25,6 +27,8 @@ vi.mock('../../../server/models/boardPost.js', () => {
}
BoardPost.find = mockFind
BoardPost.findById = mockFindById
BoardPost.findOneAndDelete = mockFindOneAndDelete
BoardPost.exists = mockExists
return { default: BoardPost }
})
@ -63,6 +67,7 @@ const MEMBER_ID = 'member-abc'
function buildFindChain(result) {
const chain = {
sort: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnThis(),
populate: vi.fn().mockReturnThis(),
lean: vi.fn().mockResolvedValue(result),
}
@ -276,39 +281,31 @@ describe('DELETE /api/board/posts/[id]', () => {
})
it('deletes own post', async () => {
const deleteOne = vi.fn().mockResolvedValue({})
mockFindById.mockResolvedValue({
_id: 'post-1',
author: { toString: () => MEMBER_ID },
deleteOne,
})
mockFindOneAndDelete.mockResolvedValue({ _id: 'post-1' })
const event = createMockEvent({ method: 'DELETE', path: '/api/board/posts/post-1' })
event.context = { params: { id: 'post-1' } }
const result = await deleteHandler(event)
expect(deleteOne).toHaveBeenCalled()
expect(mockFindOneAndDelete).toHaveBeenCalledWith({ _id: 'post-1', author: MEMBER_ID })
expect(result).toEqual({ success: true })
})
it('rejects deleting another members post with 403', async () => {
const deleteOne = vi.fn()
mockFindById.mockResolvedValue({
_id: 'post-1',
author: { toString: () => 'someone-else' },
deleteOne,
})
mockFindOneAndDelete.mockResolvedValue(null)
mockExists.mockResolvedValue({ _id: 'post-1' })
const event = createMockEvent({ method: 'DELETE', path: '/api/board/posts/post-1' })
event.context = { params: { id: 'post-1' } }
await expect(deleteHandler(event)).rejects.toMatchObject({ statusCode: 403 })
expect(deleteOne).not.toHaveBeenCalled()
})
it('returns 404 when post not found', async () => {
mockFindById.mockResolvedValue(null)
mockFindOneAndDelete.mockResolvedValue(null)
mockExists.mockResolvedValue(null)
const event = createMockEvent({ method: 'DELETE', path: '/api/board/posts/missing' })
event.context = { params: { id: 'missing' } }