diff --git a/src/allowlist/index.test.ts b/src/allowlist/index.test.ts new file mode 100644 index 0000000..5549f1b --- /dev/null +++ b/src/allowlist/index.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi } from 'vitest' +import { isQueryAllowed } from './index' +import { DataSource } from '../types' +import { StarbaseDBConfiguration } from '../handler' + +describe('isQueryAllowed', () => { + const createMockDataSource = (rows: any[] = []): DataSource => ({ + source: 'main', + rpc: { + executeQuery: vi.fn().mockResolvedValue(rows), + executeBatch: vi.fn(), + }, + }) + + it('returns true immediately when allowlist is disabled', async () => { + const ds = createMockDataSource() + const result = await isQueryAllowed({ + sql: 'SELECT * FROM users', + isEnabled: false, + dataSource: ds, + config: { role: 'user' } as StarbaseDBConfiguration, + }) + expect(result).toBe(true) + }) + + it('returns true immediately when role is admin', async () => { + const ds = createMockDataSource() + const result = await isQueryAllowed({ + sql: 'DROP TABLE users', + isEnabled: true, + dataSource: ds, + config: { role: 'admin' } as StarbaseDBConfiguration, + }) + expect(result).toBe(true) + }) + + it('allows matching SQL query regardless of trailing semicolon', async () => { + const ds = createMockDataSource([ + { sql_statement: 'SELECT id, name FROM users', source: 'main' }, + ]) + const result = await isQueryAllowed({ + sql: 'SELECT id, name FROM users;', + isEnabled: true, + dataSource: ds, + config: { role: 'user' } as StarbaseDBConfiguration, + }) + expect(result).toBe(true) + }) + + it('throws Query not allowed for unlisted SQL statement', async () => { + const ds = createMockDataSource([ + { sql_statement: 'SELECT id FROM users', source: 'main' }, + ]) + await expect( + isQueryAllowed({ + sql: 'DELETE FROM users', + isEnabled: true, + dataSource: ds, + config: { role: 'user' } as StarbaseDBConfiguration, + }) + ).rejects.toThrow('Query not allowed') + }) +}) diff --git a/src/import/csv.test.ts b/src/import/csv.test.ts new file mode 100644 index 0000000..3f8c68e --- /dev/null +++ b/src/import/csv.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { importTableFromCsvRoute } from './csv' +import { executeOperation } from '../export' +import { DataSource } from '../types' +import { StarbaseDBConfiguration } from '../handler' + +vi.mock('../export', () => ({ + executeOperation: vi.fn(), +})) + +describe('importTableFromCsvRoute', () => { + const mockDataSource: DataSource = { + source: 'test_source', + rpc: { + executeQuery: vi.fn(), + executeBatch: vi.fn(), + }, + } + const mockConfig: StarbaseDBConfiguration = { + role: 'admin', + } as any + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns 400 when request body is empty', async () => { + const req = new Request('http://localhost/import/csv', { method: 'POST' }) + const res = await importTableFromCsvRoute('users', req, mockDataSource, mockConfig) + expect(res.status).toBe(400) + const json: any = await res.json() + expect(json.error).toBe('Request body is empty') + }) + + it('returns 400 on unsupported Content-Type', async () => { + const req = new Request('http://localhost/import/csv', { + method: 'POST', + headers: { 'Content-Type': 'text/plain' }, + body: 'id,name\n1,Alice', + }) + const res = await importTableFromCsvRoute('users', req, mockDataSource, mockConfig) + expect(res.status).toBe(400) + const json: any = await res.json() + expect(json.error).toBe('Unsupported Content-Type') + }) + + it('imports valid text/csv records successfully', async () => { + vi.mocked(executeOperation).mockResolvedValueOnce(undefined as any) + const req = new Request('http://localhost/import/csv', { + method: 'POST', + headers: { 'Content-Type': 'text/csv' }, + body: 'id,name\n1,Alice', + }) + const res = await importTableFromCsvRoute('users', req, mockDataSource, mockConfig) + expect(res.status).toBe(200) + const json: any = await res.json() + expect(json.data.message).toContain('Imported 1 out of 1 records successfully') + expect(executeOperation).toHaveBeenCalledWith( + [{ sql: 'INSERT INTO users (id, name) VALUES (?, ?)', params: ['1', 'Alice'] }], + mockDataSource, + mockConfig + ) + }) + + it('imports application/json with columnMapping and tracks failed statements', async () => { + vi.mocked(executeOperation) + .mockResolvedValueOnce(undefined as any) + .mockRejectedValueOnce(new Error('Unique constraint failed')) + + const req = new Request('http://localhost/import/csv', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + data: 'user_id,full_name\n1,Alice\n2,Bob', + columnMapping: { user_id: 'id', full_name: 'name' }, + }), + }) + const res = await importTableFromCsvRoute('users', req, mockDataSource, mockConfig) + expect(res.status).toBe(200) + const json: any = await res.json() + expect(json.data.message).toContain('Imported 1 out of 2 records successfully. 1 records failed.') + expect(json.data.failedStatements).toHaveLength(1) + }) +})