"""Tests for GenericClientTask — TCP client tunnel.""" import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest from i2p_apps.i2ptunnel.client_task import GenericClientTask def _make_config(**overrides): config = MagicMock() config.name = "test-client" config.interface = "127.0.0.1" config.listen_port = 0 config.target_destination = overrides.get("target_destination", "example.i2p") config.type = MagicMock() return config def _make_task(**overrides): config = _make_config(**overrides) session = AsyncMock() return GenericClientTask(config, session) def _mock_writer(): writer = MagicMock() writer.write = MagicMock() writer.drain = AsyncMock() writer.close = MagicMock() writer.wait_closed = AsyncMock() return writer class TestParseDestinations: def test_single(self): result = GenericClientTask._parse_destinations("example.i2p") assert result == ["example.i2p"] def test_comma_separated(self): result = GenericClientTask._parse_destinations("a.i2p,b.i2p") assert result == ["a.i2p", "b.i2p"] def test_space_separated(self): result = GenericClientTask._parse_destinations("a.i2p b.i2p") assert result == ["a.i2p", "b.i2p"] def test_mixed(self): result = GenericClientTask._parse_destinations("a.i2p, b.i2p c.i2p") assert result == ["a.i2p", "b.i2p", "c.i2p"] def test_empty(self): result = GenericClientTask._parse_destinations("") assert result == [] class TestPickDestination: def test_single_always_picked(self): task = _make_task(target_destination="only.i2p") assert task._pick_destination() == "only.i2p" def test_multiple_picks_from_list(self): task = _make_task(target_destination="a.i2p,b.i2p,c.i2p") for _ in range(20): assert task._pick_destination() in ("a.i2p", "b.i2p", "c.i2p") class TestResolveDestination: @pytest.mark.asyncio async def test_b32_passthrough(self): task = _make_task() result = await task._resolve_destination("abc.b32.i2p") assert result == "abc.b32.i2p" @pytest.mark.asyncio async def test_i2p_lookup(self): task = _make_task() task._session.lookup = AsyncMock(return_value="resolved-dest") result = await task._resolve_destination("example.i2p") assert result == "resolved-dest" @pytest.mark.asyncio async def test_i2p_lookup_fail(self): task = _make_task() task._session.lookup = AsyncMock(return_value=None) result = await task._resolve_destination("unknown.i2p") assert result is None @pytest.mark.asyncio async def test_base64_passthrough(self): task = _make_task() result = await task._resolve_destination("AAAA...base64dest") assert result == "AAAA...base64dest" class TestHandleClient: @pytest.mark.asyncio async def test_resolve_fail_closes(self): task = _make_task(target_destination="unknown.i2p") task._session.lookup = AsyncMock(return_value=None) reader = AsyncMock() writer = _mock_writer() await task.handle_client(reader, writer) writer.close.assert_called() @pytest.mark.asyncio async def test_connect_fail_closes(self): task = _make_task(target_destination="abc.b32.i2p") task._session.connect = AsyncMock(side_effect=ConnectionError("fail")) reader = AsyncMock() writer = _mock_writer() await task.handle_client(reader, writer) writer.close.assert_called() @pytest.mark.asyncio async def test_success_bridges(self): task = _make_task(target_destination="abc.b32.i2p") remote_reader = AsyncMock() remote_writer = MagicMock() remote_writer.close = MagicMock() task._session.connect = AsyncMock(return_value=(remote_reader, remote_writer)) reader = AsyncMock() writer = _mock_writer() with patch("i2p_apps.i2ptunnel.client_task.bridge", new_callable=AsyncMock) as mock_bridge: await task.handle_client(reader, writer) mock_bridge.assert_called_once()