From 0b98fbcb89aaa73640d1eb6c3984951efceaed4f Mon Sep 17 00:00:00 2001 From: rissrice2105-agent Date: Tue, 30 Jun 2026 13:42:53 -0600 Subject: [PATCH] fix(files): resolve file id route params --- src/app/api/files/[fileId]/route.js | 21 +++--- src/app/api/files/[fileId]/route.test.js | 83 ++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 src/app/api/files/[fileId]/route.test.js diff --git a/src/app/api/files/[fileId]/route.js b/src/app/api/files/[fileId]/route.js index 660d8669..9962debf 100644 --- a/src/app/api/files/[fileId]/route.js +++ b/src/app/api/files/[fileId]/route.js @@ -3,6 +3,10 @@ import { NextResponse } from 'next/server'; import { createSupabaseServerClient } from '@/lib/supabase.js'; import { postQuantumEncryption } from '@/lib/crypto/post-quantum-encryption.js'; +async function resolveRouteParams(params) { + return (await params) || {}; +} + export async function GET(request, { params } = {}) { try { // Create Supabase server client @@ -14,7 +18,7 @@ export async function GET(request, { params } = {}) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const fileId = params.fileId; + const { fileId } = await resolveRouteParams(params); console.log(`📁 [FILE-DOWNLOAD] Download request from auth user: ${user.id} for file: ${fileId}`); // Get the internal user ID from the users table using auth_user_id @@ -117,7 +121,7 @@ export async function GET(request, { params } = {}) { const metadataObj = JSON.parse(decryptedMetadata); mimeType = metadataObj.mimeType || 'application/octet-stream'; } - } catch (metaError) { + } catch { console.warn('📁 [FILE-DOWNLOAD] Could not decrypt metadata for mimeType'); } @@ -146,7 +150,7 @@ export async function GET(request, { params } = {}) { } // Get file info without downloading the actual file content -export async function HEAD(event) { +export async function HEAD(request, { params } = {}) { try { // Create Supabase server client const supabase = await createSupabaseServerClient(); @@ -158,7 +162,7 @@ export async function HEAD(event) { } const userId = user.id; - const fileId = params.fileId; + const { fileId } = await resolveRouteParams(params); // Get file metadata from database const { data: fileData, error: fileError } = await supabase @@ -212,7 +216,7 @@ export async function POST(request, { params } = {}) { } const userId = user.id; - const fileId = params.fileId; + const { fileId } = await resolveRouteParams(params); console.log(`📁 [FILE-INFO] Info request from user: ${userId} for file: ${fileId}`); @@ -259,10 +263,3 @@ export async function POST(request, { params } = {}) { } } -function formatFileSize(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} \ No newline at end of file diff --git a/src/app/api/files/[fileId]/route.test.js b/src/app/api/files/[fileId]/route.test.js new file mode 100644 index 00000000..ec0f53fb --- /dev/null +++ b/src/app/api/files/[fileId]/route.test.js @@ -0,0 +1,83 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + from: vi.fn(), + eq: vi.fn() +})); + +vi.mock('@/lib/supabase.js', () => ({ + createSupabaseServerClient: vi.fn(async () => ({ + auth: { + getUser: mocks.authGetUser + }, + from: mocks.from + })) +})); + +vi.mock('@/lib/crypto/post-quantum-encryption.js', () => ({ + postQuantumEncryption: { + initialize: vi.fn(), + decryptFromSender: vi.fn() + } +})); + +function createFileQuery() { + const query = { + select: vi.fn(() => query), + eq: mocks.eq, + single: vi.fn().mockResolvedValue({ + data: { + id: 'file-1', + message_id: 'message-1', + encrypted_metadata: {}, + created_at: '2026-01-01T00:00:00.000Z', + created_by: 'user-1', + messages: [{ conversation_id: 'conversation-1' }] + }, + error: null + }) + }; + mocks.eq.mockReturnValue(query); + return query; +} + +describe('/api/files/[fileId]', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'user-1' } }, + error: null + }); + mocks.from.mockImplementation((table) => { + if (table === 'encrypted_files') return createFileQuery(); + throw new Error(`Unexpected table: ${table}`); + }); + }); + + it('resolves async route params for HEAD metadata requests', async () => { + const { HEAD } = await import('./route.js'); + const response = await HEAD(new Request('https://qrypt.chat/api/files/file-1'), { + params: Promise.resolve({ fileId: 'file-1' }) + }); + + expect(response.status).toBe(200); + expect(response.headers.get('Content-Type')).toBe('application/octet-stream'); + expect(mocks.eq).toHaveBeenCalledWith('id', 'file-1'); + }); + + it('resolves async route params for POST metadata requests', async () => { + const { POST } = await import('./route.js'); + const response = await POST(new Request('https://qrypt.chat/api/files/file-1', { method: 'POST' }), { + params: Promise.resolve({ fileId: 'file-1' }) + }); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.id).toBe('file-1'); + expect(body.conversationId).toBe('conversation-1'); + expect(mocks.eq).toHaveBeenCalledWith('id', 'file-1'); + }); +});