Skip to content

Commit 4fa7756

Browse files
committed
test: add comprehensive socket error and cleanup coverage tests for psycopg
1 parent 23516dd commit 4fa7756

1 file changed

Lines changed: 186 additions & 0 deletions

File tree

‎tests/unit/test_psycopg.py‎

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@
2424
from google.cloud.sql.connector.psycopg import connect
2525

2626

27+
class MockableSocket(socket.socket):
28+
pass
29+
30+
31+
def mockable_socketpair() -> tuple[MockableSocket, MockableSocket]:
32+
"""Create a socketpair wrapped in MockableSocket to allow method mocking."""
33+
s1, s2 = socket.socketpair()
34+
fd1 = s1.detach()
35+
fd2 = s2.detach()
36+
ms1 = MockableSocket(socket.AF_UNIX, socket.SOCK_STREAM, fileno=fd1)
37+
ms2 = MockableSocket(socket.AF_UNIX, socket.SOCK_STREAM, fileno=fd2)
38+
return ms1, ms2
39+
40+
2741
def test_proxy_bidirectional() -> None:
2842
"""Test that _proxy forwards bytes in both directions and exits on EOF."""
2943
# local_client <-> local_server (simulates psycopg <-> proxy)
@@ -259,3 +273,175 @@ def test_proxy_remote_eof() -> None:
259273
assert remote_client.fileno() == -1
260274

