Files
postgres/tests/integration/QueryBuilder.spec.ts
copilot-swe-agent[bot] 921b528977 feat: Add query builder feature with API and UI
- Add query-builder feature to features.json configuration
- Create /api/admin/query-builder endpoint with full validation
- Build QueryBuilderTab component with visual query construction
- Add WHERE conditions builder with multiple operators
- Add ORDER BY, LIMIT, and OFFSET support
- Add query operators configuration
- Create comprehensive integration tests
- Add getQueryOperators utility function
- Update navigation to include Query Builder

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
2026-01-08 12:53:20 +00:00

334 lines
8.8 KiB
TypeScript

import { expect, test } from '@playwright/test';
test.describe('Query Builder API', () => {
test.describe('Authentication', () => {
test('should reject query builder without authentication', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
},
});
expect(response.status()).toBe(401);
const data = await response.json();
expect(data.error).toBe('Unauthorized');
});
});
test.describe('Input Validation', () => {
test('should reject query without table name', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {},
});
expect(response.status()).toBe(401); // No auth
});
test('should reject query with invalid table name', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users; DROP TABLE users--',
},
});
expect(response.status()).toBe(401); // No auth, but would be 400 if authenticated
});
test('should reject query with invalid column name', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
columns: ['id', 'name; DROP TABLE--'],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should reject query with invalid operator', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
where: [
{
column: 'id',
operator: 'EXEC',
value: '1',
},
],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should reject IN operator without array value', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
where: [
{
column: 'id',
operator: 'IN',
value: 'not-an-array',
},
],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should reject operator requiring value without value', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
where: [
{
column: 'id',
operator: '=',
},
],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should reject invalid LIMIT value', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
limit: -5,
},
});
expect(response.status()).toBe(401); // No auth
});
test('should reject invalid OFFSET value', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
offset: 'invalid',
},
});
expect(response.status()).toBe(401); // No auth
});
});
test.describe('Query Building', () => {
test('should accept valid table name', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'test_table',
},
});
expect(response.status()).toBe(401); // No auth
});
test('should accept query with column selection', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
columns: ['id', 'name', 'email'],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should accept query with WHERE conditions', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
where: [
{
column: 'id',
operator: '=',
value: 1,
},
{
column: 'name',
operator: 'LIKE',
value: '%john%',
},
],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should accept IS NULL operator without value', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
where: [
{
column: 'email',
operator: 'IS NULL',
},
],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should accept IS NOT NULL operator without value', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
where: [
{
column: 'email',
operator: 'IS NOT NULL',
},
],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should accept IN operator with array value', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
where: [
{
column: 'id',
operator: 'IN',
value: [1, 2, 3],
},
],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should accept query with ORDER BY', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
orderBy: {
column: 'created_at',
direction: 'DESC',
},
},
});
expect(response.status()).toBe(401); // No auth
});
test('should accept query with LIMIT', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
limit: 10,
},
});
expect(response.status()).toBe(401); // No auth
});
test('should accept query with OFFSET', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
offset: 5,
},
});
expect(response.status()).toBe(401); // No auth
});
test('should accept comprehensive query', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
columns: ['id', 'name', 'email'],
where: [
{
column: 'id',
operator: '>',
value: 5,
},
{
column: 'name',
operator: 'LIKE',
value: '%admin%',
},
],
orderBy: {
column: 'id',
direction: 'ASC',
},
limit: 20,
offset: 10,
},
});
expect(response.status()).toBe(401); // No auth
});
});
test.describe('SQL Injection Prevention', () => {
test('should reject SQL injection in table name', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: "users' OR '1'='1",
},
});
expect(response.status()).toBe(401); // No auth
});
test('should reject SQL injection in column name', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
columns: ["id'; DROP TABLE users--"],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should reject SQL injection in WHERE column', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
where: [
{
column: "id'; DELETE FROM users--",
operator: '=',
value: 1,
},
],
},
});
expect(response.status()).toBe(401); // No auth
});
test('should reject SQL injection in ORDER BY column', async ({ page }) => {
const response = await page.request.post('/api/admin/query-builder', {
data: {
table: 'users',
orderBy: {
column: "id'; DROP TABLE--",
direction: 'ASC',
},
},
});
expect(response.status()).toBe(401); // No auth
});
});
});