@@ -5,8 +5,19 @@ import { ConnectorManager } from '../manager.js';
55import { ConnectorRegistry } from '../interface.js' ;
66import { SSHTunnel } from '../../utils/ssh-tunnel.js' ;
77import type { SSHTunnelConfig } from '../../types/ssh.js' ;
8+ import type { SourceConfig } from '../../types/config.js' ;
89import * as sshConfigParser from '../../utils/ssh-config-parser.js' ;
910
11+ /**
12+ * Helper function to create a SourceConfig from a DSN for testing
13+ */
14+ function createSourceConfigFromDSN ( dsn : string , sourceId : string = 'test' ) : SourceConfig {
15+ return {
16+ id : sourceId ,
17+ dsn : dsn ,
18+ } ;
19+ }
20+
1021describe ( 'PostgreSQL SSH Tunnel Simple Integration Tests' , ( ) => {
1122 let postgresContainer : StartedPostgreSqlContainer ;
1223
@@ -60,161 +71,119 @@ describe('PostgreSQL SSH Tunnel Simple Integration Tests', () => {
6071
6172 // Make sure no SSH config is set
6273 delete process . env . SSH_HOST ;
63-
74+
6475 const dsn = postgresContainer . getConnectionUri ( ) ;
65-
66- await manager . connectWithDSN ( dsn ) ;
67-
76+ const sourceConfig = createSourceConfigFromDSN ( dsn ) ;
77+
78+ await manager . connectWithSources ( [ sourceConfig ] ) ;
79+
6880 // Test that connection works
6981 const connector = manager . getConnector ( ) ;
7082 const result = await connector . executeSQL ( 'SELECT 1 as test' , { } ) ;
7183 expect ( result . rows ) . toHaveLength ( 1 ) ;
7284 expect ( result . rows [ 0 ] . test ) . toBe ( 1 ) ;
73-
85+
7486 await manager . disconnect ( ) ;
7587 } ) ;
7688
7789 it ( 'should fail gracefully when SSH config is invalid' , async ( ) => {
7890 const manager = new ConnectorManager ( ) ;
79-
80- // Set invalid SSH config (missing required fields)
81- process . env . SSH_HOST = 'example.com' ;
82- // Missing SSH_USER
83-
84- try {
85- const dsn = postgresContainer . getConnectionUri ( ) ;
86- await expect ( manager . connectWithDSN ( dsn ) ) . rejects . toThrow ( / S S H t u n n e l c o n f i g u r a t i o n r e q u i r e s / ) ;
87- } finally {
88- delete process . env . SSH_HOST ;
89- }
91+
92+ // Create source config with invalid SSH config (missing required fields)
93+ const dsn = postgresContainer . getConnectionUri ( ) ;
94+ const sourceConfig = createSourceConfigFromDSN ( dsn ) ;
95+ sourceConfig . ssh_host = 'example.com' ;
96+ // Missing ssh_user
97+
98+ await expect ( manager . connectWithSources ( [ sourceConfig ] ) ) . rejects . toThrow ( / S S H t u n n e l r e q u i r e s s s h _ u s e r / ) ;
9099 } ) ;
91100
92101 it ( 'should validate SSH authentication method' , async ( ) => {
93102 const manager = new ConnectorManager ( ) ;
94-
95- // Set SSH config without authentication method
96- process . env . SSH_HOST = 'example.com' ;
97- process . env . SSH_USER = 'testuser' ;
98- // Missing both SSH_PASSWORD and SSH_KEY
99-
100- try {
101- const dsn = postgresContainer . getConnectionUri ( ) ;
102- await expect ( manager . connectWithDSN ( dsn ) ) . rejects . toThrow ( / S S H t u n n e l c o n f i g u r a t i o n r e q u i r e s e i t h e r / ) ;
103- } finally {
104- delete process . env . SSH_HOST ;
105- delete process . env . SSH_USER ;
106- }
103+
104+ // Create source config with SSH but without authentication method
105+ const dsn = postgresContainer . getConnectionUri ( ) ;
106+ const sourceConfig = createSourceConfigFromDSN ( dsn ) ;
107+ sourceConfig . ssh_host = 'example.com' ;
108+ sourceConfig . ssh_user = 'testuser' ;
109+ // Missing both ssh_password and ssh_key
110+
111+ await expect ( manager . connectWithSources ( [ sourceConfig ] ) ) . rejects . toThrow ( / S S H t u n n e l r e q u i r e s e i t h e r s s h _ p a s s w o r d o r s s h _ k e y / ) ;
107112 } ) ;
108113
109- it ( 'should handle SSH config file resolution ' , async ( ) => {
114+ it ( 'should handle SSH tunnel with source config ' , async ( ) => {
110115 const manager = new ConnectorManager ( ) ;
111-
112- // Mock the SSH config parser functions
113- const mockParseSSHConfig = vi . spyOn ( sshConfigParser , 'parseSSHConfig' ) ;
114- const mockLooksLikeSSHAlias = vi . spyOn ( sshConfigParser , 'looksLikeSSHAlias' ) ;
115-
116+
116117 // Spy on the SSH tunnel establish method to verify the config values
117118 const mockSSHTunnelEstablish = vi . spyOn ( SSHTunnel . prototype , 'establish' ) ;
118-
119+
119120 try {
120- // Configure mocks to simulate SSH config file lookup with specific values
121- mockLooksLikeSSHAlias . mockReturnValue ( true ) ;
122- mockParseSSHConfig . mockReturnValue ( {
123- host : 'bastion.example.com' ,
124- username : 'sshuser' ,
125- port : 2222 ,
126- privateKey : '/home/user/.ssh/id_rsa'
127- } ) ;
128-
129121 // Mock SSH tunnel establish to capture the config and prevent actual connection
130122 mockSSHTunnelEstablish . mockRejectedValue ( new Error ( 'SSH connection failed (expected in test)' ) ) ;
131-
132- // Set SSH host alias (would normally come from command line)
133- process . env . SSH_HOST = 'mybastion' ;
134-
123+
135124 const dsn = postgresContainer . getConnectionUri ( ) ;
136-
137- // This should fail during SSH connection (expected), but we can verify the config parsing
138- await expect ( manager . connectWithDSN ( dsn ) ) . rejects . toThrow ( ) ;
139-
140- // Verify that SSH config parsing functions were called correctly
141- expect ( mockLooksLikeSSHAlias ) . toHaveBeenCalledWith ( 'mybastion' ) ;
142- expect ( mockParseSSHConfig ) . toHaveBeenCalledWith ( 'mybastion' , expect . stringContaining ( '.ssh/config' ) ) ;
143-
144- // Verify that SSH tunnel was attempted with the correct config values from SSH config
125+ const sourceConfig = createSourceConfigFromDSN ( dsn ) ;
126+
127+ // Configure SSH tunnel via source config
128+ sourceConfig . ssh_host = 'bastion.example.com' ;
129+ sourceConfig . ssh_user = 'sshuser' ;
130+ sourceConfig . ssh_port = 2222 ;
131+ sourceConfig . ssh_key = '/home/user/.ssh/id_rsa' ;
132+
133+ // This should fail during SSH connection (expected), but we can verify the config
134+ await expect ( manager . connectWithSources ( [ sourceConfig ] ) ) . rejects . toThrow ( ) ;
135+
136+ // Verify that SSH tunnel was attempted with the correct config values
145137 expect ( mockSSHTunnelEstablish ) . toHaveBeenCalledTimes ( 1 ) ;
146138 const sshTunnelCall = mockSSHTunnelEstablish . mock . calls [ 0 ] ;
147139 const [ sshConfig , tunnelOptions ] = sshTunnelCall ;
148-
149- // Debug: Log the actual values being passed (for verification)
150- // SSH Config should contain the values from our mocked SSH config file
151- // Tunnel Options should contain database connection details from the container DSN
152-
153- // Verify SSH config values were properly resolved from the SSH config file
140+
141+ // Verify SSH config values were properly set from source config
154142 expect ( sshConfig ) . toMatchObject ( {
155- host : 'bastion.example.com' , // Should use HostName from SSH config
156- username : 'sshuser' , // Should use User from SSH config
157- port : 2222 , // Should use Port from SSH config
158- privateKey : '/home/user/.ssh/id_rsa' // Should use IdentityFile from SSH config
143+ host : 'bastion.example.com' ,
144+ username : 'sshuser' ,
145+ port : 2222 ,
146+ privateKey : '/home/user/.ssh/id_rsa'
159147 } ) ;
160-
148+
161149 // Verify tunnel options are correctly set up for the database connection
162- expect ( tunnelOptions ) . toMatchObject ( {
163- targetHost : expect . any ( String ) , // Database host from DSN
164- targetPort : expect . any ( Number ) // Database port from DSN
165- } ) ;
166-
167- // The localPort might be undefined for dynamic allocation, so check separately if it exists
168- if ( tunnelOptions . localPort !== undefined ) {
169- expect ( typeof tunnelOptions . localPort ) . toBe ( 'number' ) ;
170- }
171-
172- // Verify that the target database details from the DSN are preserved
173150 const originalDsnUrl = new URL ( dsn ) ;
174151 expect ( tunnelOptions . targetHost ) . toBe ( originalDsnUrl . hostname ) ;
175152 expect ( tunnelOptions . targetPort ) . toBe ( parseInt ( originalDsnUrl . port ) ) ;
176-
153+
177154 } finally {
178- // Clean up
179- delete process . env . SSH_HOST ;
180- mockParseSSHConfig . mockRestore ( ) ;
181- mockLooksLikeSSHAlias . mockRestore ( ) ;
182155 mockSSHTunnelEstablish . mockRestore ( ) ;
183156 }
184157 } ) ;
185158
186- it ( 'should skip SSH config lookup for direct hostnames ' , async ( ) => {
159+ it ( 'should handle SSH tunnel with password authentication ' , async ( ) => {
187160 const manager = new ConnectorManager ( ) ;
188-
189- // Mock the SSH config parser functions
190- const mockParseSSHConfig = vi . spyOn ( sshConfigParser , 'parseSSHConfig' ) ;
191- const mockLooksLikeSSHAlias = vi . spyOn ( sshConfigParser , 'looksLikeSSHAlias' ) ;
192-
161+
162+ // Spy on the SSH tunnel establish method
163+ const mockSSHTunnelEstablish = vi . spyOn ( SSHTunnel . prototype , 'establish' ) ;
164+
193165 try {
194- // Configure mocks - direct hostname should not trigger SSH config lookup
195- mockLooksLikeSSHAlias . mockReturnValue ( false ) ;
196-
197- // Set a direct hostname with required SSH credentials
198- process . env . SSH_HOST = 'ssh.example.com' ;
199- process . env . SSH_USER = 'sshuser' ;
200- process . env . SSH_PASSWORD = 'sshpass' ;
201-
166+ // Mock SSH tunnel establish to prevent actual connection
167+ mockSSHTunnelEstablish . mockRejectedValue ( new Error ( 'SSH connection failed (expected in test)' ) ) ;
168+
202169 const dsn = postgresContainer . getConnectionUri ( ) ;
203-
204- // This should fail during actual SSH connection, but we can verify the parsing behavior
205- await expect ( manager . connectWithDSN ( dsn ) ) . rejects . toThrow ( ) ;
206-
207- // Verify that SSH config parsing was checked but not executed
208- expect ( mockLooksLikeSSHAlias ) . toHaveBeenCalledWith ( 'ssh.example.com' ) ;
209- expect ( mockParseSSHConfig ) . not . toHaveBeenCalled ( ) ;
210-
170+ const sourceConfig = createSourceConfigFromDSN ( dsn ) ;
171+
172+ // Configure SSH tunnel with password authentication
173+ sourceConfig . ssh_host = 'ssh.example.com' ;
174+ sourceConfig . ssh_user = 'sshuser' ;
175+ sourceConfig . ssh_password = 'sshpass' ;
176+
177+ // This should fail during actual SSH connection
178+ await expect ( manager . connectWithSources ( [ sourceConfig ] ) ) . rejects . toThrow ( ) ;
179+
180+ // Verify SSH tunnel was attempted with password auth
181+ expect ( mockSSHTunnelEstablish ) . toHaveBeenCalledTimes ( 1 ) ;
182+ const [ sshConfig ] = mockSSHTunnelEstablish . mock . calls [ 0 ] ;
183+ expect ( sshConfig . password ) . toBe ( 'sshpass' ) ;
184+
211185 } finally {
212- // Clean up
213- delete process . env . SSH_HOST ;
214- delete process . env . SSH_USER ;
215- delete process . env . SSH_PASSWORD ;
216- mockParseSSHConfig . mockRestore ( ) ;
217- mockLooksLikeSSHAlias . mockRestore ( ) ;
186+ mockSSHTunnelEstablish . mockRestore ( ) ;
218187 }
219188 } ) ;
220189 } ) ;
0 commit comments