/**
* Tests for validation utilities
*/
import {
sanitizeHtml,
sanitizeText,
isValidChartType,
isValidAppToken,
isValidTableId,
isValidViewId,
isValidFieldName,
validateDataSource,
validateChartConfig,
isValidColor,
sanitizeChartConfig,
sanitizeFieldValue,
RateLimiter,
} from '../validation';
describe('Validation Module', () => {
describe('sanitizeHtml', () => {
it('should escape HTML special characters', () => {
expect(sanitizeHtml('<script>alert("xss")</script>')).toBe(
'<script>alert("xss")</script>'
);
});
it('should handle empty strings', () => {
expect(sanitizeHtml('')).toBe('');
});
it('should escape all dangerous characters', () => {
const input = '& < > " \' /';
const expected = '& < > " ' /';
expect(sanitizeHtml(input)).toBe(expected);
});
});
describe('sanitizeText', () => {
it('should sanitize text same as HTML', () => {
const input = '<b>Bold</b>';
expect(sanitizeText(input)).toBe(sanitizeHtml(input));
});
});
describe('isValidChartType', () => {
it('should accept valid chart types', () => {
const validTypes = ['bar', 'line', 'pie', 'area', 'scatter', 'funnel', 'radar'];
validTypes.forEach(type => {
expect(isValidChartType(type)).toBe(true);
});
});
it('should reject invalid chart types', () => {
expect(isValidChartType('invalid')).toBe(false);
expect(isValidChartType('')).toBe(false);
expect(isValidChartType('BAR')).toBe(false);
});
});
describe('isValidAppToken', () => {
it('should accept valid app tokens', () => {
expect(isValidAppToken('abc123_-xyz')).toBe(true);
expect(isValidAppToken('testAppToken123')).toBe(true);
});
it('should reject invalid app tokens', () => {
expect(isValidAppToken('')).toBe(false);
expect(isValidAppToken('abc')).toBe(false); // too short
expect(isValidAppToken('a'.repeat(101))).toBe(false); // too long
expect(isValidAppToken('test token')).toBe(false); // contains space
expect(isValidAppToken('test@token')).toBe(false); // invalid char
});
});
describe('isValidTableId', () => {
it('should accept valid table IDs', () => {
expect(isValidTableId('tblAhbwMbAk9mBCJ')).toBe(true);
expect(isValidTableId('tbl1234567890')).toBe(true);
});
it('should reject invalid table IDs', () => {
expect(isValidTableId('invalid')).toBe(false); // missing tbl prefix
expect(isValidTableId('tbl')).toBe(false); // too short
expect(isValidTableId('TBL1234567890')).toBe(false); // wrong case
expect(isValidTableId('')).toBe(false);
});
});
describe('isValidViewId', () => {
it('should accept valid view IDs', () => {
expect(isValidViewId('vewiFKuwgW')).toBe(true);
expect(isValidViewId('vew1234567890')).toBe(true);
});
it('should reject invalid view IDs', () => {
expect(isValidViewId('invalid')).toBe(false);
expect(isValidViewId('vew')).toBe(false); // too short
expect(isValidViewId('VEW1234567890')).toBe(false); // wrong case
expect(isValidViewId('')).toBe(false);
});
});
describe('isValidFieldName', () => {
it('should accept valid field names', () => {
expect(isValidFieldName('Name')).toBe(true);
expect(isValidFieldName('First Name')).toBe(true);
expect(isValidFieldName('field_123')).toBe(true);
expect(isValidFieldName('中文字段')).toBe(true); // Chinese characters
});
it('should reject invalid field names', () => {
expect(isValidFieldName('')).toBe(false);
expect(isValidFieldName('a'.repeat(101))).toBe(false); // too long
expect(isValidFieldName('field@name')).toBe(false); // invalid char
expect(isValidFieldName('field<script>')).toBe(false); // XSS attempt
});
});
describe('validateDataSource', () => {
it('should validate correct data source', () => {
const dataSource = {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date',
yAxis: 'Views',
},
};
const result = validateDataSource(dataSource);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject missing data source', () => {
const result = validateDataSource(null);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Data source is required');
});
it('should reject invalid app token', () => {
const dataSource = {
appToken: 'invalid token',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date',
yAxis: 'Views',
},
};
const result = validateDataSource(dataSource);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Invalid app token format');
});
it('should reject invalid table ID', () => {
const dataSource = {
appToken: 'testAppToken123',
tableId: 'invalid',
fields: {
xAxis: 'Date',
yAxis: 'Views',
},
};
const result = validateDataSource(dataSource);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Invalid table ID format');
});
it('should validate Y-axis array', () => {
const dataSource = {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date',
yAxis: ['Views', 'Likes', 'invalid<field>'],
},
};
const result = validateDataSource(dataSource);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('Invalid Y-axis field name'))).toBe(true);
});
it('should validate optional view ID', () => {
const dataSource = {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
viewId: 'invalid',
fields: {
xAxis: 'Date',
yAxis: 'Views',
},
};
const result = validateDataSource(dataSource);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Invalid view ID format');
});
});
describe('validateChartConfig', () => {
it('should validate correct chart configuration', () => {
const config = {
chartType: 'bar',
title: 'Test Chart',
dataSource: {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date',
yAxis: 'Views',
},
},
options: {
colors: ['#3370FF', '#34C759'],
showLegend: true,
},
};
const result = validateChartConfig(config);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject missing configuration', () => {
const result = validateChartConfig(null);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Chart configuration is required');
});
it('should reject invalid chart type', () => {
const config = {
chartType: 'invalid',
dataSource: {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date',
yAxis: 'Views',
},
},
};
const result = validateChartConfig(config);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('Invalid chart type'))).toBe(true);
});
it('should reject too long title', () => {
const config = {
chartType: 'bar',
title: 'a'.repeat(201),
dataSource: {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date',
yAxis: 'Views',
},
},
};
const result = validateChartConfig(config);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Chart title too long (max 200 characters)');
});
it('should reject invalid colors', () => {
const config = {
chartType: 'bar',
dataSource: {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date',
yAxis: 'Views',
},
},
options: {
colors: ['#3370FF', 'invalid-color'],
},
};
const result = validateChartConfig(config);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('Invalid color'))).toBe(true);
});
it('should reject too many colors', () => {
const config = {
chartType: 'bar',
dataSource: {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date',
yAxis: 'Views',
},
},
options: {
colors: Array(21).fill('#3370FF'),
},
};
const result = validateChartConfig(config);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Too many colors (max 20)');
});
});
describe('isValidColor', () => {
it('should accept valid hex colors', () => {
expect(isValidColor('#3370FF')).toBe(true);
expect(isValidColor('#FFF')).toBe(true);
expect(isValidColor('#000000')).toBe(true);
});
it('should accept valid RGB colors', () => {
expect(isValidColor('rgb(255, 0, 0)')).toBe(true);
expect(isValidColor('rgba(255, 0, 0, 0.5)')).toBe(true);
});
it('should reject invalid colors', () => {
expect(isValidColor('red')).toBe(false); // named colors not supported
expect(isValidColor('#GGG')).toBe(false);
expect(isValidColor('rgb(256, 0, 0)')).toBe(false);
expect(isValidColor('')).toBe(false);
});
});
describe('sanitizeChartConfig', () => {
it('should sanitize chart configuration', () => {
const config: any = {
chartType: 'bar',
title: '<script>alert("xss")</script>',
dataSource: {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date<script>',
yAxis: 'Views',
},
},
options: {
showLegend: true,
},
};
const sanitized = sanitizeChartConfig(config);
expect(sanitized.title).not.toContain('<script>');
expect(sanitized.dataSource.fields.xAxis).not.toContain('<script>');
});
});
describe('sanitizeFieldValue', () => {
it('should sanitize string values', () => {
const value = 'Test'.repeat(3000);
const sanitized = sanitizeFieldValue(value);
expect(sanitized.length).toBeLessThanOrEqual(10000);
});
it('should preserve number values', () => {
expect(sanitizeFieldValue(123)).toBe(123);
});
it('should preserve boolean values', () => {
expect(sanitizeFieldValue(true)).toBe(true);
expect(sanitizeFieldValue(false)).toBe(false);
});
it('should sanitize arrays', () => {
const value = Array(200).fill('test');
const sanitized = sanitizeFieldValue(value);
expect(Array.isArray(sanitized)).toBe(true);
expect(sanitized.length).toBeLessThanOrEqual(100);
});
it('should sanitize objects', () => {
const value: any = {};
for (let i = 0; i < 100; i++) {
value[`key${i}`] = `value${i}`;
}
const sanitized = sanitizeFieldValue(value);
expect(Object.keys(sanitized).length).toBeLessThanOrEqual(50);
});
it('should handle null and undefined', () => {
expect(sanitizeFieldValue(null)).toBeNull();
expect(sanitizeFieldValue(undefined)).toBeNull();
});
});
describe('RateLimiter', () => {
it('should allow requests within limit', () => {
const limiter = new RateLimiter(5, 1000);
for (let i = 0; i < 5; i++) {
expect(limiter.isAllowed('test-key')).toBe(true);
}
});
it('should block requests exceeding limit', () => {
const limiter = new RateLimiter(3, 1000);
for (let i = 0; i < 3; i++) {
limiter.isAllowed('test-key');
}
expect(limiter.isAllowed('test-key')).toBe(false);
});
it('should allow requests after window expires', async () => {
const limiter = new RateLimiter(2, 100);
limiter.isAllowed('test-key');
limiter.isAllowed('test-key');
expect(limiter.isAllowed('test-key')).toBe(false);
// Wait for window to expire
await new Promise(resolve => setTimeout(resolve, 150));
expect(limiter.isAllowed('test-key')).toBe(true);
});
it('should track different keys separately', () => {
const limiter = new RateLimiter(2, 1000);
limiter.isAllowed('key1');
limiter.isAllowed('key1');
expect(limiter.isAllowed('key1')).toBe(false);
expect(limiter.isAllowed('key2')).toBe(true);
expect(limiter.isAllowed('key2')).toBe(true);
});
it('should clear all limits', () => {
const limiter = new RateLimiter(2, 1000);
limiter.isAllowed('test-key');
limiter.isAllowed('test-key');
expect(limiter.isAllowed('test-key')).toBe(false);
limiter.clear();
expect(limiter.isAllowed('test-key')).toBe(true);
});
});
});