import { describe, it, expect, vi, beforeEach } from 'vitest' vi.mock('@/lib/prisma', () => ({ prisma: { story: { findFirst: vi.fn(), }, task: { update: vi.fn(), }, $transaction: vi.fn(), }, })) vi.mock('@/lib/api-auth', () => ({ authenticateApiRequest: vi.fn(), })) import { prisma } from '@/lib/prisma' import { authenticateApiRequest } from '@/lib/api-auth' import { PATCH as patchReorder } from '@/app/api/stories/[id]/tasks/reorder/route' const mockPrisma = prisma as unknown as { story: { findFirst: ReturnType } task: { update: ReturnType } $transaction: ReturnType } const mockAuth = authenticateApiRequest as ReturnType function makeStory(taskIds: string[]) { return { id: 'story-1', product_id: 'prod-1', tasks: taskIds.map(id => ({ id })), } } function makeRequest(body: unknown, storyId = 'story-1'): [Request, { params: Promise<{ id: string }> }] { return [ new Request(`http://localhost/api/stories/${storyId}/tasks/reorder`, { method: 'PATCH', headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' }, body: JSON.stringify(body), }), { params: Promise.resolve({ id: storyId }) }, ] } describe('PATCH /api/stories/:id/tasks/reorder', () => { beforeEach(() => { vi.clearAllMocks() mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false }) mockPrisma.$transaction.mockResolvedValue([]) mockPrisma.task.update.mockResolvedValue({ id: 'task-1', sort_order: 1 }) }) // TC-RO-06 — body validation fires before story lookup it('returns 422 when task_ids is an empty array', async () => { const res = await patchReorder(...makeRequest({ task_ids: [] })) expect(res.status).toBe(422) expect(mockPrisma.story.findFirst).not.toHaveBeenCalled() }) // TC-RO-07 it('returns 422 when task_ids is not an array', async () => { const res = await patchReorder(...makeRequest({ task_ids: 'task-1' })) expect(res.status).toBe(422) expect(mockPrisma.story.findFirst).not.toHaveBeenCalled() }) it('returns 422 when task_ids is missing entirely', async () => { const res = await patchReorder(...makeRequest({})) expect(res.status).toBe(422) }) // TC-RO-08 it('returns 422 when task_ids contains an ID not belonging to the story', async () => { mockPrisma.story.findFirst.mockResolvedValue(makeStory(['task-1', 'task-2'])) const res = await patchReorder(...makeRequest({ task_ids: ['task-1', 'task-from-other-story'] })) const data = await res.json() expect(res.status).toBe(422) expect(data.error).toContain('task-from-other-story') }) // TC-RO-09 it('reorders tasks and returns 200 with success: true', async () => { mockPrisma.story.findFirst.mockResolvedValue(makeStory(['task-1', 'task-2', 'task-3'])) const res = await patchReorder(...makeRequest({ task_ids: ['task-3', 'task-1', 'task-2'] })) const data = await res.json() expect(res.status).toBe(200) expect(data).toEqual({ success: true }) expect(mockPrisma.$transaction).toHaveBeenCalled() }) it('updates each task with its new sort_order index', async () => { mockPrisma.story.findFirst.mockResolvedValue(makeStory(['task-1', 'task-2'])) await patchReorder(...makeRequest({ task_ids: ['task-2', 'task-1'] })) expect(mockPrisma.task.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: 'task-2' }, data: { sort_order: 1 } }) ) expect(mockPrisma.task.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: 'task-1' }, data: { sort_order: 2 } }) ) }) })