261275
local_client.close()
276+
277+
278+
def test_proxy_local_recv_error() -> None:
279+
"""Test that _proxy exits when local.recv raises OSError."""
280+
local_client, local_server = mockable_socketpair()
281+
remote_client, remote_server = mockable_socketpair()
282+
283+
# Mock local_server.recv to raise OSError
284+
local_server.recv = MagicMock(side_effect=OSError("local recv failed"))
285+
286+
# Trigger selector by sending data to local_server
287+
local_client.send(b"x")
288+
289+
# Run proxy. It should detect local is readable, call local.recv() which raises OSError, and exit.
290+
_proxy(local_server, remote_client)
291+
292+
# Sockets should be closed
293+
assert local_server.fileno() == -1
294+
assert remote_client.fileno() == -1
295+
296+
local_client.close()
297+
remote_server.close()
298+
299+
300+
def test_proxy_remote_send_error() -> None:
301+
"""Test that _proxy exits when remote.sendall raises OSError."""
302+
local_client, local_server = mockable_socketpair()
303+
remote_client, remote_server = mockable_socketpair()
304+
305+
# Mock remote_client.sendall to raise OSError
306+
remote_client.sendall = MagicMock(side_effect=OSError("remote send failed"))
307+
308+
# Trigger selector by sending data from local_client -> local_server
309+
local_client.send(b"x")
310+
311+
# Run proxy. It reads "x" from local, tries to send to remote, fails, and exits.
312+
_proxy(local_server, remote_client)
313+
314+
assert local_server.fileno() == -1
315+
assert remote_client.fileno() == -1
316+
317+
local_client.close()
318+
remote_server.close()
319+
320+
321+
def test_proxy_remote_recv_error() -> None:
322+
"""Test that _proxy exits when remote.recv raises OSError."""
323+
local_client, local_server = mockable_socketpair()
324+
remote_client, remote_server = mockable_socketpair()
325+
326+
# Mock remote_client.recv to raise OSError
327+
remote_client.recv = MagicMock(side_effect=OSError("remote recv failed"))
328+
329+
# Trigger selector by sending data from remote_server -> remote_client
330+
remote_server.send(b"x")
331+
332+
# Run proxy. It detects remote is readable, calls remote.recv() which fails, and exits.
333+
_proxy(local_server, remote_client)
334+
335+
assert local_server.fileno() == -1
336+
assert remote_client.fileno() == -1
337+
338+
local_client.close()
339+
remote_server.close()
340+
341+
342+
def test_proxy_local_send_error() -> None:
343+
"""Test that _proxy exits when local.sendall raises OSError."""
344+
local_client, local_server = mockable_socketpair()
345+
remote_client, remote_server = mockable_socketpair()
346+
347+
# Mock local_server.sendall to raise OSError
348+
local_server.sendall = MagicMock(side_effect=OSError("local send failed"))
349+
350+
# Trigger selector by sending data from remote_server -> remote_client
351+
remote_server.send(b"x")
352+
353+
# Run proxy. It reads "x" from remote, tries to send to local, fails, and exits.
354+
_proxy(local_server, remote_client)
355+
356+
assert local_server.fileno() == -1
357+
assert remote_client.fileno() == -1
358+
359+
local_client.close()
360+
remote_server.close()
361+
362+
363+
def test_proxy_pending_recv_error() -> None:
364+
"""Test that _proxy exits when remote.recv raises OSError during pending check."""
365+
local_client, local_server = socket.socketpair()
366+
remote_client, remote_server = mockable_socketpair()
367+
368+
# Mock remote_client (remote sock in proxy)
369+
remote_client.pending = MagicMock(return_value=10)
370+
remote_client.recv = MagicMock(side_effect=OSError("pending recv failed"))
371+
372+
# Run proxy
373+
_proxy(local_server, remote_client)
374+
375+
assert local_server.fileno() == -1
376+
assert remote_client.fileno() == -1
377+
378+
local_client.close()
379+
remote_server.close()
380+
381+
382+
def test_proxy_pending_local_send_error() -> None:
383+
"""Test that _proxy exits when local.sendall raises OSError during pending check."""
384+
local_client, local_server = mockable_socketpair()
385+
remote_client, remote_server = mockable_socketpair()
386+
387+
# Mock remote_client (remote sock in proxy)
388+
remote_client.pending = MagicMock(return_value=10)
389+
remote_client.recv = MagicMock(return_value=b"pending data")
390+
391+
# Mock local_server.sendall to raise OSError
392+
local_server.sendall = MagicMock(
393+
side_effect=OSError("local send pending failed")
394+
)
395+
396+
# Run proxy
397+
_proxy(local_server, remote_client)
398+
399+
assert local_server.fileno() == -1
400+
assert remote_client.fileno() == -1
401+
402+
local_client.close()
403+
remote_server.close()
404+
405+
406+
@patch.dict("sys.modules", {"psycopg": None})
407+
def test_connect_import_error() -> None:
408+
"""Test that connect raises ImportError if psycopg is not installed."""
409+
mock_remote_sock = MagicMock(spec=ssl.SSLSocket)
410+
411+
import pytest
412+
413+
with pytest.raises(ImportError, match='Unable to import module "psycopg."'):
414+
connect("127.0.0.1", mock_remote_sock)
415+
416+
417+
def test_connect_cleanup_errors() -> None:
418+
"""Test that connect ignores OSErrors when removing temp files/dirs during cleanup."""
419+
mock_remote_sock = MagicMock(spec=ssl.SSLSocket)
420+
421+
def mock_connect_impl(*args: Any, **kwargs: Any) -> MagicMock:
422+
host = kwargs.get("host")
423+
socket_path = os.path.join(host, ".s.PGSQL.5432")
424+
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
425+
client.connect(socket_path)
426+
client.close()
427+
return MagicMock()
428+
429+
with patch("psycopg.connect", side_effect=mock_connect_impl), patch(
430+
"google.cloud.sql.connector.psycopg.os.remove",
431+
side_effect=OSError("remove failed"),
432+
) as mock_remove, patch(
433+
"google.cloud.sql.connector.psycopg.os.rmdir",
434+
side_effect=OSError("rmdir failed"),
435+
) as mock_rmdir:
436+
conn = connect(
437+
"127.0.0.1",
438+
mock_remote_sock,
439+
user="test_user",
440+
db="test_db",
441+
password="test_password",
442+
)
443+
444+
assert conn is not None
445+
assert mock_remove.called
446+
assert mock_rmdir.called
447+

0 commit comments

Comments
 (0)