@@ -154,3 +154,108 @@ def mock_connect_impl(*args: Any, **kwargs: Any) -> MagicMock:
154154
155155 # Verify temp dir was cleaned up
156156 assert not os .path .exists (kwargs ["host" ])
157+
158+
159+ def test_proxy_timeout () -> None :
160+ """Test that _proxy exits and cleans up on selectors timeout."""
161+ local_client , local_server = socket .socketpair ()
162+ remote_client , remote_server = socket .socketpair ()
163+
164+ # Mock selectors.DefaultSelector.select to return empty list (timeout)
165+ with patch (
166+ "google.cloud.sql.connector.psycopg.selectors.DefaultSelector"
167+ ) as mock_selector_cls :
168+ mock_selector = MagicMock ()
169+ mock_selector .select .return_value = [] # Timeout
170+ mock_selector_cls .return_value = mock_selector
171+
172+ # Run proxy
173+ _proxy (local_server , remote_client )
174+
175+ # Sockets should be closed
176+ assert local_server .fileno () == - 1
177+ assert remote_client .fileno () == - 1
178+
179+ # Clean up outer sockets
180+ local_client .close ()
181+ remote_server .close ()
182+
183+
184+ @patch ("psycopg.connect" )
185+ def test_connect_wrapper_failure (mock_psycopg_connect : MagicMock ) -> None :
186+ """Test that connect wrapper cleans up correctly when psycopg.connect fails."""
187+ mock_remote_sock = MagicMock (spec = ssl .SSLSocket )
188+ mock_psycopg_connect .side_effect = Exception ("connection failed simulated" )
189+
190+ # Call the connect wrapper and expect it to raise
191+ import pytest
192+
193+ with pytest .raises (Exception , match = "connection failed simulated" ):
194+ connect (
195+ "127.0.0.1" ,
196+ mock_remote_sock ,
197+ user = "test_user" ,
198+ db = "test_db" ,
199+ password = "test_password" ,
200+ )
201+
202+ # Verify remote socket was closed
203+ assert mock_remote_sock .close .called
204+
205+ # Verify cleanup with mocked paths
206+ with patch (
207+ "google.cloud.sql.connector.psycopg.tempfile.mkdtemp"
208+ ) as mock_mkdtemp :
209+ mock_mkdtemp .return_value = "/tmp/mock_temp_dir_failure"
210+
211+ with patch (
212+ "google.cloud.sql.connector.psycopg.os.rmdir"
213+ ) as mock_rmdir , patch (
214+ "google.cloud.sql.connector.psycopg.os.remove"
215+ ) as mock_remove , patch (
216+ "google.cloud.sql.connector.psycopg.socket.socket"
217+ ) as mock_socket_cls :
218+ # Mock the local socket to avoid real OS bind/listen
219+ mock_local_sock = MagicMock ()
220+ mock_socket_cls .return_value = mock_local_sock
221+
222+ with pytest .raises (Exception , match = "connection failed simulated" ):
223+ connect (
224+ "127.0.0.1" ,
225+ mock_remote_sock ,
226+ user = "test_user" ,
227+ db = "test_db" ,
228+ password = "test_password" ,
229+ )
230+
231+ # Verify rmdir and remove were called for cleanup
232+ mock_rmdir .assert_called_once_with ("/tmp/mock_temp_dir_failure" )
233+ mock_remove .assert_called_once ()
234+
235+
236+ def test_proxy_remote_eof () -> None :
237+ """Test that _proxy exits when remote socket receives EOF."""
238+ local_client , local_server = socket .socketpair ()
239+ remote_client , remote_server = socket .socketpair ()
240+
241+ # Start proxy in background
242+ proxy_thread = threading .Thread (
243+ target = _proxy , args = (local_server , remote_client ), daemon = True
244+ )
245+ proxy_thread .start ()
246+
247+ # Close remote server to trigger EOF on remote_client
248+ remote_server .close ()
249+
250+ # local_client should receive EOF (b"")
251+ assert local_client .recv (1024 ) == b""
252+
253+ # Wait for proxy thread to finish
254+ proxy_thread .join (timeout = 2.0 )
255+ assert not proxy_thread .is_alive ()
256+
257+ # Sockets should be closed
258+ assert local_server .fileno () == - 1
259+ assert remote_client .fileno () == - 1
260+
261+ local_client .close ()
0 commit comments