"""Tests for SOCKSProxy server — _handle_client and start/stop.""" import asyncio from unittest.mock import AsyncMock, MagicMock import pytest from i2p_apps.i2ptunnel.socks_proxy import ( SOCKSProxy, parse_socks_greeting, parse_socks_request, build_socks_greeting_reply, build_socks_reply, SOCKS_VERSION, AUTH_NONE, REPLY_HOST_UNREACHABLE, ) def _mock_writer(): writer = MagicMock() writer.write = MagicMock() writer.drain = AsyncMock() writer.close = MagicMock() writer.wait_closed = AsyncMock() return writer class TestSOCKSProxyProperties: def test_defaults(self): proxy = SOCKSProxy() assert proxy.listen_address == ("127.0.0.1", 4445) assert proxy.is_running is False def test_custom(self): proxy = SOCKSProxy("0.0.0.0", 9050) assert proxy.listen_address == ("0.0.0.0", 9050) @pytest.mark.asyncio async def test_start_stop(self): proxy = SOCKSProxy("127.0.0.1", 0) await proxy.start() assert proxy.is_running is True await proxy.stop() assert proxy.is_running is False @pytest.mark.asyncio async def test_stop_without_start(self): proxy = SOCKSProxy() await proxy.stop() assert proxy.is_running is False class TestSOCKSProxyHandleClient: @pytest.mark.asyncio async def test_valid_connect(self): """Full SOCKS5 handshake: greeting → reply → request → host unreachable.""" proxy = SOCKSProxy() # Build greeting: VER=5, NMETHODS=1, METHODS=[0x00] greeting = bytes([SOCKS_VERSION, 1, AUTH_NONE]) # Build request: VER=5, CMD=1 (CONNECT), RSV=0, ATYP=3 (domain) domain = b"example.i2p" request = bytes([SOCKS_VERSION, 1, 0, 3, len(domain)]) + domain + b"\x01\xbb" # port 443 read_count = 0 async def mock_read(n): nonlocal read_count read_count += 1 if read_count == 1: return greeting return request reader = AsyncMock() reader.read = mock_read writer = _mock_writer() await proxy._handle_client(reader, writer) # Should have sent greeting reply then SOCKS reply calls = writer.write.call_args_list assert len(calls) == 2 # First call: greeting reply assert calls[0][0][0] == build_socks_greeting_reply(AUTH_NONE) # Second call: host unreachable reply assert calls[1][0][0] == build_socks_reply(REPLY_HOST_UNREACHABLE) @pytest.mark.asyncio async def test_no_acceptable_auth(self): """Client offers only auth methods we don't support → 0xFF reply.""" proxy = SOCKSProxy() greeting = bytes([SOCKS_VERSION, 1, 0x02]) # only USERNAME/PASSWORD reader = AsyncMock() reader.read = AsyncMock(return_value=greeting) writer = _mock_writer() await proxy._handle_client(reader, writer) calls = writer.write.call_args_list assert len(calls) == 1 assert calls[0][0][0] == build_socks_greeting_reply(0xFF) writer.close.assert_called() @pytest.mark.asyncio async def test_exception_closes_writer(self): proxy = SOCKSProxy() reader = AsyncMock() reader.read = AsyncMock(side_effect=ConnectionResetError("reset")) writer = _mock_writer() await proxy._handle_client(reader, writer) writer.close.assert_called() @pytest.mark.asyncio async def test_invalid_greeting(self): """Too-short greeting → ValueError → exception path.""" proxy = SOCKSProxy() reader = AsyncMock() reader.read = AsyncMock(return_value=b"\x04") # SOCKS4, not 5 writer = _mock_writer() await proxy._handle_client(reader, writer) writer.close.assert_called()