"""Tests for the managed proxy pool.""" import json import time import pytest from s5p.config import ChainHop, PoolSourceConfig, ProxyPoolConfig from s5p.pool import ProxyEntry, ProxyPool class TestProxyEntry: """Test ProxyEntry defaults.""" def test_defaults(self): hop = ChainHop(proto="socks5", host="1.2.3.4", port=1080) entry = ProxyEntry(hop=hop) assert entry.alive is False assert entry.fails == 0 assert entry.tests == 0 class TestProxyPoolMerge: """Test proxy deduplication and merge.""" def test_merge_dedup(self): cfg = ProxyPoolConfig(sources=[]) pool = ProxyPool(cfg, [], timeout=10.0) proxies = [ ChainHop(proto="socks5", host="1.2.3.4", port=1080), ChainHop(proto="socks5", host="1.2.3.4", port=1080), ChainHop(proto="socks5", host="5.6.7.8", port=1080), ] pool._merge(proxies) assert pool.count == 2 def test_merge_updates_existing(self): cfg = ProxyPoolConfig(sources=[]) pool = ProxyPool(cfg, [], timeout=10.0) hop = ChainHop(proto="socks5", host="1.2.3.4", port=1080) pool._merge([hop]) first_seen = pool._proxies["socks5://1.2.3.4:1080"].last_seen # merge again -- last_seen should update time.sleep(0.01) pool._merge([hop]) assert pool._proxies["socks5://1.2.3.4:1080"].last_seen >= first_seen assert pool.count == 1 class TestProxyPoolGet: """Test proxy selection.""" def test_get_empty(self): import asyncio cfg = ProxyPoolConfig(sources=[]) pool = ProxyPool(cfg, [], timeout=10.0) result = asyncio.run(pool.get()) assert result is None def test_get_returns_alive(self): import asyncio cfg = ProxyPoolConfig(sources=[]) pool = ProxyPool(cfg, [], timeout=10.0) hop = ChainHop(proto="socks5", host="1.2.3.4", port=1080) pool._proxies["socks5://1.2.3.4:1080"] = ProxyEntry(hop=hop, alive=True) pool._rebuild_alive() result = asyncio.run(pool.get()) assert result is not None assert result.host == "1.2.3.4" class TestProxyPoolFetchFile: """Test file source parsing.""" def test_parse_file(self, tmp_path): proxy_file = tmp_path / "proxies.txt" proxy_file.write_text( "# comment\n" "socks5://1.2.3.4:1080\n" "socks5://user:pass@5.6.7.8:1080\n" "http://proxy.example.com:8080\n" "\n" " # another comment\n" ) cfg = ProxyPoolConfig(sources=[]) pool = ProxyPool(cfg, [], timeout=10.0) src = PoolSourceConfig(file=str(proxy_file)) result = pool._fetch_file_sync(src) assert len(result) == 3 assert result[0].proto == "socks5" assert result[0].host == "1.2.3.4" assert result[1].username == "user" assert result[1].password == "pass" assert result[2].proto == "http" def test_parse_file_with_proto_filter(self, tmp_path): proxy_file = tmp_path / "proxies.txt" proxy_file.write_text( "socks5://1.2.3.4:1080\n" "http://proxy.example.com:8080\n" ) cfg = ProxyPoolConfig(sources=[]) pool = ProxyPool(cfg, [], timeout=10.0) src = PoolSourceConfig(file=str(proxy_file), proto="socks5") result = pool._fetch_file_sync(src) assert len(result) == 1 assert result[0].proto == "socks5" def test_missing_file(self, tmp_path): cfg = ProxyPoolConfig(sources=[]) pool = ProxyPool(cfg, [], timeout=10.0) src = PoolSourceConfig(file=str(tmp_path / "nonexistent.txt")) result = pool._fetch_file_sync(src) assert result == [] class TestProxyPoolPersistence: """Test state save/load.""" def test_save_and_load(self, tmp_path): state_file = str(tmp_path / "pool.json") cfg = ProxyPoolConfig(sources=[], state_file=state_file) pool = ProxyPool(cfg, [], timeout=10.0) hop = ChainHop(proto="socks5", host="1.2.3.4", port=1080) pool._proxies["socks5://1.2.3.4:1080"] = ProxyEntry( hop=hop, alive=True, fails=0, tests=5, last_ok=1000.0, ) pool._rebuild_alive() pool._save_state() # load into a fresh pool pool2 = ProxyPool(cfg, [], timeout=10.0) pool2._load_state() assert pool2.count == 1 assert pool2.alive_count == 1 entry = pool2._proxies["socks5://1.2.3.4:1080"] assert entry.hop.host == "1.2.3.4" assert entry.tests == 5 assert entry.alive is True def test_corrupt_state(self, tmp_path): state_file = tmp_path / "pool.json" state_file.write_text("{invalid json") cfg = ProxyPoolConfig(sources=[], state_file=str(state_file)) pool = ProxyPool(cfg, [], timeout=10.0) pool._load_state() assert pool.count == 0 def test_missing_state(self, tmp_path): cfg = ProxyPoolConfig(sources=[], state_file=str(tmp_path / "missing.json")) pool = ProxyPool(cfg, [], timeout=10.0) pool._load_state() # should not raise assert pool.count == 0 def test_state_with_auth(self, tmp_path): state_file = str(tmp_path / "pool.json") cfg = ProxyPoolConfig(sources=[], state_file=state_file) pool = ProxyPool(cfg, [], timeout=10.0) hop = ChainHop( proto="socks5", host="1.2.3.4", port=1080, username="user", password="pass", ) pool._proxies["socks5://1.2.3.4:1080"] = ProxyEntry(hop=hop, alive=True) pool._save_state() pool2 = ProxyPool(cfg, [], timeout=10.0) pool2._load_state() entry = pool2._proxies["socks5://1.2.3.4:1080"] assert entry.hop.username == "user" assert entry.hop.password == "pass"