1- import { describe , it , expect , beforeAll , afterAll } from 'vitest' ;
1+ import { describe , it , expect , beforeAll , afterAll , vi } from 'vitest' ;
22import { PostgreSqlContainer , StartedPostgreSqlContainer } from '@testcontainers/postgresql' ;
33import { PostgresConnector } from '../postgres/index.js' ;
44import { 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 * as sshConfigParser from '../../utils/ssh-config-parser.js' ;
89
910describe ( 'PostgreSQL SSH Tunnel Simple Integration Tests' , ( ) => {
1011 let postgresContainer : StartedPostgreSqlContainer ;
@@ -104,5 +105,117 @@ describe('PostgreSQL SSH Tunnel Simple Integration Tests', () => {
104105 delete process . env . SSH_USER ;
105106 }
106107 } ) ;
108+
109+ it ( 'should handle SSH config file resolution' , async ( ) => {
110+ 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+ // Spy on the SSH tunnel establish method to verify the config values
117+ const mockSSHTunnelEstablish = vi . spyOn ( SSHTunnel . prototype , 'establish' ) ;
118+
119+ 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+
129+ // Mock SSH tunnel establish to capture the config and prevent actual connection
130+ 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+
135+ 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' ) ;
143+
144+ // Verify that SSH tunnel was attempted with the correct config values from SSH config
145+ expect ( mockSSHTunnelEstablish ) . toHaveBeenCalledTimes ( 1 ) ;
146+ const sshTunnelCall = mockSSHTunnelEstablish . mock . calls [ 0 ] ;
147+ 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
154+ 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
159+ } ) ;
160+
161+ // 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
173+ const originalDsnUrl = new URL ( dsn ) ;
174+ expect ( tunnelOptions . targetHost ) . toBe ( originalDsnUrl . hostname ) ;
175+ expect ( tunnelOptions . targetPort ) . toBe ( parseInt ( originalDsnUrl . port ) ) ;
176+
177+ } finally {
178+ // Clean up
179+ delete process . env . SSH_HOST ;
180+ mockParseSSHConfig . mockRestore ( ) ;
181+ mockLooksLikeSSHAlias . mockRestore ( ) ;
182+ mockSSHTunnelEstablish . mockRestore ( ) ;
183+ }
184+ } ) ;
185+
186+ it ( 'should skip SSH config lookup for direct hostnames' , async ( ) => {
187+ 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+
193+ 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+
202+ 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+
211+ } 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 ( ) ;
218+ }
219+ } ) ;
107220 } ) ;
108221} ) ;
0 commit comments