sources.integration.test.ts•13.4 kB
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import express, { Application } from 'express';
import { setupManagerWithFixture, FIXTURES } from '../../__fixtures__/helpers.js';
import type { ConnectorManager } from '../../connectors/manager.js';
import { listSources, getSource } from '../sources.js';
import type { components } from '../openapi.js';
import { Server } from 'http';
// Import SQLite connector to ensure it's registered
import '../../connectors/sqlite/index.js';
type DataSource = components['schemas']['DataSource'];
type ErrorResponse = components['schemas']['Error'];
describe('Data Sources API Integration Tests', () => {
let manager: ConnectorManager;
let app: Application;
let server: Server;
const TEST_PORT = 13579; // Use a unique port to avoid conflicts
const BASE_URL = `http://localhost:${TEST_PORT}`;
beforeAll(async () => {
// Initialize ConnectorManager with readonly-maxrows fixture
// This fixture provides 3 SQLite sources with different execution options:
// - readonly_limited: readonly=true, max_rows=100
// - writable_limited: readonly=false, max_rows=500
// - writable_unlimited: readonly=false, no max_rows
manager = await setupManagerWithFixture(FIXTURES.READONLY_MAXROWS);
// Set up Express app with API routes
app = express();
app.use(express.json());
app.get('/api/sources', listSources);
app.get('/api/sources/:sourceId', getSource);
// Start server
await new Promise<void>((resolve) => {
server = app.listen(TEST_PORT, () => {
resolve();
});
});
}, 30000);
afterAll(async () => {
// Cleanup
if (server) {
await new Promise<void>((resolve, reject) => {
server.close((err) => {
if (err) reject(err);
else resolve();
});
});
}
if (manager) {
await manager.disconnect();
}
});
describe('GET /api/sources', () => {
it('should return array of all data sources', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toContain('application/json');
const sources = (await response.json()) as DataSource[];
expect(Array.isArray(sources)).toBe(true);
expect(sources).toHaveLength(3);
});
it('should include correct source IDs', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
const ids = sources.map((s) => s.id);
expect(ids).toEqual(['readonly_limited', 'writable_limited', 'writable_unlimited']);
});
it('should mark first source as default', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
expect(sources[0].is_default).toBe(true);
expect(sources[1].is_default).toBe(false);
expect(sources[2].is_default).toBe(false);
});
it('should include database type for all sources', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
sources.forEach((source) => {
expect(source.type).toBe('sqlite');
});
});
it('should include execution options', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
// First source has readonly and max_rows
expect(sources[0].readonly).toBe(true);
expect(sources[0].max_rows).toBe(100);
// Second source has different settings
expect(sources[1].readonly).toBe(false);
expect(sources[1].max_rows).toBe(500);
// Third source has no explicit settings
expect(sources[2].readonly).toBeUndefined();
expect(sources[2].max_rows).toBeUndefined();
});
it('should include database connection details', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
sources.forEach((source) => {
expect(source.database).toBe(':memory:');
expect(source.id).toBeDefined();
expect(source.type).toBe('sqlite');
});
});
it('should not include sensitive fields like passwords', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
sources.forEach((source) => {
expect(source).not.toHaveProperty('password');
expect(source).not.toHaveProperty('ssh_password');
expect(source).not.toHaveProperty('ssh_key');
expect(source).not.toHaveProperty('ssh_passphrase');
});
});
it('should include tools array for all sources', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
sources.forEach((source) => {
expect(source.tools).toBeDefined();
expect(Array.isArray(source.tools)).toBe(true);
expect(source.tools.length).toBeGreaterThan(0);
});
});
it('should include correct tool metadata structure', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
sources.forEach((source) => {
source.tools.forEach((tool) => {
// Verify tool has required fields
expect(tool.name).toBeDefined();
expect(typeof tool.name).toBe('string');
expect(tool.description).toBeDefined();
expect(typeof tool.description).toBe('string');
expect(tool.parameters).toBeDefined();
expect(Array.isArray(tool.parameters)).toBe(true);
// Verify parameter structure
tool.parameters.forEach((param) => {
expect(param.name).toBeDefined();
expect(typeof param.name).toBe('string');
expect(param.type).toBeDefined();
expect(typeof param.type).toBe('string');
expect(param.required).toBeDefined();
expect(typeof param.required).toBe('boolean');
expect(param.description).toBeDefined();
expect(typeof param.description).toBe('string');
});
});
});
});
it('should include execute_sql tools with correct naming', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
// Find sources by ID to avoid relying on array order
const readonlySource = sources.find(s => s.id === 'readonly_limited');
const writableSource = sources.find(s => s.id === 'writable_limited');
const unlimitedSource = sources.find(s => s.id === 'writable_unlimited');
expect(readonlySource?.tools[0].name).toBe('execute_sql_readonly_limited');
expect(writableSource?.tools[0].name).toBe('execute_sql_writable_limited');
expect(unlimitedSource?.tools[0].name).toBe('execute_sql_writable_unlimited');
});
it('should include source ID and type in tool descriptions', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
sources.forEach((source) => {
const tool = source.tools[0];
expect(tool.description).toContain(source.id);
expect(tool.description).toContain(source.type);
});
});
it('should mark default source in tool description', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
const defaultSource = sources.find(s => s.is_default);
expect(defaultSource).toBeDefined();
expect(defaultSource?.tools[0].description).toContain('(default)');
const nonDefaultSources = sources.filter(s => !s.is_default);
nonDefaultSources.forEach((source) => {
expect(source.tools[0].description).not.toContain('(default)');
});
});
it('should include sql parameter in execute_sql tool', async () => {
const response = await fetch(`${BASE_URL}/api/sources`);
const sources = (await response.json()) as DataSource[];
sources.forEach((source) => {
const tool = source.tools[0];
const sqlParam = tool.parameters.find((p) => p.name === 'sql');
expect(sqlParam).toBeDefined();
expect(sqlParam!.type).toBe('string');
expect(sqlParam!.required).toBe(true);
expect(sqlParam!.description).toContain('SQL');
});
});
});
describe('GET /api/sources/{source-id}', () => {
it('should return specific source by ID', async () => {
const response = await fetch(`${BASE_URL}/api/sources/readonly_limited`);
expect(response.status).toBe(200);
const source = (await response.json()) as DataSource;
expect(source.id).toBe('readonly_limited');
expect(source.type).toBe('sqlite');
expect(source.is_default).toBe(true);
expect(source.readonly).toBe(true);
expect(source.max_rows).toBe(100);
});
it('should return correct data for non-default source', async () => {
const response = await fetch(`${BASE_URL}/api/sources/writable_limited`);
expect(response.status).toBe(200);
const source = (await response.json()) as DataSource;
expect(source.id).toBe('writable_limited');
expect(source.is_default).toBe(false);
expect(source.readonly).toBe(false);
expect(source.max_rows).toBe(500);
});
it('should return 404 for non-existent source', async () => {
const response = await fetch(`${BASE_URL}/api/sources/nonexistent_source`);
expect(response.status).toBe(404);
const error = (await response.json()) as ErrorResponse;
expect(error.error).toBe('Source not found');
expect(error.source_id).toBe('nonexistent_source');
});
it('should not include sensitive fields in single source response', async () => {
const response = await fetch(`${BASE_URL}/api/sources/readonly_limited`);
const source = (await response.json()) as DataSource;
expect(source).not.toHaveProperty('password');
expect(source).not.toHaveProperty('ssh_password');
expect(source).not.toHaveProperty('ssh_key');
expect(source).not.toHaveProperty('ssh_passphrase');
});
it('should handle URL-encoded source IDs', async () => {
// Test with underscores in ID
const response = await fetch(`${BASE_URL}/api/sources/${encodeURIComponent('readonly_limited')}`);
expect(response.status).toBe(200);
const source = (await response.json()) as DataSource;
expect(source.id).toBe('readonly_limited');
});
it('should include tools array in single source response', async () => {
const response = await fetch(`${BASE_URL}/api/sources/readonly_limited`);
const source = (await response.json()) as DataSource;
expect(source.tools).toBeDefined();
expect(Array.isArray(source.tools)).toBe(true);
expect(source.tools.length).toBeGreaterThan(0);
});
it('should include correct tool name for specific source', async () => {
const response = await fetch(`${BASE_URL}/api/sources/writable_limited`);
const source = (await response.json()) as DataSource;
expect(source.tools[0].name).toBe('execute_sql_writable_limited');
expect(source.tools[0].description).toContain('writable_limited');
expect(source.tools[0].description).toContain('sqlite');
});
it('should include complete tool metadata in single source response', async () => {
const response = await fetch(`${BASE_URL}/api/sources/readonly_limited`);
const source = (await response.json()) as DataSource;
const tool = source.tools[0];
expect(tool.name).toBe('execute_sql_readonly_limited');
expect(tool.description).toBeDefined();
expect(tool.parameters).toBeDefined();
expect(Array.isArray(tool.parameters)).toBe(true);
// Verify sql parameter exists
const sqlParam = tool.parameters.find((p) => p.name === 'sql');
expect(sqlParam).toBeDefined();
expect(sqlParam!.type).toBe('string');
expect(sqlParam!.required).toBe(true);
});
});
describe('Error Handling', () => {
it('should return proper error format for 404', async () => {
const response = await fetch(`${BASE_URL}/api/sources/invalid_id`);
expect(response.status).toBe(404);
expect(response.headers.get('content-type')).toContain('application/json');
const error = (await response.json()) as ErrorResponse;
expect(error).toHaveProperty('error');
expect(error).toHaveProperty('source_id');
expect(typeof error.error).toBe('string');
expect(error.source_id).toBe('invalid_id');
});
it('should handle special characters in source ID for 404', async () => {
const specialId = 'test@#$%';
const response = await fetch(`${BASE_URL}/api/sources/${encodeURIComponent(specialId)}`);
expect(response.status).toBe(404);
const error = (await response.json()) as ErrorResponse;
expect(error.source_id).toBe(specialId);
});
});
});