diff --git a/src/allowlist/index.test.ts b/src/allowlist/index.test.ts new file mode 100644 index 0000000..1235cd8 --- /dev/null +++ b/src/allowlist/index.test.ts @@ -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() + }) +}) diff --git a/src/literest/index.test.ts b/src/literest/index.test.ts index 51f8b31..3bc09a5 100644 --- a/src/literest/index.test.ts +++ b/src/literest/index.test.ts @@ -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() + }) + }) })