From bb71f46a2edd561a357cc43be277005bc3aaa832 Mon Sep 17 00:00:00 2001 From: Gimyoonsoo Date: Fri, 15 May 2026 05:22:01 +0900 Subject: [PATCH] test: cover SQL macros plugin --- plugins/sql-macros/index.test.ts | 98 ++++++++++++++++++++++++++++++++ plugins/sql-macros/index.ts | 6 +- 2 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 plugins/sql-macros/index.test.ts diff --git a/plugins/sql-macros/index.test.ts b/plugins/sql-macros/index.test.ts new file mode 100644 index 0000000..5610f96 --- /dev/null +++ b/plugins/sql-macros/index.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it, vi } from 'vitest' +import { SqlMacrosPlugin } from './index' +import type { DataSource } from '../../src/types' + +const makeInternalDataSource = (columns: string[] = []): DataSource => + ({ + source: 'internal', + rpc: { + executeQuery: vi + .fn() + .mockResolvedValue( + columns.map((column_name) => ({ column_name })) + ), + }, + }) as unknown as DataSource + +const makeExternalDataSource = (): DataSource => + ({ + source: 'external', + rpc: { + executeQuery: vi.fn(), + }, + }) as unknown as DataSource + +describe('SqlMacrosPlugin', () => { + it('returns the original query when no data source is available', async () => { + const plugin = new SqlMacrosPlugin({ preventSelectStar: true }) + + const result = await plugin.beforeQuery({ + sql: 'SELECT * FROM users WHERE id = ?', + params: [1], + }) + + expect(result).toEqual({ + sql: 'SELECT * FROM users WHERE id = ?', + params: [1], + }) + }) + + it('does not rewrite queries for external data sources', async () => { + const dataSource = makeExternalDataSource() + const plugin = new SqlMacrosPlugin({ preventSelectStar: false }) + + const result = await plugin.beforeQuery({ + sql: 'SELECT $_exclude(email) FROM users', + dataSource, + }) + + expect(result.sql).toBe('SELECT $_exclude(email) FROM users') + expect(dataSource.rpc.executeQuery).not.toHaveBeenCalled() + }) + + it('rejects SELECT star for non-admin users when the guard is enabled', async () => { + const plugin = new SqlMacrosPlugin({ preventSelectStar: true }) + plugin.config = { role: 'user' } as any + + await expect( + plugin.beforeQuery({ + sql: 'SELECT * FROM users', + dataSource: makeInternalDataSource(), + }) + ).rejects.toThrow( + 'SELECT * is not allowed. Please specify explicit columns.' + ) + }) + + it('allows SELECT star for admins', async () => { + const plugin = new SqlMacrosPlugin({ preventSelectStar: true }) + plugin.config = { role: 'admin' } as any + + const result = await plugin.beforeQuery({ + sql: 'SELECT * FROM users', + dataSource: makeInternalDataSource(), + }) + + expect(result.sql).toBe('SELECT * FROM users') + }) + + it('expands $_exclude into explicit included columns for internal SQLite sources', async () => { + const dataSource = makeInternalDataSource(['id', 'name', 'email']) + const plugin = new SqlMacrosPlugin({ preventSelectStar: false }) + + const result = await plugin.beforeQuery({ + sql: 'SELECT $_exclude(email) FROM users', + params: ['active'], + dataSource, + }) + + expect(dataSource.rpc.executeQuery).toHaveBeenCalledWith({ + sql: expect.stringContaining("pragma_table_info('users')"), + }) + expect(result.params).toEqual(['active']) + expect(result.sql).toContain('id') + expect(result.sql).toContain('name') + expect(result.sql).not.toContain('email') + expect(result.sql).not.toContain('$_exclude') + }) +}) diff --git a/plugins/sql-macros/index.ts b/plugins/sql-macros/index.ts index 9dfbb85..686a7ee 100644 --- a/plugins/sql-macros/index.ts +++ b/plugins/sql-macros/index.ts @@ -8,6 +8,8 @@ import { DataSource, QueryResult } from '../../src/types' const parser = new (require('node-sql-parser').Parser)() +const firstStatement = (ast: any) => (Array.isArray(ast) ? ast[0] : ast) + export class SqlMacrosPlugin extends StarbasePlugin { config?: StarbaseDBConfiguration @@ -59,7 +61,7 @@ export class SqlMacrosPlugin extends StarbasePlugin { private checkSelectStar(sql: string, params?: unknown[]): string { try { - const ast = parser.astify(sql)[0] + const ast = firstStatement(parser.astify(sql)) // Only check SELECT statements if (ast.type === 'select') { @@ -116,7 +118,7 @@ export class SqlMacrosPlugin extends StarbasePlugin { '$_exclude', '__exclude' ) - const normalizedQuery = parser.astify(preparedSql)[0] + const normalizedQuery = firstStatement(parser.astify(preparedSql)) // Only process SELECT statements if (normalizedQuery.type !== 'select') {