109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
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<typeof vi.fn> }
|
|
todo: { create: ReturnType<typeof vi.fn> }
|
|
}
|
|
const mockAuth = authenticateApiRequest as ReturnType<typeof vi.fn>
|
|
|
|
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)
|
|
})
|
|
})
|