Files
dbmole-mcp/test/unit/db/sqlGuard.test.ts
T
2026-06-11 23:58:14 +05:00

180 lines
7.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { assertSafeStatement, SqlGuardError } from '../../../src/db/sqlGuard.js'
describe('assertSafeStatement', () => {
describe('single-statement guard', () => {
it('allows a single statement', () => {
expect(() => assertSafeStatement('select 1', 'postgres')).not.toThrow()
expect(() => assertSafeStatement('select 1', 'mysql')).not.toThrow()
})
it('allows a single trailing semicolon', () => {
expect(() => assertSafeStatement('select 1;', 'postgres')).not.toThrow()
expect(() => assertSafeStatement('select 1; ', 'postgres')).not.toThrow()
})
it('rejects two statements', () => {
expect(() => assertSafeStatement('select 1; select 2', 'postgres')).toThrow(
/one SQL statement/
)
expect(() => assertSafeStatement('select 1; select 2', 'mysql')).toThrow(
/one SQL statement/
)
})
it('throws SqlGuardError specifically', () => {
expect(() => assertSafeStatement('select 1; select 2', 'postgres')).toThrow(
SqlGuardError
)
})
it('rejects two statements with a comment after the trailing semicolon line', () => {
expect(() => assertSafeStatement('select 1;\nselect 2', 'postgres')).toThrow(
/one SQL statement/
)
})
it('allows a trailing semicolon followed only by a comment', () => {
expect(() => assertSafeStatement('select 1; -- done', 'postgres')).not.toThrow()
expect(() => assertSafeStatement('select 1; /* done */', 'postgres')).not.toThrow()
})
it('ignores semicolons inside single-quoted strings', () => {
expect(() => assertSafeStatement("select 'a;b'", 'postgres')).not.toThrow()
expect(() => assertSafeStatement("select 'a;b'", 'mysql')).not.toThrow()
})
it('handles single-quote escape by doubling', () => {
expect(() => assertSafeStatement("select 'a''b;c'", 'postgres')).not.toThrow()
})
it('ignores semicolons inside double-quoted identifiers', () => {
expect(() => assertSafeStatement('select "a;b"', 'postgres')).not.toThrow()
})
it('ignores semicolons inside mysql backtick identifiers', () => {
expect(() => assertSafeStatement('select `a;b`', 'mysql')).not.toThrow()
})
it('ignores semicolons inside pg dollar-quoted strings', () => {
expect(() => assertSafeStatement('select $$ a ; b $$', 'postgres')).not.toThrow()
})
it('ignores semicolons inside tagged pg dollar-quoted strings', () => {
expect(() =>
assertSafeStatement('select $fn$ a ; select 2 $fn$', 'postgres')
).not.toThrow()
})
it('ignores semicolons inside line comments', () => {
expect(() => assertSafeStatement('select 1 -- ; select 2', 'postgres')).not.toThrow()
expect(() => assertSafeStatement('select 1 # ; select 2', 'mysql')).not.toThrow()
})
it('ignores semicolons inside block comments', () => {
expect(() => assertSafeStatement('select /* ; */ 1', 'postgres')).not.toThrow()
expect(() => assertSafeStatement('select /* ; */ 1', 'mysql')).not.toThrow()
})
it('treats # as a comment only for mysql', () => {
// For postgres '#' is not a comment, so the ; after it is top-level
expect(() => assertSafeStatement('select 1 # ; select 2', 'postgres')).toThrow(
/one SQL statement/
)
})
it('handles nested block comments for postgres', () => {
expect(() =>
assertSafeStatement('/* outer /* inner */ still comment */ select 1', 'postgres')
).not.toThrow()
})
it('handles mysql backslash escape inside string', () => {
// \' does not end the string for mysql, so the ; stays inside
expect(() => assertSafeStatement("select 'a\\'; select 2'", 'mysql')).not.toThrow()
})
})
describe('session-statement guard', () => {
it('rejects BEGIN', () => {
expect(() => assertSafeStatement('BEGIN', 'postgres')).toThrow(/session-level/)
})
it('rejects set session transaction read write', () => {
expect(() =>
assertSafeStatement('set session transaction read write', 'mysql')
).toThrow(/session-level/)
})
it('rejects USE other', () => {
expect(() => assertSafeStatement('USE other', 'mysql')).toThrow(/session-level/)
})
it('is case-insensitive', () => {
expect(() => assertSafeStatement('CoMmIt', 'postgres')).toThrow(/session-level/)
})
it('skips leading whitespace', () => {
expect(() => assertSafeStatement(' \n begin', 'postgres')).toThrow(/session-level/)
})
it('skips a leading comment', () => {
expect(() => assertSafeStatement('/* comment */ begin', 'postgres')).toThrow(
/session-level/
)
})
it('rejects start, rollback, savepoint, release, prepare, deallocate, discard, reset', () => {
for (const kw of [
'start transaction',
'rollback',
'savepoint s1',
'release savepoint s1',
'prepare p as select 1',
'deallocate p',
'discard all',
'reset all'
]) {
expect(() => assertSafeStatement(kw, 'postgres')).toThrow(/session-level/)
}
})
it('does not false-positive on identifiers that start with a keyword', () => {
expect(() =>
assertSafeStatement('select * from SETTINGS_TABLE', 'postgres')
).not.toThrow()
expect(() => assertSafeStatement('select * from usefulness', 'mysql')).not.toThrow()
expect(() => assertSafeStatement('select * from beginning', 'postgres')).not.toThrow()
})
it('allows a leading paren before a real query', () => {
expect(() =>
assertSafeStatement('(select 1) union (select 2)', 'postgres')
).not.toThrow()
})
it('skips a leading line comment before the keyword', () => {
expect(() => assertSafeStatement('-- note\nbegin', 'postgres')).toThrow(/session-level/)
})
it('skips a leading mysql hash comment before the keyword', () => {
expect(() => assertSafeStatement('# note\nuse other', 'mysql')).toThrow(/session-level/)
})
it('does not treat a leading hash comment as a comment for postgres', () => {
// For postgres '#' is not a comment, so the first token is '#', not a keyword.
expect(() => assertSafeStatement('# note\nselect 1', 'postgres')).not.toThrow()
})
it('treats empty or comment-only sql as having no keyword', () => {
expect(() => assertSafeStatement('', 'postgres')).not.toThrow()
expect(() => assertSafeStatement(' ', 'postgres')).not.toThrow()
expect(() => assertSafeStatement('-- just a comment', 'postgres')).not.toThrow()
})
it('does not treat a statement starting with a non-word char as a keyword', () => {
expect(() => assertSafeStatement('123', 'postgres')).not.toThrow()
})
})
})