diff --git a/src/import/csv.test.ts b/src/import/csv.test.ts new file mode 100644 index 0000000..d51fd56 --- /dev/null +++ b/src/import/csv.test.ts @@ -0,0 +1,236 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { importTableFromCsvRoute } from './csv' +import { executeOperation } from '../export' +import type { DataSource } from '../types' +import type { StarbaseDBConfiguration } from '../handler' + +vi.mock('../export', () => ({ + executeOperation: vi.fn(), +})) + +vi.mock('../utils', () => ({ + createResponse: vi.fn( + (data, message, status) => + new Response(JSON.stringify({ result: data, error: message }), { + status, + headers: { 'Content-Type': 'application/json' }, + }) + ), +})) + +let mockDataSource: DataSource +let mockConfig: StarbaseDBConfiguration + +beforeEach(() => { + vi.clearAllMocks() + + mockDataSource = { + source: 'external', + external: { dialect: 'sqlite' }, + rpc: { executeQuery: vi.fn() }, + } as any + + mockConfig = { + outerbaseApiKey: 'mock-api-key', + role: 'admin', + features: { allowlist: true, rls: true, rest: true }, + } +}) + +describe('CSV Import Module', () => { + it('should return 400 when the request body is empty', async () => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': 'text/csv' }, + }) + + const response = await importTableFromCsvRoute( + 'users', + request, + mockDataSource, + mockConfig + ) + + expect(response.status).toBe(400) + const jsonResponse = (await response.json()) as { error?: string } + expect(jsonResponse.error).toBe('Request body is empty') + expect(executeOperation).not.toHaveBeenCalled() + }) + + it('should return 400 for unsupported Content-Type', async () => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: 'id,name\n1,Alice', + }) + + const response = await importTableFromCsvRoute( + 'users', + request, + mockDataSource, + mockConfig + ) + + expect(response.status).toBe(400) + const jsonResponse = (await response.json()) as { error?: string } + expect(jsonResponse.error).toBe('Unsupported Content-Type') + expect(executeOperation).not.toHaveBeenCalled() + }) + + it('should return 400 if no CSV file is uploaded', async () => { + const formData = new FormData() + const request = new Request('http://localhost', { + method: 'POST', + body: formData, + }) + + const response = await importTableFromCsvRoute( + 'users', + request, + mockDataSource, + mockConfig + ) + + expect(response.status).toBe(400) + const jsonResponse = (await response.json()) as { error?: string } + expect(jsonResponse.error).toBe('No file uploaded') + expect(executeOperation).not.toHaveBeenCalled() + }) + + it('should reject empty CSV data', async () => { + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': 'text/csv' }, + body: 'id,name\n', + }) + + const response = await importTableFromCsvRoute( + 'users', + request, + mockDataSource, + mockConfig + ) + + expect(response.status).toBe(400) + const jsonResponse = (await response.json()) as { error?: string } + expect(jsonResponse.error).toBe('Invalid CSV format or empty data') + expect(executeOperation).not.toHaveBeenCalled() + }) + + it('should import raw text/csv rows', async () => { + vi.mocked(executeOperation).mockResolvedValue([]) + + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': 'text/csv' }, + body: 'id,name\n1,Alice\n2,Bob', + }) + + const response = await importTableFromCsvRoute( + 'users', + request, + mockDataSource, + mockConfig + ) + + expect(response.status).toBe(200) + expect(executeOperation).toHaveBeenNthCalledWith( + 1, + [ + { + sql: 'INSERT INTO users (id, name) VALUES (?, ?)', + params: ['1', 'Alice'], + }, + ], + mockDataSource, + mockConfig + ) + expect(executeOperation).toHaveBeenNthCalledWith( + 2, + [ + { + sql: 'INSERT INTO users (id, name) VALUES (?, ?)', + params: ['2', 'Bob'], + }, + ], + mockDataSource, + mockConfig + ) + const jsonResponse = (await response.json()) as { + result: { message: string; failedStatements: any[] } + } + expect(jsonResponse.result.message).toBe( + 'Imported 2 out of 2 records successfully. 0 records failed.' + ) + expect(jsonResponse.result.failedStatements).toEqual([]) + }) + + it('should apply column mappings from JSON-wrapped CSV data', async () => { + vi.mocked(executeOperation).mockResolvedValue([]) + + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + data: 'full_name,years\nAlice,30', + columnMapping: { full_name: 'name', years: 'age' }, + }), + }) + + const response = await importTableFromCsvRoute( + 'users', + request, + mockDataSource, + mockConfig + ) + + expect(response.status).toBe(200) + expect(executeOperation).toHaveBeenCalledWith( + [ + { + sql: 'INSERT INTO users (name, age) VALUES (?, ?)', + params: ['Alice', '30'], + }, + ], + mockDataSource, + mockConfig + ) + }) + + it('should report failed statements while continuing later rows', async () => { + vi.mocked(executeOperation) + .mockResolvedValueOnce([]) + .mockRejectedValueOnce(new Error('Database Error')) + .mockResolvedValueOnce([]) + + const request = new Request('http://localhost', { + method: 'POST', + headers: { 'Content-Type': 'text/csv' }, + body: 'id,name\n1,Alice\n2,Bob\n3,Carol', + }) + + const response = await importTableFromCsvRoute( + 'users', + request, + mockDataSource, + mockConfig + ) + + expect(response.status).toBe(200) + const jsonResponse = (await response.json()) as { + result: { + message: string + failedStatements: { statement: string; error: string }[] + } + } + expect(jsonResponse.result.message).toBe( + 'Imported 2 out of 3 records successfully. 1 records failed.' + ) + expect(jsonResponse.result.failedStatements).toEqual([ + { + statement: 'INSERT INTO users (id, name) VALUES (?, ?)', + error: 'Database Error', + }, + ]) + }) +})