Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions src/import/csv.test.ts
Original file line number Diff line number Diff line change
@@ -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',
},
])
})
})