Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
test: add Vitest suites for allowlist, dump header/errors, and plugin…
… registry
  • Loading branch information
jdjdjjdrjnsn committed Sep 8, 2026
commit 7fd1698da0bf229e5b3f25f21f7200533cc80f52
198 changes: 198 additions & 0 deletions src/allowlist/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { isQueryAllowed } from './index'
import { StarbaseDBConfiguration } from '../handler'

const mockDataSource = {
source: 'internal',
rpc: {
executeQuery: vi.fn(),
},
} as any

const clientConfig: StarbaseDBConfiguration = {
role: 'client',
features: { allowlist: true },
}

const adminConfig: StarbaseDBConfiguration = {
role: 'admin',
features: { allowlist: true },
}

function mockAllowlistRows(rows: { sql_statement: string; source: string }[]) {
vi.mocked(mockDataSource.rpc.executeQuery).mockImplementation(
async (opts: any) => {
const sql: string = opts?.sql ?? ''
if (sql.startsWith('SELECT')) {
return rows as any
}
// INSERT into rejections — record call, return empty
return [] as any
}
)
}

describe('isQueryAllowed - allowlist query checker', () => {
beforeEach(() => {
vi.resetAllMocks()
vi.spyOn(console, 'error').mockImplementation(() => {})
})

it('allows any query when the feature is disabled without touching the DB', async () => {
const result = await isQueryAllowed({
sql: 'DROP TABLE users',
isEnabled: false,
dataSource: mockDataSource,
config: clientConfig,
})

expect(result).toBe(true)
expect(mockDataSource.rpc.executeQuery).not.toHaveBeenCalled()
})

it('allows any query for admin role without touching the DB', async () => {
const result = await isQueryAllowed({
sql: 'DELETE FROM users',
isEnabled: true,
dataSource: mockDataSource,
config: adminConfig,
})

expect(result).toBe(true)
expect(mockDataSource.rpc.executeQuery).not.toHaveBeenCalled()
})

it('allows an exact allowlisted query', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
])

const result = await isQueryAllowed({
sql: 'SELECT * FROM users',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})

expect(result).toBe(true)
})

it('treats trailing semicolon as equivalent (normalizeSQL)', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
])

const result = await isQueryAllowed({
sql: 'SELECT * FROM users;',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})

expect(result).toBe(true)
})

it('ignores allowlist rows from other sources', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'external' },
{ sql_statement: 'SELECT id FROM orders', source: 'internal' },
])

const allowed = await isQueryAllowed({
sql: 'SELECT id FROM orders',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
expect(allowed).toBe(true)

await expect(
isQueryAllowed({
sql: 'SELECT * FROM users',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
).rejects.toThrow('Query not allowed')
})

it('rejects non-allowlisted query, logs rejection, and throws', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
])

await expect(
isQueryAllowed({
sql: 'DROP TABLE users',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
).rejects.toThrow('Query not allowed')

// SELECT for load + INSERT for rejection audit
const calls = vi.mocked(mockDataSource.rpc.executeQuery).mock.calls
expect(calls.length).toBeGreaterThanOrEqual(2)
expect(String(calls[1][0]?.sql)).toContain('INSERT INTO tmp_allowlist_rejections')
expect(calls[1][0]?.params).toEqual(['DROP TABLE users', 'internal'])
})

it('returns an Error object when SQL is missing', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
])

const result = await isQueryAllowed({
sql: '',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})

expect(result).toBeInstanceOf(Error)
expect((result as Error).message).toContain('No SQL provided')
})

it('denies when allowlist fails to load (empty list fail-closed)', async () => {
vi.mocked(mockDataSource.rpc.executeQuery).mockImplementation(
async (opts: any) => {
if (String(opts?.sql).startsWith('SELECT')) {
throw new Error('Database error')
}
return [] as any
}
)

await expect(
isQueryAllowed({
sql: 'SELECT * FROM users',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
).rejects.toThrow('Query not allowed')
})

it('still rejects cleanly when rejection audit insert fails', async () => {
vi.mocked(mockDataSource.rpc.executeQuery).mockImplementation(
async (opts: any) => {
const sql: string = opts?.sql ?? ''
if (sql.startsWith('SELECT')) {
return [
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
] as any
}
throw new Error('audit table missing')
}
)

await expect(
isQueryAllowed({
sql: 'SELECT * FROM secrets',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
).rejects.toThrow('Query not allowed')
})
})
52 changes: 52 additions & 0 deletions src/import/dump.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,56 @@ describe('Import Dump Module', () => {

expect(response.status).toBe(207)
})
it('should strip SQLite format header and import remaining statements', async () => {
vi.mocked(executeOperation).mockResolvedValue([{ ok: 1 }] as any)

const sqlFile = new File(
['SQLite format 3\x00extra-header-bytes\nCREATE TABLE users (id INT);'],
'dump.sql',
{ type: 'application/sql' }
)

const request = await createFormDataRequest(sqlFile)
const response = await importDumpRoute(request, mockDataSource, mockConfig)

expect(response.status).toBe(200)
expect(vi.mocked(executeOperation)).toHaveBeenCalledTimes(1)
expect(vi.mocked(executeOperation).mock.calls[0][0]).toEqual([
{ sql: 'CREATE TABLE users (id INT);' },
])
})

it('should skip comments/blank lines and keep trailing statement without semicolon', async () => {
vi.mocked(executeOperation).mockResolvedValue([{ ok: 1 }] as any)

const sqlFile = new File(
['-- seed dump\n\nCREATE TABLE a (id INT);\nINSERT INTO a VALUES (1)'],
'dump.sql',
{ type: 'application/sql' }
)

const request = await createFormDataRequest(sqlFile)
const response = await importDumpRoute(request, mockDataSource, mockConfig)

expect(response.status).toBe(200)
expect(vi.mocked(executeOperation)).toHaveBeenCalledTimes(2)
const seen = vi.mocked(executeOperation).mock.calls.map((c) => (c[0] as { sql: string }[])[0].sql)
expect(seen[0]).toContain('CREATE TABLE a')
expect(seen[1]).toContain('INSERT INTO a VALUES (1)')
})

it('should return 500 when form parsing fails (outer catch)', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

const bad = new Request('http://localhost', {
method: 'POST',
headers: { 'Content-Type': 'multipart/form-data; boundary=x' },
body: 'not-valid-form-data!!!',
})

const response = await importDumpRoute(bad, mockDataSource, mockConfig)

expect(response.status).toBe(500)
consoleErrorSpy.mockRestore()
})
})
18 changes: 18 additions & 0 deletions src/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,22 @@ describe('StarbasePluginRegistry', () => {
expect(mockPlugin.afterQuery).toHaveBeenCalled()
expect(result).toEqual({ data: [], modified: true })
})
it('should list registered plugin names via currentPlugins()', async () => {
const second = new MockPlugin('Second')
const reg = new StarbasePluginRegistry({
app: mockApp,
plugins: [mockPlugin, second],
})

expect(reg.currentPlugins()).toEqual(['MockPlugin', 'Second'])
expect(new StarbasePluginRegistry({ app: mockApp, plugins: [] }).currentPlugins()).toEqual([])
})

it('should skip plugins without register implementation (UnimplementedError)', async () => {
const plain = new TestPlugin('Plain')
const reg = new StarbasePluginRegistry({ app: mockApp, plugins: [plain] })

await expect(reg.init()).resolves.toBeUndefined()
expect(reg.currentPlugins()).toEqual(['Plain'])
})
})