- Add Jest configuration for TypeScript testing - Add security utilities tests (44 tests) - Add Zod validation tests (34 tests) - Add cursor pagination tests (25 tests) - Add query builder tests (38 tests) - Add tools structure validation (68 tests) - All 164 tools validated for correct structure - Version bump to 1.3.4 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
298 lines
9.8 KiB
TypeScript
298 lines
9.8 KiB
TypeScript
/**
|
|
* Query Builder Tests
|
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
|
*/
|
|
|
|
import {
|
|
SafeQueryBuilder,
|
|
createQueryBuilder,
|
|
buildSelectQuery,
|
|
buildCountQuery
|
|
} from '../query-builder';
|
|
|
|
describe('Query Builder', () => {
|
|
describe('SafeQueryBuilder', () => {
|
|
let builder: SafeQueryBuilder;
|
|
|
|
beforeEach(() => {
|
|
builder = new SafeQueryBuilder();
|
|
});
|
|
|
|
describe('addParam', () => {
|
|
it('should add params and return placeholders', () => {
|
|
expect(builder.addParam('value1')).toBe('$1');
|
|
expect(builder.addParam('value2')).toBe('$2');
|
|
expect(builder.addParam('value3')).toBe('$3');
|
|
expect(builder.getParams()).toEqual(['value1', 'value2', 'value3']);
|
|
});
|
|
|
|
it('should handle different types', () => {
|
|
builder.addParam('string');
|
|
builder.addParam(123);
|
|
builder.addParam(true);
|
|
builder.addParam(null);
|
|
expect(builder.getParams()).toEqual(['string', 123, true, null]);
|
|
});
|
|
});
|
|
|
|
describe('getNextIndex', () => {
|
|
it('should return the next parameter index', () => {
|
|
expect(builder.getNextIndex()).toBe(1);
|
|
builder.addParam('value');
|
|
expect(builder.getNextIndex()).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('buildILike', () => {
|
|
it('should build ILIKE condition with wildcards', () => {
|
|
const condition = builder.buildILike('"name"', 'test');
|
|
expect(condition).toBe('"name" ILIKE $1');
|
|
expect(builder.getParams()).toEqual(['%test%']);
|
|
});
|
|
|
|
it('should sanitize input', () => {
|
|
const condition = builder.buildILike('"name"', ' test\0value ');
|
|
expect(builder.getParams()).toEqual(['%testvalue%']);
|
|
});
|
|
});
|
|
|
|
describe('buildILikeExact', () => {
|
|
it('should build ILIKE condition without wildcards', () => {
|
|
const condition = builder.buildILikeExact('"email"', 'test@example.com');
|
|
expect(condition).toBe('"email" ILIKE $1');
|
|
expect(builder.getParams()).toEqual(['test@example.com']);
|
|
});
|
|
});
|
|
|
|
describe('buildILikePrefix', () => {
|
|
it('should build ILIKE condition with trailing wildcard', () => {
|
|
const condition = builder.buildILikePrefix('"title"', 'intro');
|
|
expect(condition).toBe('"title" ILIKE $1');
|
|
expect(builder.getParams()).toEqual(['intro%']);
|
|
});
|
|
});
|
|
|
|
describe('buildIn', () => {
|
|
it('should build IN clause using ANY', () => {
|
|
const condition = builder.buildIn('"status"', ['active', 'pending']);
|
|
expect(condition).toBe('"status" = ANY($1)');
|
|
expect(builder.getParams()).toEqual([['active', 'pending']]);
|
|
});
|
|
|
|
it('should return FALSE for empty array', () => {
|
|
const condition = builder.buildIn('"status"', []);
|
|
expect(condition).toBe('FALSE');
|
|
expect(builder.getParams()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('buildNotIn', () => {
|
|
it('should build NOT IN clause using ALL', () => {
|
|
const condition = builder.buildNotIn('"status"', ['deleted', 'archived']);
|
|
expect(condition).toBe('"status" != ALL($1)');
|
|
});
|
|
|
|
it('should return TRUE for empty array', () => {
|
|
const condition = builder.buildNotIn('"status"', []);
|
|
expect(condition).toBe('TRUE');
|
|
});
|
|
});
|
|
|
|
describe('comparison operators', () => {
|
|
it('should build equals condition', () => {
|
|
expect(builder.buildEquals('"id"', 1)).toBe('"id" = $1');
|
|
});
|
|
|
|
it('should build not equals condition', () => {
|
|
expect(builder.buildNotEquals('"id"', 1)).toBe('"id" != $1');
|
|
});
|
|
|
|
it('should build greater than condition', () => {
|
|
expect(builder.buildGreaterThan('"count"', 10)).toBe('"count" > $1');
|
|
});
|
|
|
|
it('should build greater than or equals condition', () => {
|
|
expect(builder.buildGreaterThanOrEquals('"count"', 10)).toBe('"count" >= $1');
|
|
});
|
|
|
|
it('should build less than condition', () => {
|
|
expect(builder.buildLessThan('"count"', 10)).toBe('"count" < $1');
|
|
});
|
|
|
|
it('should build less than or equals condition', () => {
|
|
expect(builder.buildLessThanOrEquals('"count"', 10)).toBe('"count" <= $1');
|
|
});
|
|
});
|
|
|
|
describe('buildBetween', () => {
|
|
it('should build BETWEEN condition', () => {
|
|
const condition = builder.buildBetween('"date"', '2024-01-01', '2024-12-31');
|
|
expect(condition).toBe('"date" BETWEEN $1 AND $2');
|
|
expect(builder.getParams()).toEqual(['2024-01-01', '2024-12-31']);
|
|
});
|
|
});
|
|
|
|
describe('buildIsNull / buildIsNotNull', () => {
|
|
it('should build IS NULL condition', () => {
|
|
expect(builder.buildIsNull('"deletedAt"')).toBe('"deletedAt" IS NULL');
|
|
});
|
|
|
|
it('should build IS NOT NULL condition', () => {
|
|
expect(builder.buildIsNotNull('"publishedAt"')).toBe('"publishedAt" IS NOT NULL');
|
|
});
|
|
});
|
|
|
|
describe('buildUUIDEquals', () => {
|
|
it('should accept valid UUIDs', () => {
|
|
const uuid = '550e8400-e29b-41d4-a716-446655440000';
|
|
const condition = builder.buildUUIDEquals('"userId"', uuid);
|
|
expect(condition).toBe('"userId" = $1');
|
|
expect(builder.getParams()).toEqual([uuid]);
|
|
});
|
|
|
|
it('should throw for invalid UUIDs', () => {
|
|
expect(() => builder.buildUUIDEquals('"userId"', 'invalid')).toThrow('Invalid UUID');
|
|
});
|
|
});
|
|
|
|
describe('buildUUIDIn', () => {
|
|
it('should accept array of valid UUIDs', () => {
|
|
const uuids = [
|
|
'550e8400-e29b-41d4-a716-446655440000',
|
|
'6ba7b810-9dad-11d1-80b4-00c04fd430c8'
|
|
];
|
|
const condition = builder.buildUUIDIn('"id"', uuids);
|
|
expect(condition).toBe('"id" = ANY($1)');
|
|
});
|
|
|
|
it('should throw if any UUID is invalid', () => {
|
|
const uuids = ['550e8400-e29b-41d4-a716-446655440000', 'invalid'];
|
|
expect(() => builder.buildUUIDIn('"id"', uuids)).toThrow('Invalid UUID');
|
|
});
|
|
});
|
|
|
|
describe('conditions management', () => {
|
|
it('should add and build WHERE clause', () => {
|
|
builder.addCondition(builder.buildEquals('"status"', 'active'));
|
|
builder.addCondition(builder.buildIsNull('"deletedAt"'));
|
|
const where = builder.buildWhereClause();
|
|
expect(where).toBe('WHERE "status" = $1 AND "deletedAt" IS NULL');
|
|
});
|
|
|
|
it('should support custom separator', () => {
|
|
builder.addCondition('"a" = 1');
|
|
builder.addCondition('"b" = 2');
|
|
const where = builder.buildWhereClause(' OR ');
|
|
expect(where).toBe('WHERE "a" = 1 OR "b" = 2');
|
|
});
|
|
|
|
it('should return empty string for no conditions', () => {
|
|
expect(builder.buildWhereClause()).toBe('');
|
|
});
|
|
|
|
it('should add condition only if value is truthy', () => {
|
|
builder.addConditionIf('"a" = 1', 'value');
|
|
builder.addConditionIf('"b" = 2', undefined);
|
|
builder.addConditionIf('"c" = 3', null);
|
|
builder.addConditionIf('"d" = 4', '');
|
|
expect(builder.getConditions()).toEqual(['"a" = 1']);
|
|
});
|
|
});
|
|
|
|
describe('reset', () => {
|
|
it('should clear all state', () => {
|
|
builder.addParam('value');
|
|
builder.addCondition('"a" = 1');
|
|
builder.reset();
|
|
expect(builder.getParams()).toEqual([]);
|
|
expect(builder.getConditions()).toEqual([]);
|
|
expect(builder.getNextIndex()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('clone', () => {
|
|
it('should create independent copy', () => {
|
|
builder.addParam('value1');
|
|
builder.addCondition('"a" = 1');
|
|
|
|
const clone = builder.clone();
|
|
clone.addParam('value2');
|
|
clone.addCondition('"b" = 2');
|
|
|
|
expect(builder.getParams()).toEqual(['value1']);
|
|
expect(builder.getConditions()).toEqual(['"a" = 1']);
|
|
expect(clone.getParams()).toEqual(['value1', 'value2']);
|
|
expect(clone.getConditions()).toEqual(['"a" = 1', '"b" = 2']);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createQueryBuilder', () => {
|
|
it('should create new builder instance', () => {
|
|
const builder = createQueryBuilder();
|
|
expect(builder).toBeInstanceOf(SafeQueryBuilder);
|
|
});
|
|
});
|
|
|
|
describe('buildSelectQuery', () => {
|
|
it('should build basic SELECT query', () => {
|
|
const builder = createQueryBuilder();
|
|
builder.addCondition(builder.buildIsNull('"deletedAt"'));
|
|
|
|
const { query, params } = buildSelectQuery(
|
|
'documents',
|
|
['id', 'title', 'content'],
|
|
builder
|
|
);
|
|
|
|
expect(query).toBe('SELECT id, title, content FROM documents WHERE "deletedAt" IS NULL');
|
|
expect(params).toEqual([]);
|
|
});
|
|
|
|
it('should add ORDER BY', () => {
|
|
const builder = createQueryBuilder();
|
|
const { query } = buildSelectQuery(
|
|
'documents',
|
|
['*'],
|
|
builder,
|
|
{ orderBy: '"createdAt"', orderDirection: 'DESC' }
|
|
);
|
|
|
|
expect(query).toContain('ORDER BY "createdAt" DESC');
|
|
});
|
|
|
|
it('should add LIMIT and OFFSET', () => {
|
|
const builder = createQueryBuilder();
|
|
const { query, params } = buildSelectQuery(
|
|
'documents',
|
|
['*'],
|
|
builder,
|
|
{ limit: 10, offset: 20 }
|
|
);
|
|
|
|
expect(query).toContain('LIMIT $1');
|
|
expect(query).toContain('OFFSET $2');
|
|
expect(params).toEqual([10, 20]);
|
|
});
|
|
});
|
|
|
|
describe('buildCountQuery', () => {
|
|
it('should build COUNT query', () => {
|
|
const builder = createQueryBuilder();
|
|
builder.addCondition(builder.buildEquals('"status"', 'active'));
|
|
|
|
const { query, params } = buildCountQuery('documents', builder);
|
|
|
|
expect(query).toBe('SELECT COUNT(*) as count FROM documents WHERE "status" = $1');
|
|
expect(params).toEqual(['active']);
|
|
});
|
|
|
|
it('should handle no conditions', () => {
|
|
const builder = createQueryBuilder();
|
|
const { query } = buildCountQuery('documents', builder);
|
|
|
|
expect(query).toBe('SELECT COUNT(*) as count FROM documents ');
|
|
});
|
|
});
|
|
});
|