Files
camoufox/pythonlib/tests/test_proxy_geo.py
T
Jake WriterandClaude Opus 5.5 6a22248c0b fix(python): NewContext looks up a proxy's exit IP through the right URL, or fails
NewContext derives the context's WebRTC IP and timezone from the proxy's exit
IP. That lookup had two defects, and both left the context showing the
host's values while its traffic went through the proxy:

- It built its own proxy URL with urlparse, which reads a scheme-less server
  such as "1.2.3.4:8080" (a form Playwright accepts) as scheme "1.2.3.4" with
  no host. urllib could not use a SOCKS proxy at all.
- Any failure was swallowed, and the context opened without the values.

The URL is now built with Proxy.as_string(), which the geoip launch path
already uses (scheme-less means http). The lookup goes through requests,
which handles SOCKS, and a failed lookup raises InvalidIP, naming the two
options that skip it. The tests cover scheme-less, http and socks5 servers
with credentials, both failure modes, and the case where no lookup is needed,
for NewContext and AsyncNewContext.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:22:28 -06:00

95 lines
3.3 KiB
Python

"""NewContext derives the WebRTC IP and timezone from the proxy's exit IP.
Two defects, both of which left a context with the host's WebRTC IP and
timezone while its traffic went through the proxy:
- The lookup built its own proxy URL with urlparse, which reads a scheme-less
server such as "1.2.3.4:8080" (a form Playwright accepts) as scheme "1.2.3.4"
and drops the host.
- A failed lookup was swallowed, and the context launched without the values.
"""
import asyncio
from unittest import mock
import pytest
from camoufox import async_api, ip, sync_api
from camoufox.exceptions import InvalidIP
class _Response:
def __init__(self, payload):
self._payload = payload
def raise_for_status(self):
pass
def json(self):
return self._payload
EXIT = {"status": "success", "query": "203.0.113.7", "timezone": "Europe/Paris"}
def _sync_browser():
browser = mock.MagicMock()
return browser
def _async_browser():
context = mock.MagicMock()
context.add_init_script = mock.AsyncMock()
browser = mock.MagicMock()
browser.new_context = mock.AsyncMock(return_value=context)
return browser
def _new_context(api, proxy, **kwargs):
if api == "sync":
browser = _sync_browser()
sync_api.NewContext(browser, os="linux", proxy=proxy, **kwargs)
return browser.new_context.call_args.kwargs, browser.new_context.return_value
browser = _async_browser()
asyncio.run(async_api.AsyncNewContext(browser, os="linux", proxy=proxy, **kwargs))
return browser.new_context.call_args.kwargs, browser.new_context.return_value
@pytest.mark.parametrize("api", ["sync", "async"])
@pytest.mark.parametrize(
"server, expected",
[
("1.2.3.4:8080", "http://u:p@1.2.3.4:8080"),
("proxy.example.com:8080", "http://u:p@proxy.example.com:8080"),
("http://proxy.example.com:8080", "http://u:p@proxy.example.com:8080"),
("socks5://proxy.example.com:1080", "socks5://u:p@proxy.example.com:1080"),
],
)
def test_lookup_goes_through_the_proxy_with_its_credentials(api, server, expected):
with mock.patch.object(ip.requests, "get", return_value=_Response(EXIT)) as get:
options, context = _new_context(api, {"server": server, "username": "u", "password": "p"})
assert get.call_args.kwargs["proxies"] == {"http": expected, "https": expected}
assert options["timezone_id"] == "Europe/Paris"
assert "203.0.113.7" in context.add_init_script.call_args.args[0]
@pytest.mark.parametrize("api", ["sync", "async"])
@pytest.mark.parametrize(
"failure",
[
ip.requests.ConnectionError("proxy refused"),
_Response({"status": "fail", "message": "private range"}),
],
)
def test_a_failed_lookup_raises_instead_of_launching_without_the_values(api, failure):
get = mock.Mock(side_effect=failure) if isinstance(failure, Exception) else mock.Mock(return_value=failure)
with mock.patch.object(ip.requests, "get", get), pytest.raises(InvalidIP, match="webrtc_ip"):
_new_context(api, {"server": "1.2.3.4:8080"})
@pytest.mark.parametrize("api", ["sync", "async"])
def test_no_lookup_when_both_values_are_given(api):
with mock.patch.object(ip.requests, "get") as get:
_new_context(api, {"server": "1.2.3.4:8080"}, webrtc_ip="198.51.100.1", timezone_id="UTC")
get.assert_not_called()