"""Tests for HTTPClientTask — header filtering, outproxy, address helpers.""" from unittest.mock import AsyncMock, MagicMock import pytest from i2p_apps.i2ptunnel.config import TunnelDefinition, TunnelType from i2p_apps.i2ptunnel.http_client import HTTPClientTask, _STRIPPED_OUTBOUND, _I2P_USER_AGENT def _make_task(proxy_list=None): config = TunnelDefinition( name="test-http", type=TunnelType.HTTPCLIENT, listen_port=0, interface="127.0.0.1", proxy_list=proxy_list or [], ) session = MagicMock() session.lookup = AsyncMock(return_value=None) session.connect = AsyncMock() return HTTPClientTask(config, session) class TestHeaderFiltering: def test_strips_privacy_headers(self): task = _make_task() headers = { "Referer": "http://example.com", "Via": "proxy", "X-Forwarded-For": "1.2.3.4", "From": "user@example.com", "Host": "site.i2p", "Accept": "text/html", } filtered = task._filter_outbound_headers(headers, is_i2p=True) assert "Referer" not in filtered assert "Via" not in filtered assert "X-Forwarded-For" not in filtered assert "From" not in filtered assert "Host" in filtered assert "Accept" in filtered def test_replaces_user_agent_for_i2p(self): task = _make_task() headers = {"User-Agent": "Mozilla/5.0"} filtered = task._filter_outbound_headers(headers, is_i2p=True) assert filtered["User-Agent"] == _I2P_USER_AGENT def test_keeps_user_agent_for_clearnet(self): task = _make_task() headers = {"User-Agent": "Mozilla/5.0"} filtered = task._filter_outbound_headers(headers, is_i2p=False) assert filtered["User-Agent"] == "Mozilla/5.0" def test_proxy_headers_stripped(self): task = _make_task() headers = { "Proxy-Connection": "keep-alive", "Proxy-Authorization": "Basic xxx", } filtered = task._filter_outbound_headers(headers, is_i2p=True) assert len(filtered) == 0 class TestAddressHelpers: def test_extract_address_helper(self): task = _make_task() host, helper = task._extract_address_helper( "http://site.i2p/page?i2paddresshelper=AAAA" ) assert host == "site.i2p" assert helper == "AAAA" def test_no_address_helper(self): task = _make_task() host, helper = task._extract_address_helper("http://site.i2p/page") assert host == "site.i2p" assert helper is None def test_cache_and_retrieve(self): task = _make_task() task._cache_address_helper("site.i2p", "DEST_BASE64") assert task._get_cached_helper("site.i2p") == "DEST_BASE64" assert task._get_cached_helper("other.i2p") is None class TestOutproxy: def test_has_outproxy(self): task = _make_task(proxy_list=["proxy.i2p"]) assert task._has_outproxy() def test_no_outproxy(self): task = _make_task(proxy_list=[]) assert not task._has_outproxy() def test_pick_outproxy(self): task = _make_task(proxy_list=["proxy1.i2p", "proxy2.i2p"]) choice = task._pick_outproxy() assert choice in ("proxy1.i2p", "proxy2.i2p") def test_pick_outproxy_avoids_failed(self): task = _make_task(proxy_list=["proxy1.i2p", "proxy2.i2p"]) task._failed_outproxies.add("proxy1.i2p") choice = task._pick_outproxy() assert choice == "proxy2.i2p" def test_pick_outproxy_resets_when_all_failed(self): task = _make_task(proxy_list=["proxy1.i2p"]) task._failed_outproxies.add("proxy1.i2p") choice = task._pick_outproxy() assert choice == "proxy1.i2p" assert len(task._failed_outproxies) == 0 def test_pick_outproxy_empty_list(self): task = _make_task(proxy_list=[]) assert task._pick_outproxy() == "" class TestRequestClassification: def test_is_i2p_request(self): assert HTTPClientTask._is_i2p_request("site.i2p") assert HTTPClientTask._is_i2p_request("a.b32.i2p") assert not HTTPClientTask._is_i2p_request("example.com") def test_is_localhost(self): assert HTTPClientTask._is_localhost("127.0.0.1") assert HTTPClientTask._is_localhost("localhost") assert HTTPClientTask._is_localhost("::1") assert HTTPClientTask._is_localhost("0.0.0.0") assert not HTTPClientTask._is_localhost("example.com") class TestErrorPages: def test_error_page_contains_message(self): task = _make_task() page = task._error_page("dnf", 503) assert b"503" in page assert b"Destination Not Found" in page assert b"text/html" in page def test_error_page_unknown_type(self): task = _make_task() page = task._error_page("unknown_type", 500) assert b"500" in page assert b"Error" in page class TestRequestRebuilding: def test_rebuild_get(self): result = HTTPClientTask._rebuild_request( "GET", "/path", {"Host": "site.i2p"}, b"" ) assert b"GET /path HTTP/1.1" in result assert b"Host: site.i2p" in result def test_rebuild_post_with_body(self): body = b"key=value" result = HTTPClientTask._rebuild_request( "POST", "/submit", {"Content-Length": "9"}, body ) assert b"POST /submit HTTP/1.1" in result assert result.endswith(body) class TestResolve: @pytest.mark.asyncio async def test_b32_passthrough(self): task = _make_task() result = await task._resolve("abcdef.b32.i2p") assert result == "abcdef.b32.i2p" @pytest.mark.asyncio async def test_cached_helper(self): task = _make_task() task._cache_address_helper("site.i2p", "CACHED_DEST") result = await task._resolve("site.i2p") assert result == "CACHED_DEST" @pytest.mark.asyncio async def test_lookup_fallback(self): task = _make_task() task._session.lookup = AsyncMock(return_value="LOOKED_UP") result = await task._resolve("site.i2p") assert result == "LOOKED_UP"