diff --git a/__tests__/api/dispute-evidence.test.ts b/__tests__/api/dispute-evidence.test.ts index 179a10d..e1bc2f8 100644 --- a/__tests__/api/dispute-evidence.test.ts +++ b/__tests__/api/dispute-evidence.test.ts @@ -1,153 +1,237 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { NextRequest } from 'next/server' +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' +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/auth/session", () => ({ + readAccessToken: vi.fn().mockReturnValue("token"), + verifyAccessToken: vi + .fn() + .mockReturnValue({ walletAddress: "GABC", jti: "jti-1" }), +})); -vi.mock('@/lib/db', () => ({ +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')), -})) +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")), + removeEncryptedFile: vi.fn().mockResolvedValue(undefined), +})); -import { sql } from '@/lib/db' +import { sql } from "@/lib/db"; -type SqlMock = ReturnType +type SqlMock = ReturnType; function queueSql(responses: unknown[]) { - const mock = sql as unknown as SqlMock + const mock = sql as unknown as SqlMock; for (const response of responses) { - mock.mockResolvedValueOnce(response) + mock.mockResolvedValueOnce(response); } } -function makeContext(disputeId = '42', evidenceId = '9') { - return { params: Promise.resolve({ id: disputeId, evidenceId }) } +function makeContext(disputeId = "42", evidenceId = "9") { + return { params: Promise.resolve({ id: disputeId, evidenceId }) }; } function makeGetRequest(url: string) { - return new NextRequest(new Request(url)) + return new NextRequest(new Request(url)); } -function makePostRequest(url: string, files: File[], description = '') { - const formData = new FormData() +function makePostRequest(url: string, files: File[], description = "") { + const formData = new FormData(); for (const file of files) { - Object.defineProperty(file, 'arrayBuffer', { + Object.defineProperty(file, "arrayBuffer", { configurable: true, - value: vi.fn().mockResolvedValue(new TextEncoder().encode('content').buffer), - }) - formData.append('files', file) + value: vi + .fn() + .mockResolvedValue(new TextEncoder().encode("content").buffer), + }); + formData.append("files", file); } - if (description) formData.append('description', description) + if (description) formData.append("description", description); return { nextUrl: new URL(url), formData: vi.fn().mockResolvedValue(formData), - } as unknown as NextRequest + } as unknown as NextRequest; } beforeEach(() => { - vi.clearAllMocks() - ;(sql as unknown as SqlMock).mockReset() -}) + 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 () => { +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: 3, role: "client" }], [{ id: 42, client_id: 1, freelancer_id: 2 }], - ]) + ]); - const response = await GET(makeGetRequest('http://localhost/api/disputes/42/evidence'), makeContext()) + 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') - }) + 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 () => { + it("returns evidence in chronological order for an authorized party", async () => { queueSql([ - [{ id: 1, role: 'client' }], + [{ 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' }], - ]) + [{ total: 1 }], + [{ 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()) + 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') - }) -}) + 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 () => { + it("includes pagination metadata when a limit is supplied", async () => { + queueSql([ + [{ id: 1, role: "client" }], + [{ id: 42, client_id: 1, freelancer_id: 2 }], + [{ total: 14 }], + [{ id: 9, file_name: "proof.pdf", created_at: "2026-01-01T00:00:00Z" }], + [], + ]); + + const response = await GET( + makeGetRequest( + "http://localhost/api/disputes/42/evidence?limit=10&offset=0", + ), + makeContext(), + ); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.pagination).toMatchObject({ + limit: 10, + offset: 0, + total: 14, + hasMore: true, + }); + expect(body.evidence).toHaveLength(1); + }); +}); + +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(['