From ab1889a1685eca4f53989b18c74c53280b969701 Mon Sep 17 00:00:00 2001 From: rissrice2105-agent Date: Mon, 29 Jun 2026 22:58:33 -0600 Subject: [PATCH] fix(files): resolve async file route params --- src/app/api/files/[fileId]/encrypted/route.js | 8 +- .../files/[fileId]/encrypted/route.test.js | 102 ++++++++++++++++++ .../api/files/message/[messageId]/route.js | 13 +-- .../files/message/[messageId]/route.test.js | 82 ++++++++++++++ 4 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 src/app/api/files/[fileId]/encrypted/route.test.js create mode 100644 src/app/api/files/message/[messageId]/route.test.js diff --git a/src/app/api/files/[fileId]/encrypted/route.js b/src/app/api/files/[fileId]/encrypted/route.js index fda278bd..d1a424ab 100644 --- a/src/app/api/files/[fileId]/encrypted/route.js +++ b/src/app/api/files/[fileId]/encrypted/route.js @@ -4,6 +4,11 @@ import { createSupabaseServerClient } from '@/lib/supabase.js'; export async function GET(request, { params } = {}) { try { + const { fileId } = (await params) || {}; + if (!fileId) { + return NextResponse.json({ error: 'File ID is required' }, { status: 400 }); + } + // Create Supabase server client const supabase = await createSupabaseServerClient(); @@ -13,7 +18,6 @@ export async function GET(request, { params } = {}) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const fileId = params.fileId; console.log(`📁 [ENCRYPTED-FILE] Request from auth user: ${user.id} for file: ${fileId}`); // Get the internal user ID from the users table using auth_user_id @@ -93,4 +97,4 @@ export async function GET(request, { params } = {}) { console.error( '📁 [ENCRYPTED-FILE] ❌ Unexpected error:', err); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/src/app/api/files/[fileId]/encrypted/route.test.js b/src/app/api/files/[fileId]/encrypted/route.test.js new file mode 100644 index 00000000..95c02738 --- /dev/null +++ b/src/app/api/files/[fileId]/encrypted/route.test.js @@ -0,0 +1,102 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + from: vi.fn(), + storageFrom: vi.fn(), + download: vi.fn(), + usersEq: vi.fn(), + filesEq: vi.fn() +})); + +vi.mock('@/lib/supabase.js', () => ({ + createSupabaseServerClient: vi.fn(async () => ({ + auth: { + getUser: mocks.authGetUser + }, + from: mocks.from, + storage: { + from: mocks.storageFrom + } + })) +})); + +function createUsersQuery() { + const query = { + select: vi.fn(() => query), + eq: mocks.usersEq, + single: vi.fn().mockResolvedValue({ + data: { id: 'internal-user-id' }, + error: null + }) + }; + mocks.usersEq.mockReturnValue(query); + return query; +} + +function createFilesQuery() { + const query = { + select: vi.fn(() => query), + eq: mocks.filesEq, + single: vi.fn().mockResolvedValue({ + data: { + id: 'file-1', + message_id: 'message-1', + storage_path: 'encrypted/file-1', + encrypted_metadata: {}, + created_at: '2026-06-30T00:00:00Z' + }, + error: null + }) + }; + mocks.filesEq.mockReturnValue(query); + return query; +} + +describe('GET /api/files/[fileId]/encrypted', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + vi.spyOn(console, 'log').mockImplementation(() => {}); + + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + mocks.from.mockImplementation((table) => { + if (table === 'users') return createUsersQuery(); + if (table === 'encrypted_files') return createFilesQuery(); + throw new Error(`Unexpected table: ${table}`); + }); + mocks.download.mockResolvedValue({ + data: { text: vi.fn().mockResolvedValue('{"internal-user-id":"ciphertext"}') }, + error: null + }); + mocks.storageFrom.mockReturnValue({ + download: mocks.download + }); + }); + + it('resolves async route params before filtering by file id', async () => { + const { GET } = await import('./route.js'); + const response = await GET(new Request('https://qrypt.chat/api/files/file-1/encrypted'), { + params: Promise.resolve({ fileId: 'file-1' }) + }); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.file.id).toBe('file-1'); + expect(mocks.filesEq).toHaveBeenCalledWith('id', 'file-1'); + expect(mocks.download).toHaveBeenCalledWith('encrypted/file-1'); + }); + + it('rejects missing file ids before auth work', async () => { + const { GET } = await import('./route.js'); + const response = await GET(new Request('https://qrypt.chat/api/files//encrypted'), { + params: Promise.resolve({}) + }); + + expect(response.status).toBe(400); + expect(mocks.authGetUser).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/api/files/message/[messageId]/route.js b/src/app/api/files/message/[messageId]/route.js index fda0c806..919d6552 100644 --- a/src/app/api/files/message/[messageId]/route.js +++ b/src/app/api/files/message/[messageId]/route.js @@ -4,6 +4,11 @@ import { createSupabaseServerClient } from '@/lib/supabase.js'; export async function GET(request, { params } = {}) { try { + const { messageId } = (await params) || {}; + if (!messageId) { + return NextResponse.json({ error: 'Message ID is required' }, { status: 400 }); + } + // Create Supabase server client const supabase = await createSupabaseServerClient(); @@ -13,7 +18,6 @@ export async function GET(request, { params } = {}) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const messageId = params.messageId; console.log(`📁 [FILES-BY-MESSAGE] Request from auth user: ${user.id} for message: ${messageId}`); // Get the internal user ID from the users table using auth_user_id @@ -87,10 +91,3 @@ export async function GET(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/message/[messageId]/route.test.js b/src/app/api/files/message/[messageId]/route.test.js new file mode 100644 index 00000000..628d51eb --- /dev/null +++ b/src/app/api/files/message/[messageId]/route.test.js @@ -0,0 +1,82 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authGetUser: vi.fn(), + from: vi.fn(), + usersEq: vi.fn(), + filesEq: vi.fn() +})); + +vi.mock('@/lib/supabase.js', () => ({ + createSupabaseServerClient: vi.fn(async () => ({ + auth: { + getUser: mocks.authGetUser + }, + from: mocks.from + })) +})); + +function createUsersQuery() { + const query = { + select: vi.fn(() => query), + eq: mocks.usersEq, + single: vi.fn().mockResolvedValue({ + data: { id: 'internal-user-id' }, + error: null + }) + }; + mocks.usersEq.mockReturnValue(query); + return query; +} + +function createFilesQuery() { + const query = { + select: vi.fn(() => query), + eq: mocks.filesEq + }; + mocks.filesEq.mockImplementation((field) => { + if (field === 'messages.conversations.conversation_participants.user_id') { + return Promise.resolve({ data: [], error: null }); + } + return query; + }); + return query; +} + +describe('GET /api/files/message/[messageId]', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + vi.spyOn(console, 'log').mockImplementation(() => {}); + + mocks.authGetUser.mockResolvedValue({ + data: { user: { id: 'auth-user-id' } }, + error: null + }); + mocks.from.mockImplementation((table) => { + if (table === 'users') return createUsersQuery(); + if (table === 'encrypted_files') return createFilesQuery(); + throw new Error(`Unexpected table: ${table}`); + }); + }); + + it('resolves async route params before filtering by message id', async () => { + const { GET } = await import('./route.js'); + const response = await GET(new Request('https://qrypt.chat/api/files/message/message-1'), { + params: Promise.resolve({ messageId: 'message-1' }) + }); + + expect(response.status).toBe(200); + expect(mocks.filesEq).toHaveBeenCalledWith('message_id', 'message-1'); + }); + + it('rejects missing message ids before auth work', async () => { + const { GET } = await import('./route.js'); + const response = await GET(new Request('https://qrypt.chat/api/files/message/'), { + params: Promise.resolve({}) + }); + + expect(response.status).toBe(400); + expect(mocks.authGetUser).not.toHaveBeenCalled(); + }); +});