/**
* Integration tests for Chart Block Creator
*/
import type { ChartConfig, DataSource } from '../types';
import { validateChartConfig, sanitizeFieldValue } from '../validation';
describe('Integration Tests', () => {
describe('Data Transformation', () => {
it('should transform Lark Base records to chart data', () => {
const records = [
{
fields: {
Date: '2024-01-01',
Views: 100,
Likes: 50,
},
},
{
fields: {
Date: '2024-01-02',
Views: 150,
Likes: 75,
},
},
{
fields: {
Date: '2024-01-03',
Views: 200,
Likes: 100,
},
},
];
const fields: DataSource['fields'] = {
xAxis: 'Date',
yAxis: 'Views',
};
// Simulate transformation logic
const chartData = records.map(record => {
const fieldValues = record.fields as Record<string, any>;
return {
x: fieldValues[fields.xAxis as string],
y: fieldValues[fields.yAxis as string] || 0,
};
});
expect(chartData).toHaveLength(3);
expect(chartData[0]).toEqual({ x: '2024-01-01', y: 100 });
expect(chartData[2]).toEqual({ x: '2024-01-03', y: 200 });
});
it('should handle multiple Y-axis fields', () => {
const records = [
{
fields: {
Platform: 'Twitter',
Views: 1000,
Likes: 100,
Shares: 50,
},
},
{
fields: {
Platform: 'Facebook',
Views: 2000,
Likes: 200,
Shares: 75,
},
},
];
const fields: DataSource['fields'] = {
xAxis: 'Platform',
yAxis: ['Views', 'Likes', 'Shares'],
};
const yAxisFields = Array.isArray(fields.yAxis) ? fields.yAxis : [fields.yAxis];
const chartData = records.map(record => {
const fieldValues = record.fields as Record<string, any>;
const dataPoint: any = {
x: fieldValues[fields.xAxis],
};
yAxisFields.forEach((yField, index) => {
const key = yAxisFields.length > 1 ? `y${index + 1}` : 'y';
dataPoint[key] = fieldValues[yField] || 0;
});
return dataPoint;
});
expect(chartData).toHaveLength(2);
expect(chartData[0]).toEqual({
x: 'Twitter',
y1: 1000,
y2: 100,
y3: 50,
});
});
it('should handle complex field values', () => {
const records = [
{
fields: {
Category: [{ text: 'Category A' }],
Value: { value: 100 },
Tags: ['tag1', 'tag2'],
},
},
];
// Test extracting complex field values
const categoryValue = records[0].fields.Category;
const numberValue = records[0].fields.Value;
const tagsValue = records[0].fields.Tags;
// Sanitize values
const sanitizedCategory = sanitizeFieldValue(categoryValue);
const sanitizedNumber = sanitizeFieldValue(numberValue);
const sanitizedTags = sanitizeFieldValue(tagsValue);
expect(sanitizedCategory).toBeDefined();
expect(sanitizedNumber).toBeDefined();
expect(sanitizedTags).toBeDefined();
});
it('should sanitize potentially malicious field values', () => {
const records = [
{
fields: {
Name: '<script>alert("xss")</script>',
Value: 100,
},
},
];
const sanitized = sanitizeFieldValue(records[0].fields.Name);
// Should be truncated and safe
expect(sanitized).toBeDefined();
expect(typeof sanitized).toBe('string');
expect(sanitized.length).toBeLessThanOrEqual(10000);
});
it('should handle empty or missing data gracefully', () => {
const emptyRecords: any[] = [];
const fields: DataSource['fields'] = {
xAxis: 'Date',
yAxis: 'Views',
};
const chartData = emptyRecords.map(record => ({
x: record.fields?.[fields.xAxis as string] || null,
y: record.fields?.[fields.yAxis as string] || 0,
}));
expect(chartData).toHaveLength(0);
});
});
describe('End-to-End Configuration', () => {
it('should validate and process a complete chart configuration', () => {
const config: ChartConfig = {
chartType: 'bar',
title: 'Monthly Sales Report',
dataSource: {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
viewId: 'vew1234567890abc',
fields: {
xAxis: 'Month',
yAxis: 'Sales',
},
},
options: {
colors: ['#3370FF', '#34C759', '#FF9500'],
showLegend: true,
showDataLabels: false,
animation: true,
stacked: false,
},
};
const validation = validateChartConfig(config);
expect(validation.valid).toBe(true);
expect(validation.errors).toHaveLength(0);
});
it('should reject configuration with security risks', () => {
const config: any = {
chartType: 'bar',
title: '<img src=x onerror=alert("xss")>',
dataSource: {
appToken: 'test token with spaces',
tableId: 'invalid-table-id',
fields: {
xAxis: '<script>alert("xss")</script>',
yAxis: 'Views',
},
},
};
const validation = validateChartConfig(config);
expect(validation.valid).toBe(false);
expect(validation.errors.length).toBeGreaterThan(0);
});
it('should handle multi-series chart configuration', () => {
const config: ChartConfig = {
chartType: 'line',
title: 'Platform Engagement',
dataSource: {
appToken: 'testAppToken123',
tableId: 'tbl1234567890abc',
fields: {
xAxis: 'Date',
yAxis: ['Twitter', 'Facebook', 'Instagram'],
series: 'Platform',
},
},
options: {
showLegend: true,
animation: true,
},
};
const validation = validateChartConfig(config);
expect(validation.valid).toBe(true);
});
});
describe('VChart Spec Generation', () => {
it('should generate correct bar chart spec', () => {
const chartData = [
{ x: 'Jan', y: 100 },
{ x: 'Feb', y: 150 },
{ x: 'Mar', y: 200 },
];
const spec = {
type: 'bar',
data: [{ values: chartData }],
xField: 'x',
yField: 'y',
title: { visible: true, text: 'Monthly Data' },
legends: { visible: true },
color: ['#3370FF'],
};
expect(spec.type).toBe('bar');
expect(spec.data[0].values).toEqual(chartData);
expect(spec.xField).toBe('x');
expect(spec.yField).toBe('y');
});
it('should generate correct pie chart spec', () => {
const chartData = [
{ x: 'Category A', y: 30 },
{ x: 'Category B', y: 50 },
{ x: 'Category C', y: 20 },
];
const spec = {
type: 'pie',
data: [{ values: chartData }],
categoryField: 'x',
valueField: 'y',
outerRadius: 0.8,
title: { visible: false },
};
expect(spec.type).toBe('pie');
expect(spec.categoryField).toBe('x');
expect(spec.valueField).toBe('y');
});
it('should generate correct line chart spec with series', () => {
const chartData = [
{ x: 'Jan', y: 100, series: 'Product A' },
{ x: 'Jan', y: 80, series: 'Product B' },
{ x: 'Feb', y: 120, series: 'Product A' },
{ x: 'Feb', y: 90, series: 'Product B' },
];
const spec = {
type: 'line',
data: [{ values: chartData }],
xField: 'x',
yField: 'y',
seriesField: 'series',
};
expect(spec.seriesField).toBe('series');
expect(spec.data[0].values.some(d => d.series)).toBe(true);
});
});
describe('Error Handling', () => {
it('should handle API request failures gracefully', () => {
const mockError = {
message: 'Network request failed',
code: 500,
};
// Simulate error handling
const errorMessage = mockError.message || 'Failed to load data';
expect(errorMessage).toBe('Network request failed');
});
it('should handle invalid response data', () => {
const invalidResponse: any = {
data: null,
};
const records = invalidResponse.data?.items || [];
expect(records).toEqual([]);
});
it('should limit excessively large datasets', () => {
const largeRecordSet = Array(10000).fill({
fields: { x: 'test', y: 100 },
});
// Simulate limiting to 5000 records
const limited = largeRecordSet.slice(0, 5000);
expect(limited).toHaveLength(5000);
});
});
describe('Security Features', () => {
it('should prevent XSS in field values', () => {
const maliciousValue = '<script>alert("xss")</script>';
const sanitized = sanitizeFieldValue(maliciousValue);
expect(sanitized).toBeDefined();
expect(sanitized.length).toBeLessThanOrEqual(10000);
});
it('should limit string lengths to prevent DoS', () => {
const longString = 'a'.repeat(20000);
const sanitized = sanitizeFieldValue(longString);
expect(sanitized.length).toBeLessThanOrEqual(10000);
});
it('should limit array sizes to prevent DoS', () => {
const largeArray = Array(200).fill('test');
const sanitized = sanitizeFieldValue(largeArray);
expect(sanitized.length).toBeLessThanOrEqual(100);
});
it('should limit object keys to prevent DoS', () => {
const largeObject: any = {};
for (let i = 0; i < 100; i++) {
largeObject[`key${i}`] = `value${i}`;
}
const sanitized = sanitizeFieldValue(largeObject);
expect(Object.keys(sanitized).length).toBeLessThanOrEqual(50);
});
});
});