diff --git a/__tests__/actions/todos-promote-idea.test.ts b/__tests__/actions/todos-promote-idea.test.ts deleted file mode 100644 index 7ddb169..0000000 --- a/__tests__/actions/todos-promote-idea.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const { mockSession } = vi.hoisted(() => ({ - mockSession: { userId: 'user-1', isDemo: false }, -})) - -vi.mock('next/cache', () => ({ revalidatePath: vi.fn() })) -vi.mock('next/headers', () => ({ cookies: vi.fn().mockResolvedValue({}) })) -vi.mock('iron-session', () => ({ - getIronSession: vi.fn().mockImplementation(async () => mockSession), -})) -vi.mock('@/lib/session', () => ({ - sessionOptions: { cookieName: 'test', password: 'test-password-32-chars-minimum-len' }, -})) -vi.mock('@/lib/idea-code-server', () => ({ - nextIdeaCode: vi.fn().mockResolvedValue('IDEA-005'), -})) -vi.mock('@/lib/product-access', () => ({ - productAccessFilter: vi.fn().mockReturnValue({}), -})) -vi.mock('@/lib/code-server', () => ({ - generateNextPbiCode: vi.fn(), - generateNextStoryCode: vi.fn(), -})) -vi.mock('@/lib/rate-limit', () => ({ - enforceUserRateLimit: vi.fn().mockReturnValue(null), -})) -vi.mock('@/lib/prisma', () => ({ - prisma: { - todo: { - findFirst: vi.fn(), - update: vi.fn(), - }, - idea: { - create: vi.fn(), - }, - ideaLog: { create: vi.fn() }, - $transaction: vi.fn(), - }, -})) - -import { prisma } from '@/lib/prisma' -import { promoteTodoToIdeaAction } from '@/actions/todos' - -type M = { - todo: { findFirst: ReturnType; update: ReturnType } - idea: { create: ReturnType } - ideaLog: { create: ReturnType } - $transaction: ReturnType -} -const m = prisma as unknown as M - -beforeEach(() => { - vi.clearAllMocks() - mockSession.userId = 'user-1' - mockSession.isDemo = false - m.$transaction.mockImplementation(async (arg: unknown) => { - if (typeof arg === 'function') { - return (arg as (tx: unknown) => unknown)(m) - } - return arg - }) -}) - -describe('promoteTodoToIdeaAction', () => { - it('happy: archives todo, creates DRAFT idea, returns idea_id', async () => { - m.todo.findFirst.mockResolvedValueOnce({ - id: 'todo-1', - title: 'My idea', - description: 'desc', - product_id: null, - archived: false, - }) - m.idea.create.mockResolvedValueOnce({ id: 'idea-9', code: 'IDEA-005' }) - - const r = await promoteTodoToIdeaAction('todo-1') - expect(r).toMatchObject({ success: true, idea_id: 'idea-9', idea_code: 'IDEA-005' }) - expect(m.todo.update).toHaveBeenCalledWith({ - where: { id: 'todo-1' }, - data: { archived: true }, - }) - }) - - it('rejects unauthenticated', async () => { - mockSession.userId = '' - const r = await promoteTodoToIdeaAction('todo-1') - expect(r).toMatchObject({ code: 401 }) - }) - - it('rejects demo-user', async () => { - mockSession.isDemo = true - const r = await promoteTodoToIdeaAction('todo-1') - expect(r).toMatchObject({ code: 403 }) - }) - - it('returns 404 when todo belongs to another user', async () => { - m.todo.findFirst.mockResolvedValueOnce(null) - const r = await promoteTodoToIdeaAction('todo-1') - expect(r).toMatchObject({ code: 404 }) - }) - - it('rejects already-archived todo', async () => { - m.todo.findFirst.mockResolvedValueOnce({ - id: 'todo-1', - title: 'x', - description: null, - product_id: null, - archived: true, - }) - const r = await promoteTodoToIdeaAction('todo-1') - expect(r).toMatchObject({ code: 422 }) - expect(m.idea.create).not.toHaveBeenCalled() - }) -}) diff --git a/__tests__/api/security.test.ts b/__tests__/api/security.test.ts index 4d37fdd..6266cda 100644 --- a/__tests__/api/security.test.ts +++ b/__tests__/api/security.test.ts @@ -41,7 +41,6 @@ import { GET as getSprintTasks } from '@/app/api/sprints/[id]/tasks/route' import { PATCH as patchReorder } from '@/app/api/stories/[id]/tasks/reorder/route' import { POST as postStoryLog } from '@/app/api/stories/[id]/log/route' import { PATCH as patchTask } from '@/app/api/tasks/[id]/route' -import { POST as postTodo } from '@/app/api/todos/route' const mockPrisma = prisma as unknown as { product: { findMany: ReturnType; findFirst: ReturnType } @@ -419,46 +418,3 @@ describe('PATCH /api/tasks/:id', () => { expect(res.status).toBe(200) }) }) - -// ─── POST /api/todos ────────────────────────────────────────────────────────── - -describe('POST /api/todos', () => { - // product_id is required by the Zod schema (z.string().min(1)) - const VALID_BODY = { title: 'Test todo', product_id: 'prod-1' } - - // TC-TD-01 - it('returns 401 when no valid token provided', async () => { - mockAuth.mockResolvedValue(UNAUTHORIZED) - const res = await postTodo(makePost('http://localhost/api/todos', VALID_BODY)) - expect(res.status).toBe(401) - }) - - // TC-TD-03 - it('returns 403 for demo users', async () => { - mockAuth.mockResolvedValue(DEMO_AUTH) - const res = await postTodo(makePost('http://localhost/api/todos', VALID_BODY)) - expect(res.status).toBe(403) - const data = await res.json() - expect(data.error).toBe('Niet beschikbaar in demo-modus') - }) - - // TC-TD-08 - it('returns 404 when product_id belongs to another user', async () => { - mockAuth.mockResolvedValue(USER_2_AUTH) - mockPrisma.product.findFirst.mockResolvedValue(null) - - const res = await postTodo( - makePost('http://localhost/api/todos', { title: 'Todo', product_id: 'prod-owned-by-user-1' }) - ) - expect(res.status).toBe(404) - // Verify it queries by user_id, not productAccessFilter - expect(mockPrisma.product.findFirst).toHaveBeenCalledWith( - expect.objectContaining({ - where: expect.objectContaining({ - id: 'prod-owned-by-user-1', - user_id: 'user-2', - }), - }) - ) - }) -}) diff --git a/__tests__/api/todos.test.ts b/__tests__/api/todos.test.ts deleted file mode 100644 index abded32..0000000 --- a/__tests__/api/todos.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -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 422 when title is missing', async () => { - const res = await postTodo(makeRequest({ product_id: 'prod-1' })) - expect(res.status).toBe(422) - }) - - // TC-TD-05 - it('returns 422 when title is empty string', async () => { - const res = await postTodo(makeRequest({ title: '', product_id: 'prod-1' })) - expect(res.status).toBe(422) - }) - - it('returns 422 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(422) - }) - - it('returns 422 when product_id is empty string', async () => { - const res = await postTodo(makeRequest({ title: 'My todo', product_id: '' })) - expect(res.status).toBe(422) - }) - - // 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) - }) -})