import io import json import unittest import os from dataclasses import replace from pathlib import Path from unittest.mock import Mock, patch from browser_worker import ( BrowserWorker, BrowserRuntimeWorker, ElementMetadata, WorkerError, BrowserOperationError, RealBrowserBackend, MAX_LINE_BYTES, MAX_READ_TEXT, process_request, run_stream, validate_cdp_url, main, ) class StartupDiagnosticsTests(unittest.TestCase): def test_missing_approved_tab_reports_origins_without_private_url_components(self): backend = RealBrowserBackend.__new__(RealBrowserBackend) backend.allowed_origins = {"https://fixture.test"} backend._helpers = Mock() backend._helpers.list_tabs.return_value = [ {"url": "https://login.example.test/private-path?token=private-token#private-fragment"}, {"url": "chrome://welcome"}, {"url": "https://user:private-password@login.example.test/"}, ] with self.assertRaises(WorkerError) as caught: backend._attach_approved_tab() message = str(caught.exception) self.assertIn("found 0", message) self.assertIn('open web origins: ["https://login.example.test"]', message) self.assertNotIn("private-", message) backend._helpers.switch_tab.assert_not_called() def test_startup_errors_use_safe_ipc_and_remain_closable(self): for error, expected in [ (WorkerError("expected exactly one existing approved-origin browser tab; found 0"), "found 0"), (RuntimeError("private-startup-value"), "browser worker initialization failed"), ]: with self.subTest(error=type(error).__name__): reader = io.BytesIO(b'{"id":1,"op":"observe"}\n{"id":2,"op":"close"}\n') writer = io.BytesIO() with patch("browser_worker.RealBrowserBackend", side_effect=error), \ patch("browser_worker.signal.signal"), \ patch("browser_worker.run_stream", side_effect=lambda worker: run_stream(worker, reader, writer)): self.assertEqual(main(["--cdp-url", "http://127.0.0.1:9222", "--allowed-origins", '["http://fixture.test"]']), 0) messages = [json.loads(line) for line in writer.getvalue().splitlines()] self.assertEqual(messages[0]["id"], 1) self.assertFalse(messages[0]["ok"]) self.assertIn(expected, messages[0]["error"]) self.assertTrue(messages[1]["ok"]) self.assertNotIn(b"private-startup-value", writer.getvalue()) class FakeBackend: def __init__(self): self.url = "http://fixture.test/form" self.target = "tab-1" self.frame = "frame-1" self.loader = "loader-1" self.elements = { 1: ElementMetadata(1, "input", "text", "textbox", "Email", True, False), 2: ElementMetadata(2, "input", "password", "textbox", "Password", True, False), 3: ElementMetadata(3, "button", "", "button", "Submit", True, False), 4: ElementMetadata(4, "input", "text", "textbox", "Disabled", True, True), } self.values = {1: ""} self.nodes = [ {"backendDOMNodeId": 1, "role": {"value": "textbox"}, "name": {"value": "Email"}}, {"backendDOMNodeId": 3, "role": {"value": "button"}, "name": {"value": "Submit"}}, {"backendDOMNodeId": 4, "role": {"value": "textbox"}, "name": {"value": "Disabled"}}, # Password controls are supported; their values remain private. {"backendDOMNodeId": 2, "role": {"value": "textbox"}, "name": {"value": "Password"}}, ] self.clicks = 0 def current_tab(self): return {"targetId": self.target, "url": self.url, "title": "Fixture"} def document_identity(self): return self.frame, self.loader def accessibility_nodes(self): return self.nodes def metadata(self, backend_id): return self.elements[backend_id] def click(self, backend_id): self.clicks += 1 if backend_id == 3: self.url = "http://fixture.test/done" def fill(self, backend_id, text): self.values[backend_id] = text def value_matches(self, backend_id, text): return self.values.get(backend_id) == text def visible_text(self, text): return text == "Finished" def close(self): pass class PopupFakeBackend(FakeBackend): def __init__(self): super().__init__() self.status = "Scenario fixture ready" def click(self, backend_id): super().click(backend_id) if backend_id == 3: self.status = "Popup opened" def visible_text(self, text): return text == self.status class RuntimeFakeBackend(FakeBackend): def __init__(self): super().__init__() self.tabs = [ {"targetId": "tab-a", "url": "http://fixture.test/form", "title": "Fixture"}, {"targetId": "tab-b", "url": "http://other.test/second", "title": "Second"}, ] self.target = "tab-a" self.text_content = "Fixture complete Привет" self.events = [] self._session_id = "fake-session" self._session_counter = 0 self.switches = [] self.closed_tabs = [] def list_tabs(self): return [dict(tab) for tab in self.tabs] def switch_tab(self, target_id): if not any(tab["targetId"] == target_id for tab in self.tabs): raise RuntimeError("missing target") self.target = target_id self._session_counter += 1 self._session_id = f"fake-session-{self._session_counter}" self.switches.append(target_id) def current_tab(self): return next(tab for tab in self.tabs if tab["targetId"] == self.target) def visible_text_content(self): return self.text_content def navigate(self, url): self.current_tab()["url"] = url self.loader = self.loader + "-nav" def reload(self): self.loader = self.loader + "-reload" def wait_ready(self, timeout): return True def close_tab(self, target_id): self.closed_tabs.append(target_id) self.tabs[:] = [tab for tab in self.tabs if tab["targetId"] != target_id] def drain_events(self): events, self.events = self.events, [] return events def screenshot_bytes(self): return b"\\x89PNG" + (b"x" * 32) class RuntimeTests(unittest.TestCase): def make_runtime(self, backend=None, origins="*"): backend = backend or RuntimeFakeBackend() return BrowserRuntimeWorker(backend, origins, session_id="runtime-test") def test_list_tabs_does_not_fail_or_select_ambiguous_pages(self): backend = RuntimeFakeBackend() runtime = self.make_runtime(backend) tabs = runtime.list_tabs() self.assertEqual(len(tabs), 2) self.assertFalse(any(tab["selected"] for tab in tabs)) with self.assertRaisesRegex(BrowserOperationError, "select an approved tab"): runtime.read({}) chosen = tabs[0]["tabId"] selected = runtime.select_tab({"tabId": chosen}) self.assertTrue(selected["tab"]["selected"]) self.assertEqual(backend.target, "tab-a" if tabs[0]["origin"] == "http://fixture.test" else "tab-b") def test_read_is_bounded_unicode_and_has_no_form_or_hidden_values(self): backend = RuntimeFakeBackend() backend.tabs = [backend.tabs[0]] backend.text_content = "Visible Привет\n" + ("é" * (MAX_READ_TEXT + 20)) runtime = self.make_runtime(backend) result = runtime.read({}) self.assertTrue(result["visibleTextTruncated"]) self.assertEqual(len(result["visibleText"]), MAX_READ_TEXT) self.assertIn("Привет", result["visibleText"]) self.assertNotIn("password-value", result["visibleText"]) self.assertNotIn("