Skip to main content
Glama
postgres-ssh.integration.test.ts7.55 kB
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql'; import { PostgresConnector } from '../postgres/index.js'; import { ConnectorManager } from '../manager.js'; import { ConnectorRegistry } from '../interface.js'; import { SSHTunnel } from '../../utils/ssh-tunnel.js'; import type { SSHTunnelConfig } from '../../types/ssh.js'; import type { SourceConfig } from '../../types/config.js'; import * as sshConfigParser from '../../utils/ssh-config-parser.js'; /** * Helper function to create a SourceConfig from a DSN for testing */ function createSourceConfigFromDSN(dsn: string, sourceId: string = 'test'): SourceConfig { return { id: sourceId, dsn: dsn, }; } describe('PostgreSQL SSH Tunnel Simple Integration Tests', () => { let postgresContainer: StartedPostgreSqlContainer; beforeAll(async () => { // Register PostgreSQL connector ConnectorRegistry.register(new PostgresConnector()); // Start PostgreSQL container postgresContainer = await new PostgreSqlContainer('postgres:15-alpine') .withDatabase('testdb') .withUsername('testuser') .withPassword('testpass') .start(); }, 60000); // 1 minute timeout for container startup afterAll(async () => { await postgresContainer?.stop(); }); describe('SSH Tunnel Basic Functionality', () => { it('should establish SSH tunnel and connect to local port', async () => { // For this test, we'll create a mock SSH tunnel that just forwards to the same port // This tests the tunnel establishment logic without needing a real SSH server const tunnel = new SSHTunnel(); // Test that the tunnel correctly reports its state expect(tunnel.getIsConnected()).toBe(false); expect(tunnel.getTunnelInfo()).toBeNull(); }); it('should parse DSN correctly when SSH tunnel is configured', async () => { const manager = new ConnectorManager(); // Test DSN parsing with getDefaultPort const testCases = [ { dsn: 'postgres://user:pass@host:5432/db', expectedPort: 5432 }, { dsn: 'mysql://user:pass@host:3306/db', expectedPort: 3306 }, { dsn: 'mariadb://user:pass@host:3306/db', expectedPort: 3306 }, { dsn: 'sqlserver://user:pass@host:1433/db', expectedPort: 1433 }, ]; for (const testCase of testCases) { // Access private method through reflection for testing const port = (manager as any).getDefaultPort(testCase.dsn); expect(port).toBe(testCase.expectedPort); } }); it('should handle connection without SSH tunnel', async () => { const manager = new ConnectorManager(); // Make sure no SSH config is set delete process.env.SSH_HOST; const dsn = postgresContainer.getConnectionUri(); const sourceConfig = createSourceConfigFromDSN(dsn); await manager.connectWithSources([sourceConfig]); // Test that connection works const connector = manager.getConnector(); const result = await connector.executeSQL('SELECT 1 as test', {}); expect(result.rows).toHaveLength(1); expect(result.rows[0].test).toBe(1); await manager.disconnect(); }); it('should fail gracefully when SSH config is invalid', async () => { const manager = new ConnectorManager(); // Create source config with invalid SSH config (missing required fields) const dsn = postgresContainer.getConnectionUri(); const sourceConfig = createSourceConfigFromDSN(dsn); sourceConfig.ssh_host = 'example.com'; // Missing ssh_user await expect(manager.connectWithSources([sourceConfig])).rejects.toThrow(/SSH tunnel requires ssh_user/); }); it('should validate SSH authentication method', async () => { const manager = new ConnectorManager(); // Create source config with SSH but without authentication method const dsn = postgresContainer.getConnectionUri(); const sourceConfig = createSourceConfigFromDSN(dsn); sourceConfig.ssh_host = 'example.com'; sourceConfig.ssh_user = 'testuser'; // Missing both ssh_password and ssh_key await expect(manager.connectWithSources([sourceConfig])).rejects.toThrow(/SSH tunnel requires either ssh_password or ssh_key/); }); it('should handle SSH tunnel with source config', async () => { const manager = new ConnectorManager(); // Spy on the SSH tunnel establish method to verify the config values const mockSSHTunnelEstablish = vi.spyOn(SSHTunnel.prototype, 'establish'); try { // Mock SSH tunnel establish to capture the config and prevent actual connection mockSSHTunnelEstablish.mockRejectedValue(new Error('SSH connection failed (expected in test)')); const dsn = postgresContainer.getConnectionUri(); const sourceConfig = createSourceConfigFromDSN(dsn); // Configure SSH tunnel via source config sourceConfig.ssh_host = 'bastion.example.com'; sourceConfig.ssh_user = 'sshuser'; sourceConfig.ssh_port = 2222; sourceConfig.ssh_key = '/home/user/.ssh/id_rsa'; // This should fail during SSH connection (expected), but we can verify the config await expect(manager.connectWithSources([sourceConfig])).rejects.toThrow(); // Verify that SSH tunnel was attempted with the correct config values expect(mockSSHTunnelEstablish).toHaveBeenCalledTimes(1); const sshTunnelCall = mockSSHTunnelEstablish.mock.calls[0]; const [sshConfig, tunnelOptions] = sshTunnelCall; // Verify SSH config values were properly set from source config expect(sshConfig).toMatchObject({ host: 'bastion.example.com', username: 'sshuser', port: 2222, privateKey: '/home/user/.ssh/id_rsa' }); // Verify tunnel options are correctly set up for the database connection const originalDsnUrl = new URL(dsn); expect(tunnelOptions.targetHost).toBe(originalDsnUrl.hostname); expect(tunnelOptions.targetPort).toBe(parseInt(originalDsnUrl.port)); } finally { mockSSHTunnelEstablish.mockRestore(); } }); it('should handle SSH tunnel with password authentication', async () => { const manager = new ConnectorManager(); // Spy on the SSH tunnel establish method const mockSSHTunnelEstablish = vi.spyOn(SSHTunnel.prototype, 'establish'); try { // Mock SSH tunnel establish to prevent actual connection mockSSHTunnelEstablish.mockRejectedValue(new Error('SSH connection failed (expected in test)')); const dsn = postgresContainer.getConnectionUri(); const sourceConfig = createSourceConfigFromDSN(dsn); // Configure SSH tunnel with password authentication sourceConfig.ssh_host = 'ssh.example.com'; sourceConfig.ssh_user = 'sshuser'; sourceConfig.ssh_password = 'sshpass'; // This should fail during actual SSH connection await expect(manager.connectWithSources([sourceConfig])).rejects.toThrow(); // Verify SSH tunnel was attempted with password auth expect(mockSSHTunnelEstablish).toHaveBeenCalledTimes(1); const [sshConfig] = mockSSHTunnelEstablish.mock.calls[0]; expect(sshConfig.password).toBe('sshpass'); } finally { mockSSHTunnelEstablish.mockRestore(); } }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bytebase/dbhub'

If you have feedback or need assistance with the MCP directory API, please join our Discord server