import { describe, it, expect, vi, beforeEach } from 'vitest' // Mock prisma vi.mock('@/lib/prisma', () => ({ prisma: { product: { findMany: vi.fn(), }, task: { findFirst: vi.fn(), update: vi.fn(), }, apiToken: { findUnique: vi.fn(), }, }, })) // Mock api-auth to control which user is "authenticated" vi.mock('@/lib/api-auth', () => ({ authenticateApiRequest: vi.fn(), })) import { prisma } from '@/lib/prisma' import { authenticateApiRequest } from '@/lib/api-auth' import { GET as getProducts } from '@/app/api/products/route' import { PATCH as patchTask } from '@/app/api/tasks/[id]/route' const mockPrisma = prisma as unknown as { product: { findMany: ReturnType } task: { findFirst: ReturnType; update: ReturnType } } const mockAuth = authenticateApiRequest as ReturnType function makeRequest(method = 'GET', body?: unknown): Request { return new Request('http://localhost/api/test', { method, headers: { 'Authorization': 'Bearer test-token', 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }) } describe('Security: cross-user access', () => { beforeEach(() => { vi.clearAllMocks() }) describe('GET /api/products', () => { it('returns only the authenticated user\'s products', async () => { mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false }) mockPrisma.product.findMany.mockResolvedValue([ { id: 'prod-1', name: 'Product A', repo_url: null }, ]) const response = await getProducts(makeRequest()) const data = await response.json() expect(response.status).toBe(200) expect(data).toHaveLength(1) // Verify the query filtered by user_id expect(mockPrisma.product.findMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ user_id: 'user-1' }), }) ) }) it('returns 401 when no valid token provided', async () => { mockAuth.mockResolvedValue({ error: 'Unauthorized', status: 401 }) const response = await getProducts(makeRequest()) expect(response.status).toBe(401) }) }) describe('PATCH /api/tasks/:id', () => { it('returns 403 when task belongs to a different user', async () => { // User 2 is authenticated but the task belongs to user 1 mockAuth.mockResolvedValue({ userId: 'user-2', isDemo: false }) mockPrisma.task.findFirst.mockResolvedValue({ id: 'task-1', story: { product: { user_id: 'user-1', // different user! }, }, }) const response = await patchTask( makeRequest('PATCH', { status: 'DONE' }), { params: Promise.resolve({ id: 'task-1' }) } ) expect(response.status).toBe(403) const data = await response.json() expect(data.error).toBeTruthy() }) it('returns 403 for demo users', async () => { mockAuth.mockResolvedValue({ userId: 'demo-user', isDemo: true }) const response = await patchTask( makeRequest('PATCH', { status: 'DONE' }), { params: Promise.resolve({ id: 'task-1' }) } ) expect(response.status).toBe(403) }) it('allows update when task belongs to the authenticated user', async () => { mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false }) mockPrisma.task.findFirst.mockResolvedValue({ id: 'task-1', story: { product: { user_id: 'user-1', // same user }, }, }) mockPrisma.task.update.mockResolvedValue({ id: 'task-1', status: 'DONE' }) const response = await patchTask( makeRequest('PATCH', { status: 'DONE' }), { params: Promise.resolve({ id: 'task-1' }) } ) expect(response.status).toBe(200) }) it('returns 404 when task does not exist', async () => { mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false }) mockPrisma.task.findFirst.mockResolvedValue(null) const response = await patchTask( makeRequest('PATCH', { status: 'DONE' }), { params: Promise.resolve({ id: 'nonexistent' }) } ) expect(response.status).toBe(404) }) it('returns 401 when no valid token', async () => { mockAuth.mockResolvedValue({ error: 'Unauthorized', status: 401 }) const response = await patchTask( makeRequest('PATCH', { status: 'DONE' }), { params: Promise.resolve({ id: 'task-1' }) } ) expect(response.status).toBe(401) }) }) })