Adds VPN-aware leak handling, chain testing UX improvements, hardened Firefox launch/profile management, privacy/device hardening modules, and tray/status upgrades so the app is production-ready as the new baseline. Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
6.3 KiB
Python
182 lines
6.3 KiB
Python
"""Automated checks for Proxy God core logic (no GUI). Run: python -m unittest discover -s tests -v"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import unittest
|
|
|
|
from proxy_chain_manager.leak_detect import is_chain_leak, is_same_subnet
|
|
|
|
from proxy_chain_manager.config import (
|
|
Settings,
|
|
merge_proxy_credentials,
|
|
normalize_proxy_url,
|
|
redact_proxy_url,
|
|
sanitize_settings,
|
|
split_proxy_for_edit,
|
|
)
|
|
from proxy_chain_manager.fetcher import fetch_proxy_json, normalize_entries
|
|
from proxy_chain_manager.validator import (
|
|
check_chain_exit_ip,
|
|
get_direct_ip,
|
|
validate_proxies,
|
|
)
|
|
|
|
|
|
class TestSameNetwork(unittest.TestCase):
|
|
def test_exact_match(self) -> None:
|
|
self.assertTrue(is_same_subnet("1.2.3.4", "1.2.3.4"))
|
|
|
|
def test_same_slash16(self) -> None:
|
|
self.assertTrue(is_same_subnet("185.220.101.5", "185.220.200.9"))
|
|
|
|
def test_different_slash16(self) -> None:
|
|
self.assertFalse(is_same_subnet("185.220.101.5", "104.28.3.99"))
|
|
|
|
def test_none_safe(self) -> None:
|
|
self.assertFalse(is_same_subnet(None, "1.2.3.4"))
|
|
self.assertFalse(is_same_subnet("1.2.3.4", None))
|
|
|
|
def test_non_ipv4_safe(self) -> None:
|
|
self.assertFalse(is_same_subnet("not-an-ip", "1.2.3.4"))
|
|
|
|
|
|
class TestLeakDetect(unittest.TestCase):
|
|
def test_strict_no_vpn_same_ip(self) -> None:
|
|
self.assertTrue(is_chain_leak("1.2.3.4", "1.2.3.4", vpn_active=False))
|
|
|
|
def test_strict_no_vpn_different_ip(self) -> None:
|
|
self.assertFalse(is_chain_leak("5.6.7.8", "1.2.3.4", vpn_active=False))
|
|
|
|
def test_vpn_subnet_leak(self) -> None:
|
|
self.assertTrue(is_chain_leak("185.220.101.5", "185.220.200.9", vpn_active=True))
|
|
|
|
def test_vpn_different_subnet_ok(self) -> None:
|
|
self.assertFalse(is_chain_leak("104.28.3.99", "185.220.101.5", vpn_active=True))
|
|
|
|
|
|
class TestConfig(unittest.TestCase):
|
|
def test_normalize_proxy_url(self) -> None:
|
|
self.assertEqual(normalize_proxy_url(""), "")
|
|
self.assertEqual(normalize_proxy_url("1.2.3.4:8080"), "http://1.2.3.4:8080")
|
|
self.assertEqual(normalize_proxy_url("socks5://x:1"), "socks5://x:1")
|
|
|
|
def test_merge_split_proxy_auth(self) -> None:
|
|
merged = merge_proxy_credentials("http://1.2.3.4:8080", "user", "p:ass")
|
|
self.assertIn("user", merged)
|
|
self.assertIn("p%3Aass", merged)
|
|
base, u, pw = split_proxy_for_edit(merged)
|
|
self.assertEqual(base, "http://1.2.3.4:8080")
|
|
self.assertEqual(u, "user")
|
|
self.assertEqual(pw, "p:ass")
|
|
|
|
def test_merge_preserves_embedded_auth_when_fields_empty(self) -> None:
|
|
raw = normalize_proxy_url("http://x:y@9.9.9.9:12")
|
|
self.assertEqual(merge_proxy_credentials(raw, "", ""), raw)
|
|
|
|
def test_redact_proxy_url(self) -> None:
|
|
r = redact_proxy_url("http://alice:secret@proxy.example:8888")
|
|
self.assertIn("***:***@", r)
|
|
self.assertIn("proxy.example", r)
|
|
self.assertNotIn("secret", r)
|
|
self.assertNotIn("alice", r)
|
|
|
|
def test_sanitize_restores_empty_sources(self) -> None:
|
|
s = Settings()
|
|
s.sources = []
|
|
s2, changed = sanitize_settings(s)
|
|
self.assertTrue(changed)
|
|
self.assertGreater(len(s2.sources), 0)
|
|
|
|
def test_sanitize_clamps_extremes(self) -> None:
|
|
s = Settings(
|
|
chain_length=0,
|
|
health_check_seconds=999999,
|
|
full_refresh_seconds=10,
|
|
validation_timeout_seconds=900.0,
|
|
)
|
|
s, changed = sanitize_settings(s)
|
|
self.assertTrue(changed)
|
|
self.assertEqual(s.chain_length, 1)
|
|
self.assertEqual(s.health_check_seconds, 3600)
|
|
self.assertEqual(s.full_refresh_seconds, 60)
|
|
self.assertEqual(s.validation_timeout_seconds, 120.0)
|
|
|
|
|
|
class TestFetcher(unittest.TestCase):
|
|
def test_normalize_entries_empty(self) -> None:
|
|
self.assertEqual(normalize_entries([], False), [])
|
|
|
|
def test_fetch_smoke(self) -> None:
|
|
"""One real HTTP GET — may fail offline; then skip assertion."""
|
|
try:
|
|
rows = fetch_proxy_json(
|
|
"https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/protocols/http/data.json",
|
|
timeout=25.0,
|
|
)
|
|
except Exception:
|
|
self.skipTest("network unavailable")
|
|
self.assertIsInstance(rows, list)
|
|
if rows:
|
|
self.assertTrue(isinstance(rows[0], dict))
|
|
|
|
|
|
class TestValidator(unittest.TestCase):
|
|
def test_validate_empty_list_returns_fast(self) -> None:
|
|
async def run() -> list[str]:
|
|
return await validate_proxies(
|
|
[],
|
|
"https://api.ipify.org?format=json",
|
|
8,
|
|
5.0,
|
|
)
|
|
|
|
self.assertEqual(asyncio.run(run()), [])
|
|
|
|
def test_validate_dead_proxy_fast(self) -> None:
|
|
async def run() -> list[str]:
|
|
return await validate_proxies(
|
|
["http://127.0.0.1:1"],
|
|
"https://api.ipify.org?format=json",
|
|
1,
|
|
3.0,
|
|
)
|
|
|
|
self.assertEqual(asyncio.run(run()), [])
|
|
|
|
def test_validate_early_exit_target(self) -> None:
|
|
"""Once target is reached remaining tasks should be skipped (all dead here → no early exit but no crash)."""
|
|
async def run() -> list[str]:
|
|
return await validate_proxies(
|
|
["http://127.0.0.1:1"] * 10,
|
|
"https://api.ipify.org?format=json",
|
|
4,
|
|
2.0,
|
|
target=2, # target unreachable — should still return [] cleanly
|
|
)
|
|
|
|
result = asyncio.run(run())
|
|
self.assertEqual(result, [])
|
|
|
|
def test_get_direct_ip_network(self) -> None:
|
|
async def run() -> str | None:
|
|
return await get_direct_ip("https://api.ipify.org?format=json", 12.0)
|
|
|
|
ip = asyncio.run(run())
|
|
if ip is None:
|
|
self.skipTest("could not reach ipify (offline or blocked)")
|
|
self.assertGreater(len(ip), 4)
|
|
|
|
def test_check_chain_exit_ip_invalid_proxy_quick(self) -> None:
|
|
async def run() -> str | None:
|
|
return await check_chain_exit_ip(
|
|
"http://127.0.0.1:1",
|
|
"https://api.ipify.org?format=json",
|
|
8.0,
|
|
)
|
|
|
|
self.assertIsNone(asyncio.run(run()))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|