diff --git a/__tests__/api/dispute-evidence.test.ts b/__tests__/api/dispute-evidence.test.ts new file mode 100644 index 0000000..179a10d --- /dev/null +++ b/__tests__/api/dispute-evidence.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { NextRequest } from 'next/server' + +import { GET, POST } from '@/app/api/disputes/[id]/evidence/route' +import { GET as downloadEvidence } from '@/app/api/disputes/[id]/evidence/[evidenceId]/route' + +vi.mock('@/lib/auth/session', () => ({ + readAccessToken: vi.fn().mockReturnValue('token'), + verifyAccessToken: vi.fn().mockReturnValue({ walletAddress: 'GABC', jti: 'jti-1' }), +})) + +vi.mock('@/lib/db', () => ({ + sql: vi.fn(), +})) + +vi.mock('@/lib/security/fileEncryption', () => ({ + computeFileHash: vi.fn().mockReturnValue('dummy-hash'), + storeEncryptedFile: vi.fn().mockResolvedValue({ iv: 'dummy-iv', filePath: '/tmp/evidence-file' }), + readEncryptedFile: vi.fn().mockResolvedValue(Buffer.from('file-content')), +})) + +import { sql } from '@/lib/db' + +type SqlMock = ReturnType + +function queueSql(responses: unknown[]) { + const mock = sql as unknown as SqlMock + for (const response of responses) { + mock.mockResolvedValueOnce(response) + } +} + +function makeContext(disputeId = '42', evidenceId = '9') { + return { params: Promise.resolve({ id: disputeId, evidenceId }) } +} + +function makeGetRequest(url: string) { + return new NextRequest(new Request(url)) +} + +function makePostRequest(url: string, files: File[], description = '') { + const formData = new FormData() + for (const file of files) { + Object.defineProperty(file, 'arrayBuffer', { + configurable: true, + value: vi.fn().mockResolvedValue(new TextEncoder().encode('content').buffer), + }) + formData.append('files', file) + } + if (description) formData.append('description', description) + return { + nextUrl: new URL(url), + formData: vi.fn().mockResolvedValue(formData), + } as unknown as NextRequest +} + +beforeEach(() => { + vi.clearAllMocks() + ;(sql as unknown as SqlMock).mockReset() +}) + +describe('GET /api/disputes/[id]/evidence', () => { + it('returns 403 when the user is not a dispute participant or admin', async () => { + queueSql([ + [{ id: 3, role: 'client' }], + [{ id: 42, client_id: 1, freelancer_id: 2 }], + ]) + + const response = await GET(makeGetRequest('http://localhost/api/disputes/42/evidence'), makeContext()) + + expect(response.status).toBe(403) + const body = await response.json() + expect(body.code).toBe('FORBIDDEN') + }) + + it('returns evidence in chronological order for an authorized party', async () => { + queueSql([ + [{ id: 1, role: 'client' }], + [{ id: 42, client_id: 1, freelancer_id: 2 }], + [{ id: 9, file_name: 'proof.pdf', created_at: '2026-01-01T00:00:00Z' }], + ]) + + const response = await GET(makeGetRequest('http://localhost/api/disputes/42/evidence'), makeContext()) + + expect(response.status).toBe(200) + const body = await response.json() + expect(body.evidence).toHaveLength(1) + expect(body.evidence[0].file_name).toBe('proof.pdf') + }) +}) + +describe('POST /api/disputes/[id]/evidence', () => { + it('rejects unsupported file types before storage', async () => { + const request = makePostRequest( + 'http://localhost/api/disputes/42/evidence', + [new File(['