diff --git a/__tests__/api/tasks.test.ts b/__tests__/api/tasks.test.ts index 619ecb5..b5eb05f 100644 --- a/__tests__/api/tasks.test.ts +++ b/__tests__/api/tasks.test.ts @@ -22,6 +22,19 @@ const mockPrisma = prisma as unknown as { } 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}`, { @@ -36,23 +49,98 @@ function makeRequest(body: unknown, taskId = 'task-1'): [Request, { params: Prom 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.todo('returns 400 for invalid status value') + it('returns 400 for invalid status value', async () => { + const res = await patchTask(...makeRequest({ status: 'INVALID' })) + expect(res.status).toBe(400) + }) // TC-T-07 - it.todo('returns 400 when body has no recognized fields') + it('returns 400 when body has no recognized fields', async () => { + const res = await patchTask(...makeRequest({})) + expect(res.status).toBe(400) + }) + + it('returns 400 when body has only unrecognized fields', async () => { + const res = await patchTask(...makeRequest({ unknown_field: 'value' })) + expect(res.status).toBe(400) + }) // TC-T-08 - it.todo('updates status only and returns 200') + 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.todo('updates implementation_plan only and returns 200') + 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.todo('updates both status and implementation_plan and returns 200') + 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.todo('allows update when user is a team member of the product') + it('allows update when user is a team member (not product owner)', async () => { + // task belongs to user-2's product, but user-1 is a member + 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('all three valid status values are accepted', async () => { + for (const status of ['TO_DO', 'IN_PROGRESS', 'DONE'] as const) { + mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status, implementation_plan: null }) + const res = await patchTask(...makeRequest({ status })) + expect(res.status).toBe(200) + } + }) })