Skip to content
Closed
Show file tree
Hide file tree
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
94 changes: 94 additions & 0 deletions src/allowlist/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { isQueryAllowed } from './index'
import type { DataSource } from '../types'
import type { StarbaseDBConfiguration } from '../handler'

describe('isQueryAllowed', () => {
let mockDataSource: DataSource
let mockConfig: StarbaseDBConfiguration

beforeEach(() => {
vi.clearAllMocks()

mockDataSource = {
source: 'external',
external: {
dialect: 'sqlite',
} as any,
rpc: {
executeQuery: vi.fn(),
},
} as any

mockConfig = {
role: 'user',
outerbaseApiKey: 'mock-api-key',
} as any
})

it('should return true immediately when isEnabled is false', async () => {
const allowed = await isQueryAllowed({
sql: 'DROP TABLE important_table',
isEnabled: false,
dataSource: mockDataSource,
config: mockConfig,
})
expect(allowed).toBe(true)
expect(mockDataSource.rpc.executeQuery).not.toHaveBeenCalled()
})

it('should return true immediately when config.role is admin', async () => {
const allowed = await isQueryAllowed({
sql: 'DELETE FROM accounts',
isEnabled: true,
dataSource: mockDataSource,
config: { ...mockConfig, role: 'admin' },
})
expect(allowed).toBe(true)
expect(mockDataSource.rpc.executeQuery).not.toHaveBeenCalled()
})

it('should allow valid SQL matching the allowlist pattern', async () => {
vi.mocked(mockDataSource.rpc.executeQuery).mockResolvedValueOnce([
{ source: 'external', sql_statement: 'SELECT id, name FROM users WHERE id = 1;' }
] as any)

const allowed = await isQueryAllowed({
sql: 'SELECT id, name FROM users WHERE id = 1',
isEnabled: true,
dataSource: mockDataSource,
config: mockConfig,
})
expect(allowed).toBe(true)
})

it('should reject unapproved queries and record audit in tmp_allowlist_rejections', async () => {
// Allowlist contains only SELECT
vi.mocked(mockDataSource.rpc.executeQuery)
.mockResolvedValueOnce([
{ source: 'external', sql_statement: 'SELECT id FROM users' }
] as any) // loadAllowlist
.mockResolvedValueOnce([] as any) // addRejectedQuery

await expect(isQueryAllowed({
sql: 'DELETE FROM users WHERE id = 5',
isEnabled: true,
dataSource: mockDataSource,
config: mockConfig,
})).rejects.toThrow('Query not allowed')

// Verify rejected query was written to tmp_allowlist_rejections
expect(mockDataSource.rpc.executeQuery).toHaveBeenCalledWith(expect.objectContaining({
params: ['DELETE FROM users WHERE id = 5', 'external']
}))
})

it('should throw an error if SQL string is empty or missing', async () => {
await expect(isQueryAllowed({
sql: '',
isEnabled: true,
dataSource: mockDataSource,
config: mockConfig,
})).rejects.toThrow()
})
})
35 changes: 35 additions & 0 deletions src/literest/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,4 +489,39 @@ describe('LiteREST', () => {
expect(query).not.toContain('DROP TABLE users')
})
})

describe('LiteREST Boundary & Edge Cases', () => {
it('should handle non-existent table gracefully and return error response', async () => {
vi.mocked(executeQuery).mockRejectedValueOnce(new Error('no such table: fake_table_xyz'))

const request = new Request('http://localhost/rest/main/fake_table_xyz', { method: 'GET' })
const response = await liteRest.handleRequest(request)

expect(response.status).toBe(500)
const json = await response.json()
expect(json.error).toContain('no such table: fake_table_xyz')
})

it('should handle negative primary key lookup safely', async () => {
vi.mocked(executeQuery).mockResolvedValueOnce([])

const request = new Request('http://localhost/rest/main/users/-1', { method: 'GET' })
const response = await liteRest.handleRequest(request)

expect(response.status).toBe(200)
const json = await response.json()
expect(json.result).toEqual([])
})

it('should ignore null query filter parameters without failing query construction', async () => {
vi.mocked(executeQuery).mockResolvedValueOnce([{ id: 1, name: 'Alice' }])

const request = new Request('http://localhost/rest/main/users?role=null&status=active', { method: 'GET' })
const response = await liteRest.handleRequest(request)

expect(response.status).toBe(200)
const json = await response.json()
expect(json.result).toBeDefined()
})
})
})