import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { DatabaseAdapter } from '../../src/adapters/base'; import { DataSource, QueryRouter } from '../../src/data-source'; import type { DataSourceConfig } from '../../src/data-source'; import type { DataSourceIntrospector } from '../../src/introspection/base'; import { DataSourceType } from '../../src/types/credentials'; // Mock the adapter factory vi.mock('../../src/adapters/factory', () => ({ createAdapter: vi.fn(), })); describe('DataSource Unit Tests', () => { let dataSource: DataSource; let mockAdapter: DatabaseAdapter; let mockIntrospector: DataSourceIntrospector; beforeEach(async () => { // Create mock adapter mockAdapter = { query: vi.fn(), testConnection: vi.fn(), introspect: vi.fn(), close: vi.fn(), initialize: vi.fn(), } as unknown as DatabaseAdapter; // Create mock introspector mockIntrospector = { getDatabases: vi.fn().mockResolvedValue([]), getSchemas: vi.fn().mockResolvedValue([]), getTables: vi.fn().mockResolvedValue([]), getColumns: vi.fn().mockResolvedValue([]), getViews: vi.fn().mockResolvedValue([]), getTableStatistics: vi.fn().mockResolvedValue({}), getColumnStatistics: vi.fn().mockResolvedValue([]), getIndexes: vi.fn().mockResolvedValue([]), getForeignKeys: vi.fn().mockResolvedValue([]), getFullIntrospection: vi.fn().mockResolvedValue({}), getDataSourceType: vi.fn().mockReturnValue('postgresql'), }; // Setup mock adapter to return mock introspector vi.mocked(mockAdapter.introspect).mockReturnValue(mockIntrospector); vi.mocked(mockAdapter.testConnection).mockResolvedValue(true); vi.mocked(mockAdapter.query).mockResolvedValue({ rows: [{ test: 'value' }], fields: [{ name: 'test', type: 'string' }], rowCount: 1, }); // Mock the createAdapter function const { createAdapter } = await import('../../src/adapters/factory'); vi.mocked(createAdapter).mockResolvedValue(mockAdapter); }); afterEach(async () => { if (dataSource) { await dataSource.close(); } vi.clearAllMocks(); }); describe('Constructor and Initialization', () => { it('should initialize with empty data sources', () => { dataSource = new DataSource({ dataSources: [] }); expect(dataSource.getDataSources()).toEqual([]); }); it('should initialize with single data source', () => { const config: DataSourceConfig = { name: 'test-postgres', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; dataSource = new DataSource({ dataSources: [config] }); expect(dataSource.getDataSources()).toEqual(['test-postgres']); }); it('should initialize with multiple data sources', () => { const configs: DataSourceConfig[] = [ { name: 'postgres', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }, { name: 'mysql', type: DataSourceType.MySQL, credentials: { type: DataSourceType.MySQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }, ]; dataSource = new DataSource({ dataSources: configs }); expect(dataSource.getDataSources()).toEqual(['postgres', 'mysql']); }); it('should set default data source', () => { const config: DataSourceConfig = { name: 'default-db', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; dataSource = new DataSource({ dataSources: [config], defaultDataSource: 'default-db', }); expect(dataSource.getDataSources()).toEqual(['default-db']); }); }); describe('Data Source Management', () => { beforeEach(() => { dataSource = new DataSource({ dataSources: [] }); }); it('should add new data source', async () => { const config: DataSourceConfig = { name: 'new-source', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; await dataSource.addDataSource(config); expect(dataSource.getDataSources()).toContain('new-source'); }); it('should throw error when adding duplicate data source', async () => { const config: DataSourceConfig = { name: 'duplicate', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; await dataSource.addDataSource(config); await expect(dataSource.addDataSource(config)).rejects.toThrow( "Data source with name 'duplicate' already exists" ); }); it('should remove data source', async () => { const config: DataSourceConfig = { name: 'to-remove', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; await dataSource.addDataSource(config); expect(dataSource.getDataSources()).toContain('to-remove'); await dataSource.removeDataSource('to-remove'); expect(dataSource.getDataSources()).not.toContain('to-remove'); expect(mockAdapter.close).toHaveBeenCalled(); }); it('should update data source configuration', async () => { const config: DataSourceConfig = { name: 'to-update', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; await dataSource.addDataSource(config); await dataSource.updateDataSource('to-update', { credentials: { type: DataSourceType.PostgreSQL, host: 'newhost', database: 'test', username: 'user', password: 'pass', }, }); const updatedConfig = dataSource.getDataSourceConfig('to-update'); expect(updatedConfig?.credentials).toMatchObject({ host: 'newhost', }); }); it('should get data sources by type', async () => { const postgresConfig: DataSourceConfig = { name: 'postgres', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; const mysqlConfig: DataSourceConfig = { name: 'mysql', type: DataSourceType.MySQL, credentials: { type: DataSourceType.MySQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; await dataSource.addDataSource(postgresConfig); await dataSource.addDataSource(mysqlConfig); const postgresSources = dataSource.getDataSourcesByType(DataSourceType.PostgreSQL); expect(postgresSources).toHaveLength(1); expect(postgresSources[0]?.name).toBe('postgres'); const mysqlSources = dataSource.getDataSourcesByType(DataSourceType.MySQL); expect(mysqlSources).toHaveLength(1); expect(mysqlSources[0]?.name).toBe('mysql'); }); }); describe('Query Execution', () => { beforeEach(async () => { const config: DataSourceConfig = { name: 'test-db', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; dataSource = new DataSource({ dataSources: [config] }); }); it('should execute query successfully', async () => { const result = await dataSource.execute({ sql: 'SELECT 1 as test', }); expect(result.success).toBe(true); expect(result.rows).toEqual([{ test: 'value' }]); expect(result.warehouse).toBe('test-db'); expect(mockAdapter.query).toHaveBeenCalledWith( 'SELECT 1 as test', undefined, undefined, undefined ); }); it('should execute query with parameters', async () => { const result = await dataSource.execute({ sql: 'SELECT * FROM users WHERE id = ?', params: [123], }); expect(result.success).toBe(true); expect(mockAdapter.query).toHaveBeenCalledWith( 'SELECT * FROM users WHERE id = ?', [123], undefined, undefined ); }); it('should route query to specific warehouse', async () => { const result = await dataSource.execute({ sql: 'SELECT 1', warehouse: 'test-db', }); expect(result.success).toBe(true); expect(result.warehouse).toBe('test-db'); }); it('should handle query errors gracefully', async () => { vi.mocked(mockAdapter.query).mockRejectedValue(new Error('Query failed')); const result = await dataSource.execute({ sql: 'SELECT * FROM non_existent_table', }); expect(result.success).toBe(false); expect(result.error).toBeDefined(); expect(result.error?.code).toBe('QUERY_EXECUTION_ERROR'); expect(result.error?.message).toBe('Query failed'); }); it('should throw error for non-existent data source', async () => { await expect( dataSource.execute({ sql: 'SELECT 1', warehouse: 'non-existent', }) ).rejects.toThrow("Specified data source 'non-existent' not found"); }); it('should execute query with maxRows option', async () => { const result = await dataSource.execute({ sql: 'SELECT * FROM users', options: { maxRows: 100 }, }); expect(result.success).toBe(true); expect(mockAdapter.query).toHaveBeenCalledWith( 'SELECT * FROM users', undefined, 100, undefined ); }); it('should include metadata when results are limited', async () => { // Mock adapter to return hasMoreRows flag vi.mocked(mockAdapter.query).mockResolvedValue({ rows: [{ test: 'value1' }, { test: 'value2' }], fields: [{ name: 'test', type: 'string' }], rowCount: 2, hasMoreRows: true, }); const result = await dataSource.execute({ sql: 'SELECT * FROM large_table', options: { maxRows: 2 }, }); expect(result.success).toBe(true); expect(result.rows).toHaveLength(2); expect(result.metadata).toBeDefined(); expect(result.metadata?.limited).toBe(true); expect(result.metadata?.maxRows).toBe(2); }); it('should not include metadata when results are not limited', async () => { // Mock adapter to return no hasMoreRows flag vi.mocked(mockAdapter.query).mockResolvedValue({ rows: [{ test: 'value' }], fields: [{ name: 'test', type: 'string' }], rowCount: 1, hasMoreRows: false, }); const result = await dataSource.execute({ sql: 'SELECT * FROM small_table', options: { maxRows: 100 }, }); expect(result.success).toBe(true); expect(result.rows).toHaveLength(1); expect(result.metadata?.limited).toBe(false); }); }); describe('Introspection Methods', () => { beforeEach(async () => { const config: DataSourceConfig = { name: 'test-db', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; dataSource = new DataSource({ dataSources: [config] }); // Setup mock introspector responses vi.mocked(mockIntrospector.getDatabases).mockResolvedValue([ { name: 'test_db', owner: 'admin' }, ]); vi.mocked(mockIntrospector.getSchemas).mockResolvedValue([ { name: 'public', database: 'test_db' }, ]); vi.mocked(mockIntrospector.getTables).mockResolvedValue([ { name: 'users', schema: 'public', database: 'test_db', type: 'TABLE', rowCount: 100, }, ]); vi.mocked(mockIntrospector.getColumns).mockResolvedValue([ { name: 'id', table: 'users', schema: 'public', database: 'test_db', position: 1, dataType: 'integer', isNullable: false, isPrimaryKey: true, }, ]); vi.mocked(mockIntrospector.getViews).mockResolvedValue([ { name: 'user_view', schema: 'public', database: 'test_db', definition: 'SELECT * FROM users', }, ]); const fixedDate = new Date('2024-01-01T00:00:00.000Z'); vi.mocked(mockIntrospector.getTableStatistics).mockResolvedValue({ table: 'users', schema: 'public', database: 'test_db', rowCount: 100, columnStatistics: [ { columnName: 'id', distinctCount: 100, nullCount: 0, minValue: '1', maxValue: '100', sampleValues: '1,2,3,4,5', }, { columnName: 'name', distinctCount: 95, nullCount: 5, minValue: undefined, maxValue: undefined, sampleValues: 'Alice,Bob,Charlie', }, ], lastUpdated: fixedDate, }); vi.mocked(mockIntrospector.getDataSourceType).mockReturnValue('postgresql'); }); it('should get introspector instance', async () => { const introspector = await dataSource.introspect('test-db'); expect(introspector).toBeDefined(); expect(typeof introspector.getDatabases).toBe('function'); expect(typeof introspector.getSchemas).toBe('function'); expect(typeof introspector.getTables).toBe('function'); expect(typeof introspector.getColumns).toBe('function'); expect(typeof introspector.getViews).toBe('function'); expect(typeof introspector.getTableStatistics).toBe('function'); expect(typeof introspector.getColumnStatistics).toBe('function'); expect(typeof introspector.getFullIntrospection).toBe('function'); expect(mockAdapter.introspect).toHaveBeenCalled(); }); it('should get databases', async () => { const databases = await dataSource.getDatabases('test-db'); expect(databases).toEqual([{ name: 'test_db', owner: 'admin' }]); expect(mockIntrospector.getDatabases).toHaveBeenCalled(); }); it('should get schemas', async () => { const schemas = await dataSource.getSchemas('test-db', 'test_db'); expect(schemas).toEqual([{ name: 'public', database: 'test_db' }]); expect(mockIntrospector.getSchemas).toHaveBeenCalledWith('test_db'); }); it('should get tables', async () => { const tables = await dataSource.getTables('test-db', 'test_db', 'public'); expect(tables).toEqual([ { name: 'users', schema: 'public', database: 'test_db', type: 'TABLE', rowCount: 100, }, ]); expect(mockIntrospector.getTables).toHaveBeenCalledWith('test_db', 'public'); }); it('should get columns', async () => { const columns = await dataSource.getColumns('test-db', 'test_db', 'public', 'users'); expect(columns).toEqual([ { name: 'id', table: 'users', schema: 'public', database: 'test_db', position: 1, dataType: 'integer', isNullable: false, isPrimaryKey: true, }, ]); expect(mockIntrospector.getColumns).toHaveBeenCalledWith('test_db', 'public', 'users'); }); it('should get views', async () => { const views = await dataSource.getViews('test-db', 'test_db', 'public'); expect(views).toEqual([ { name: 'user_view', schema: 'public', database: 'test_db', definition: 'SELECT * FROM users', }, ]); expect(mockIntrospector.getViews).toHaveBeenCalledWith('test_db', 'public'); }); it('should get table statistics', async () => { const stats = await dataSource.getTableStatistics('test_db', 'public', 'users', 'test-db'); expect(stats).toEqual({ table: 'users', schema: 'public', database: 'test_db', rowCount: 100, columnStatistics: [ { columnName: 'id', distinctCount: 100, nullCount: 0, minValue: '1', maxValue: '100', sampleValues: '1,2,3,4,5', }, { columnName: 'name', distinctCount: 95, nullCount: 5, minValue: undefined, maxValue: undefined, sampleValues: 'Alice,Bob,Charlie', }, ], lastUpdated: new Date('2024-01-01T00:00:00.000Z'), }); expect(mockIntrospector.getTableStatistics).toHaveBeenCalledWith( 'test_db', 'public', 'users' ); }); it('should get full introspection', async () => { const mockFullResult = { dataSourceName: 'test-db', dataSourceType: 'postgresql', databases: [{ name: 'test_db', owner: 'admin' }], schemas: [{ name: 'public', database: 'test_db' }], tables: [], columns: [], views: [], introspectedAt: new Date(), }; vi.mocked(mockIntrospector.getFullIntrospection).mockResolvedValue(mockFullResult); const result = await dataSource.getFullIntrospection('test-db'); expect(result).toEqual(mockFullResult); expect(mockIntrospector.getFullIntrospection).toHaveBeenCalled(); }); it('should use default data source when none specified', async () => { // Create data source with default const config: DataSourceConfig = { name: 'default-db', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; dataSource = new DataSource({ dataSources: [config], defaultDataSource: 'default-db', }); await dataSource.getDatabases(); // No data source specified expect(mockAdapter.introspect).toHaveBeenCalled(); }); }); describe('Connection Testing', () => { beforeEach(async () => { const config: DataSourceConfig = { name: 'test-db', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; dataSource = new DataSource({ dataSources: [config] }); }); it('should test single data source connection', async () => { const result = await dataSource.testDataSource('test-db'); expect(result).toBe(true); expect(mockAdapter.testConnection).toHaveBeenCalled(); }); it('should handle connection test failure', async () => { // Create a simple test that verifies the method catches errors and returns false const config: DataSourceConfig = { name: 'failing-db', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'invalid-host', database: 'test', username: 'user', password: 'pass', }, }; const testDataSource = new DataSource({ dataSources: [config] }); // Mock createAdapter to throw an error for this specific call const { createAdapter } = await import('../../src/adapters/factory'); vi.mocked(createAdapter).mockRejectedValueOnce(new Error('Connection failed')); const result = await testDataSource.testDataSource('failing-db'); expect(result).toBe(false); await testDataSource.close(); }); it('should test all data source connections', async () => { const results = await dataSource.testAllDataSources(); expect(results).toEqual({ 'test-db': true }); }); }); describe('Error Handling', () => { it('should throw error when no data sources configured and trying to get default', async () => { dataSource = new DataSource({ dataSources: [] }); await expect(dataSource.execute({ sql: 'SELECT 1' })).rejects.toThrow( 'No data source specified in request and no default data source configured' ); }); it('should throw error when multiple data sources but no default specified', async () => { const configs: DataSourceConfig[] = [ { name: 'db1', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }, { name: 'db2', type: DataSourceType.MySQL, credentials: { type: DataSourceType.MySQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }, ]; dataSource = new DataSource({ dataSources: configs }); await expect(dataSource.execute({ sql: 'SELECT 1' })).rejects.toThrow( 'No data source specified in request and no default data source configured' ); }); }); describe('Backward Compatibility', () => { it('should work with QueryRouter alias', () => { const config: DataSourceConfig = { name: 'test-db', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; const router = new QueryRouter({ dataSources: [config] }); expect(router.getDataSources()).toEqual(['test-db']); }); }); describe('Resource Cleanup', () => { it('should close all adapters on close', async () => { const config: DataSourceConfig = { name: 'test-db', type: DataSourceType.PostgreSQL, credentials: { type: DataSourceType.PostgreSQL, host: 'localhost', database: 'test', username: 'user', password: 'pass', }, }; dataSource = new DataSource({ dataSources: [config] }); // Trigger adapter creation await dataSource.execute({ sql: 'SELECT 1' }); await dataSource.close(); expect(mockAdapter.close).toHaveBeenCalled(); }); }); });