A Python port of the Invisible Internet Project (I2P)
1"""Tests for GenericClientTask — TCP client tunnel."""
2
3import asyncio
4from unittest.mock import AsyncMock, MagicMock, patch
5
6import pytest
7
8from i2p_apps.i2ptunnel.client_task import GenericClientTask
9
10
11def _make_config(**overrides):
12 config = MagicMock()
13 config.name = "test-client"
14 config.interface = "127.0.0.1"
15 config.listen_port = 0
16 config.target_destination = overrides.get("target_destination", "example.i2p")
17 config.type = MagicMock()
18 return config
19
20
21def _make_task(**overrides):
22 config = _make_config(**overrides)
23 session = AsyncMock()
24 return GenericClientTask(config, session)
25
26
27def _mock_writer():
28 writer = MagicMock()
29 writer.write = MagicMock()
30 writer.drain = AsyncMock()
31 writer.close = MagicMock()
32 writer.wait_closed = AsyncMock()
33 return writer
34
35
36class TestParseDestinations:
37 def test_single(self):
38 result = GenericClientTask._parse_destinations("example.i2p")
39 assert result == ["example.i2p"]
40
41 def test_comma_separated(self):
42 result = GenericClientTask._parse_destinations("a.i2p,b.i2p")
43 assert result == ["a.i2p", "b.i2p"]
44
45 def test_space_separated(self):
46 result = GenericClientTask._parse_destinations("a.i2p b.i2p")
47 assert result == ["a.i2p", "b.i2p"]
48
49 def test_mixed(self):
50 result = GenericClientTask._parse_destinations("a.i2p, b.i2p c.i2p")
51 assert result == ["a.i2p", "b.i2p", "c.i2p"]
52
53 def test_empty(self):
54 result = GenericClientTask._parse_destinations("")
55 assert result == []
56
57
58class TestPickDestination:
59 def test_single_always_picked(self):
60 task = _make_task(target_destination="only.i2p")
61 assert task._pick_destination() == "only.i2p"
62
63 def test_multiple_picks_from_list(self):
64 task = _make_task(target_destination="a.i2p,b.i2p,c.i2p")
65 for _ in range(20):
66 assert task._pick_destination() in ("a.i2p", "b.i2p", "c.i2p")
67
68
69class TestResolveDestination:
70 @pytest.mark.asyncio
71 async def test_b32_passthrough(self):
72 task = _make_task()
73 result = await task._resolve_destination("abc.b32.i2p")
74 assert result == "abc.b32.i2p"
75
76 @pytest.mark.asyncio
77 async def test_i2p_lookup(self):
78 task = _make_task()
79 task._session.lookup = AsyncMock(return_value="resolved-dest")
80 result = await task._resolve_destination("example.i2p")
81 assert result == "resolved-dest"
82
83 @pytest.mark.asyncio
84 async def test_i2p_lookup_fail(self):
85 task = _make_task()
86 task._session.lookup = AsyncMock(return_value=None)
87 result = await task._resolve_destination("unknown.i2p")
88 assert result is None
89
90 @pytest.mark.asyncio
91 async def test_base64_passthrough(self):
92 task = _make_task()
93 result = await task._resolve_destination("AAAA...base64dest")
94 assert result == "AAAA...base64dest"
95
96
97class TestHandleClient:
98 @pytest.mark.asyncio
99 async def test_resolve_fail_closes(self):
100 task = _make_task(target_destination="unknown.i2p")
101 task._session.lookup = AsyncMock(return_value=None)
102 reader = AsyncMock()
103 writer = _mock_writer()
104 await task.handle_client(reader, writer)
105 writer.close.assert_called()
106
107 @pytest.mark.asyncio
108 async def test_connect_fail_closes(self):
109 task = _make_task(target_destination="abc.b32.i2p")
110 task._session.connect = AsyncMock(side_effect=ConnectionError("fail"))
111 reader = AsyncMock()
112 writer = _mock_writer()
113 await task.handle_client(reader, writer)
114 writer.close.assert_called()
115
116 @pytest.mark.asyncio
117 async def test_success_bridges(self):
118 task = _make_task(target_destination="abc.b32.i2p")
119 remote_reader = AsyncMock()
120 remote_writer = MagicMock()
121 remote_writer.close = MagicMock()
122 task._session.connect = AsyncMock(return_value=(remote_reader, remote_writer))
123 reader = AsyncMock()
124 writer = _mock_writer()
125
126 with patch("i2p_apps.i2ptunnel.client_task.bridge", new_callable=AsyncMock) as mock_bridge:
127 await task.handle_client(reader, writer)
128 mock_bridge.assert_called_once()