"""Click-path allowlist tests for the Playwright browser (fake only). Covers the continuation of the navigate-only allowlist: click / click_ref get the SAME check before the click when the target anchor href is resolvable, and all three click paths get a post-click URL check. click_at has no pre-click gate by design (coordinates identify no element). No real Chromium, no network, no real credentials -- values here are test placeholders. """ import os import sys import tempfile import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from playwright_browser import Browser, BrowserError # noqa: E402 from playwright_browser import _check_navigated_url # noqa: E402 class _FakeLocator: def __init__(self, page, href): self._page = page self._href = href @property def first(self): return self def click(self, timeout=None): self._page.calls.append(("locator_click",)) if self._page.click_landing is not None: self._page.url = self._page.click_landing def get_attribute(self, name): if name == "href": return self._href return None def inner_text(self, timeout=None): return "text" class _FakeMouse: def __init__(self, page): self._page = page def click(self, x, y): self._page.calls.append(("mouse_click", x, y)) if self._page.click_landing is not None: self._page.url = self._page.click_landing def move(self, x, y): self._page.calls.append(("mouse_move", x, y)) class _FakePage: def __init__(self): self.url = "https://example.com/" self.calls = [] self.href_map = {} self.click_landing = None self.mouse = _FakeMouse(self) def title(self): return "Example" def goto(self, url, timeout=None, wait_until=None): self.calls.append(("goto", url)) self.url = url def locator(self, selector): self.calls.append(("locator", selector)) return _FakeLocator(self, self.href_map.get(selector)) def click(self, selector, timeout=None): self.calls.append(("click", selector)) if self.click_landing is not None: self.url = self.click_landing def fill(self, selector, text, timeout=None): self.calls.append(("fill", selector, text)) def evaluate(self, expression): self.calls.append(("evaluate", expression)) return None def screenshot(self, path=None): with open(path, "wb") as fh: fh.write(b"\x89PNG\r\n\x1a\n" + b"\x00" * 10) def close(self): pass class _FakeHandle: def __init__(self, page): self._page = page def new_page(self, viewport=None): return self._page def close(self): pass class _FakePW: def __init__(self, page): self._page = page self.launch_count = 0 def start(self): return self @property def chromium(self): outer = self class _Chromium: def launch(self, **kwargs): outer.launch_count += 1 return _FakeHandle(outer._page) return _Chromium() def stop(self): pass class _EnvGuard: def setUp(self): self._old = os.environ.get("BROWSER_ALLOWED_HOSTS") def tearDown(self): if self._old is None: os.environ.pop("BROWSER_ALLOWED_HOSTS", None) else: os.environ["BROWSER_ALLOWED_HOSTS"] = self._old class ClickAllowlistTest(_EnvGuard, unittest.TestCase): def setUp(self): _EnvGuard.setUp(self) self._tmp = tempfile.TemporaryDirectory() self.page = _FakePage() self.factory = _FakePW(self.page) self.browser = Browser(cwd=self._tmp.name, proxy="http://proxy:8888", browser_factory=lambda: self.factory) def tearDown(self): self._tmp.cleanup() _EnvGuard.tearDown(self) def _start(self, url="https://example.com/"): return self.browser.navigate(url) def test_click_allowed_href_executes(self): os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com, .python.org" self._start() self.page.href_map["a.docs"] = "https://docs.python.org/3/" self.page.click_landing = "https://docs.python.org/3/" out = self.browser.click("a.docs") self.assertTrue(out["ok"]) self.assertIn(("click", "a.docs"), self.page.calls) self.assertEqual(out["url"], "https://docs.python.org/3/") def test_click_blocked_href_refused_before_click(self): os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com" self._start() self.page.href_map["a.evil"] = "https://evil.com/x" self.page.click_landing = "https://evil.com/x" with self.assertRaises(BrowserError) as ctx: self.browser.click("a.evil") self.assertIn("evil.com", str(ctx.exception)) self.assertNotIn(("click", "a.evil"), self.page.calls) self.assertEqual(self.browser._current_url, "https://example.com/") def test_click_suffix_trick_blocked(self): os.environ["BROWSER_ALLOWED_HOSTS"] = ".python.org" self._start("https://python.org/") for bad in ("https://notpython.org/", "https://python.org.evil.com/"): self.page.href_map["a.bad"] = bad with self.assertRaises(BrowserError): self.browser.click("a.bad") self.assertNotIn(("click", "a.bad"), self.page.calls) self.page.calls.clear() def test_click_plain_entry_is_exact_only(self): # Same semantics as navigate: a plain entry does NOT # match subdomains (that is what leading-dot entries # are for). Documented, not a bug: the activation # candidate therefore uses dot forms. os.environ["BROWSER_ALLOWED_HOSTS"] = "python.org" self._start("https://python.org/") self.page.href_map["a.docs"] = "https://docs.python.org/" with self.assertRaises(BrowserError): self.browser.click("a.docs") self.assertNotIn(("click", "a.docs"), self.page.calls) def test_click_port_and_case_insensitive(self): os.environ["BROWSER_ALLOWED_HOSTS"] = "Example.COM" self._start() self.page.href_map["a.p"] = "https://EXAMPLE.com:8443/p" self.page.click_landing = "https://EXAMPLE.com:8443/p" out = self.browser.click("a.p") self.assertTrue(out["ok"]) def test_click_file_href_allowed(self): os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com" self._start() self.page.href_map["a.file"] = "file:///tmp/page.html" self.page.click_landing = "file:///tmp/page.html" out = self.browser.click("a.file") self.assertTrue(out["ok"]) def test_click_localhost_needs_explicit_listing(self): os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com" self._start() self.page.href_map["a.local"] = "http://localhost:8000/" with self.assertRaises(BrowserError): self.browser.click("a.local") self.assertNotIn(("click", "a.local"), self.page.calls) os.environ["BROWSER_ALLOWED_HOSTS"] = "localhost" self.page.click_landing = "http://localhost:8000/" out = self.browser.click("a.local") self.assertTrue(out["ok"]) def test_click_relative_href_resolves_against_current(self): os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com" self._start("https://example.com/dir/") self.page.href_map["a.rel"] = "/other" self.page.click_landing = "https://example.com/other" out = self.browser.click("a.rel") self.assertTrue(out["ok"]) # Fragment-only hrefs never navigate to another host. self.page.href_map["a.frag"] = "#section" self.page.click_landing = None out = self.browser.click("a.frag") self.assertTrue(out["ok"]) def test_click_unknown_target_post_click_detection(self): # Button / JS handler: no href, so no pre-click gate. # The click DOES execute; a blocked landing is then # reported (detection, not prevention -- the proxy 403 # is the layer that stops the load in production). os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com" self._start() self.page.href_map["button.go"] = None self.page.click_landing = "https://evil.com/" with self.assertRaises(BrowserError) as ctx: self.browser.click("button.go") self.assertIn("evil.com", str(ctx.exception)) self.assertIn(("click", "button.go"), self.page.calls) # Same target landing on an allowed host is fine. self.page.click_landing = "https://example.com/ok" out = self.browser.click("button.go") self.assertTrue(out["ok"]) def test_click_unset_allowlist_unchanged(self): os.environ.pop("BROWSER_ALLOWED_HOSTS", None) self._start("https://anything.example/") self.page.href_map["a.evil"] = "https://evil.com/" self.page.click_landing = "https://evil.com/" out = self.browser.click("a.evil") self.assertTrue(out["ok"]) self.assertIn(("click", "a.evil"), self.page.calls) def test_click_ref_blocked_href_refused_before_click(self): os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com" self._start() self.browser._ref_paths = {"e0": "a.evil", "e1": "a.docs"} self.page.href_map["a.evil"] = "https://evil.com/" self.page.href_map["a.docs"] = "https://example.com/docs" self.page.click_landing = "https://evil.com/" with self.assertRaises(BrowserError): self.browser.click_ref("e0") self.assertNotIn(("locator_click",), self.page.calls) self.page.click_landing = "https://example.com/docs" out = self.browser.click_ref("e1") self.assertTrue(out["ok"]) self.assertIn(("locator_click",), self.page.calls) def test_click_at_post_click_only(self): # click_at has no pre-click gate by design; a blocked # landing raises after the click, an allowed one passes. os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com" self._start() self.page.click_landing = "https://evil.com/" with self.assertRaises(BrowserError): self.browser.click_at(100, 200) self.assertIn(("mouse_click", 100.0, 200.0), self.page.calls) self.page.click_landing = "https://example.com/" out = self.browser.click_at(100, 200) self.assertTrue(out["ok"]) def test_check_navigated_url_skips_non_page_schemes(self): os.environ["BROWSER_ALLOWED_HOSTS"] = "example.com" # about:blank / mailto: / javascript: never navigate # the page to a host, so they must not raise here. _check_navigated_url("about:blank") _check_navigated_url("mailto:someone@example.com") _check_navigated_url("javascript:void(0)") _check_navigated_url("file:///tmp/page.html") with self.assertRaises(BrowserError): _check_navigated_url("https://evil.com/")