import { describe, it, expect, vi, beforeEach } from 'vitest' vi.mock('@/lib/prisma', () => ({ prisma: { product: { findFirst: vi.fn(), }, todo: { create: vi.fn(), }, }, })) vi.mock('@/lib/api-auth', () => ({ authenticateApiRequest: vi.fn(), })) import { prisma } from '@/lib/prisma' import { authenticateApiRequest } from '@/lib/api-auth' import { POST as postTodo } from '@/app/api/todos/route' const mockPrisma = prisma as unknown as { product: { findFirst: ReturnType } todo: { create: ReturnType } } const mockAuth = authenticateApiRequest as ReturnType const PRODUCT = { id: 'prod-1', name: 'DevPlanner', archived: false, user_id: 'user-1' } const TODO_RESULT = { id: 'todo-1', title: 'Test todo', created_at: new Date('2026-04-30T10:00:00Z') } function makeRequest(body: unknown): Request { return new Request('http://localhost/api/todos', { method: 'POST', headers: { Authorization: 'Bearer test-token', 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) } describe('POST /api/todos', () => { beforeEach(() => { vi.clearAllMocks() mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false }) mockPrisma.product.findFirst.mockResolvedValue(PRODUCT) mockPrisma.todo.create.mockResolvedValue(TODO_RESULT) }) // TC-TD-04 it('returns 400 when title is missing', async () => { const res = await postTodo(makeRequest({ product_id: 'prod-1' })) expect(res.status).toBe(400) }) // TC-TD-05 it('returns 400 when title is empty string', async () => { const res = await postTodo(makeRequest({ title: '', product_id: 'prod-1' })) expect(res.status).toBe(400) }) it('returns 400 when product_id is missing', async () => { // product_id is required by the Zod schema (z.string().min(1)) const res = await postTodo(makeRequest({ title: 'My todo' })) expect(res.status).toBe(400) }) it('returns 400 when product_id is empty string', async () => { const res = await postTodo(makeRequest({ title: 'My todo', product_id: '' })) expect(res.status).toBe(400) }) // TC-TD-07 it('creates todo with valid product_id and returns 201', async () => { const res = await postTodo(makeRequest({ title: 'Test todo', product_id: 'prod-1' })) const data = await res.json() expect(res.status).toBe(201) expect(data).toMatchObject({ id: 'todo-1', title: 'Test todo' }) expect(data).toHaveProperty('created_at') expect(mockPrisma.todo.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ user_id: 'user-1', product_id: 'prod-1', title: 'Test todo', }), }) ) }) it('queries product by user_id (not productAccessFilter) to enforce ownership', async () => { await postTodo(makeRequest({ title: 'Test todo', product_id: 'prod-1' })) expect(mockPrisma.product.findFirst).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ id: 'prod-1', user_id: 'user-1', archived: false, }), }) ) }) it('returns 404 when product does not exist or is archived', async () => { mockPrisma.product.findFirst.mockResolvedValue(null) const res = await postTodo(makeRequest({ title: 'My todo', product_id: 'nonexistent' })) expect(res.status).toBe(404) }) })