import { describe, it, expect, vi, beforeEach } from 'vitest' vi.mock('@/lib/prisma', () => ({ prisma: { task: { findFirst: vi.fn(), update: vi.fn(), }, }, })) vi.mock('@/lib/api-auth', () => ({ authenticateApiRequest: vi.fn(), })) import { prisma } from '@/lib/prisma' import { authenticateApiRequest } from '@/lib/api-auth' import { PATCH as patchTask } from '@/app/api/tasks/[id]/route' const mockPrisma = prisma as unknown as { task: { findFirst: ReturnType; update: ReturnType } } const mockAuth = authenticateApiRequest as ReturnType function makeTask(overrides: { userId?: string; membersLength?: number } = {}) { const { userId = 'user-1', membersLength = 0 } = overrides return { id: 'task-1', story: { product: { user_id: userId, members: Array.from({ length: membersLength }, (_, i) => ({ id: `member-${i}` })), }, }, } } function makeRequest(body: unknown, taskId = 'task-1'): [Request, { params: Promise<{ id: string }> }] { return [ new Request(`http://localhost/api/tasks/${taskId}`, { method: 'PATCH', headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' }, body: JSON.stringify(body), }), { params: Promise.resolve({ id: taskId }) }, ] } describe('PATCH /api/tasks/:id', () => { beforeEach(() => { vi.clearAllMocks() mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false }) mockPrisma.task.findFirst.mockResolvedValue(makeTask()) mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'DONE', implementation_plan: null, }) }) // TC-T-06 it('returns 422 for invalid status value', async () => { const res = await patchTask(...makeRequest({ status: 'INVALID' })) expect(res.status).toBe(422) }) // TC-T-07 it('returns 422 when body has no recognized fields', async () => { const res = await patchTask(...makeRequest({})) expect(res.status).toBe(422) }) it('returns 422 when body has only unrecognized fields', async () => { const res = await patchTask(...makeRequest({ unknown_field: 'value' })) expect(res.status).toBe(422) }) // TC-T-08 it('updates status only and returns 200 with updated task', async () => { mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'IN_PROGRESS', implementation_plan: null }) const res = await patchTask(...makeRequest({ status: 'in_progress' })) const data = await res.json() expect(res.status).toBe(200) expect(data).toMatchObject({ id: 'task-1', status: 'in_progress' }) expect(mockPrisma.task.update).toHaveBeenCalledWith( expect.objectContaining({ data: { status: 'IN_PROGRESS' }, }) ) }) // TC-T-09 it('updates implementation_plan only and returns 200', async () => { const plan = 'Step 1: implement. Step 2: test.' mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'TO_DO', implementation_plan: plan }) const res = await patchTask(...makeRequest({ implementation_plan: plan })) const data = await res.json() expect(res.status).toBe(200) expect(data.implementation_plan).toBe(plan) expect(mockPrisma.task.update).toHaveBeenCalledWith( expect.objectContaining({ data: { implementation_plan: plan }, }) ) }) // TC-T-10 it('updates both status and implementation_plan and returns 200', async () => { const plan = 'Full plan here.' mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'DONE', implementation_plan: plan }) const res = await patchTask(...makeRequest({ status: 'done', implementation_plan: plan })) const data = await res.json() expect(res.status).toBe(200) expect(data).toMatchObject({ status: 'done', implementation_plan: plan }) expect(mockPrisma.task.update).toHaveBeenCalledWith( expect.objectContaining({ data: { status: 'DONE', implementation_plan: plan }, }) ) }) // TC-T-11 it('allows update when user is a team member (not product owner)', async () => { mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false }) mockPrisma.task.findFirst.mockResolvedValue(makeTask({ userId: 'user-2', membersLength: 1 })) mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'DONE', implementation_plan: null }) const res = await patchTask(...makeRequest({ status: 'done' })) expect(res.status).toBe(200) }) it('the three patchable status values are accepted (review is rejected)', async () => { for (const apiStatus of ['todo', 'in_progress', 'done'] as const) { const dbStatus = { todo: 'TO_DO', in_progress: 'IN_PROGRESS', done: 'DONE' }[apiStatus] mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: dbStatus, implementation_plan: null }) const res = await patchTask(...makeRequest({ status: apiStatus })) expect(res.status).toBe(200) } const reviewRes = await patchTask(...makeRequest({ status: 'review' })) expect(reviewRes.status).toBe(422) }) it('returns 400 for malformed JSON', async () => { const req = new Request('http://localhost/api/tasks/task-1', { method: 'PATCH', headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' }, body: '{not json', }) const res = await patchTask(req, { params: Promise.resolve({ id: 'task-1' }) }) expect(res.status).toBe(400) }) })