Files
openswarm/backend/tests/test_browser_session_import.py
T

189 lines
8.7 KiB
Python

"""Borrowing the user's existing sign-in instead of interrupting them for a password.
This module reads the user's real browser, so the tests are mostly about what it must REFUSE to do.
Nothing here touches a real store or a keychain: the reader is stubbed at every call site.
"""
import os
import re
import pytest
from backend.apps.agents.browser import browser_session_import as si
from backend.apps.settings.models import AppSettings
P_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
P_MAIN_JS = os.path.join(P_REPO_ROOT, "electron", "main.js")
RECORDS = [{"name": "sid", "value": "opaque", "domain": ".x.com", "path": "/",
"secure": True, "httponly": True, "expires": 1900000000.0}]
def test_opt_in_is_off_by_default():
"""Reading someone's real browser is their call to make explicitly. If this ever defaults True,
an upgrade would silently start reading stores the user never agreed to expose."""
assert si.is_enabled(AppSettings()) is False
def test_opt_in_flips():
s = AppSettings()
s.browser_import_signins = True
assert si.is_enabled(s) is True
def test_google_properties_route_to_the_sso_scope():
"""A Gmail/YouTube session lives on the parent SSO domain, not the property's own host, and the
reader has a NAMED scope for it. Getting this wrong means either no session at all or a general
sweep of every google entry the user owns."""
for d in ("mail.google.com", "google.com", "docs.google.com", "youtube.com", "www.youtube.com"):
assert si.is_google_property(d), d
for d in ("reddit.com", "x.com", "notgoogle.com", "google.com.evil.net", ""):
assert not si.is_google_property(d), d
def test_domain_normalisation_matches_the_handoff():
"""One definition of 'which site is this', shared with the login handoff, or the two can
disagree about which domain we just borrowed for."""
assert si.site_domain("https://www.reddit.com/submit?x=1") == "reddit.com"
assert si.site_domain("x.com") == "x.com"
assert si.site_domain("") == ""
@pytest.mark.asyncio
async def test_no_session_never_wakes_the_bridge(monkeypatch):
"""Nothing to import means nothing to send. Calling the renderer with an empty payload would
burn a round trip and log a bogus failure."""
called = []
monkeypatch.setattr(si, "read_site_records", lambda d: [])
monkeypatch.setattr(si.ws_manager, "send_browser_command",
lambda *a, **k: called.append(a) or {})
result = await si.import_signin("x.com", "b1")
assert result.outcome == "no_session"
assert result.ok is False
assert called == []
@pytest.mark.asyncio
async def test_empty_domain_reads_nothing(monkeypatch):
"""A blank URL must not turn into a wildcard read."""
monkeypatch.setattr(si, "read_site_records",
lambda d: pytest.fail("must not read for an empty domain"))
assert (await si.import_signin("", "b1")).outcome == "no_session"
@pytest.mark.asyncio
async def test_successful_import_reports_what_landed(monkeypatch):
async def fake_send(rid, action, browser_id, params, **kw):
assert action == "import_session"
assert params["domain"] == "x.com"
assert params["cookies"] == RECORDS
return {"ok": True, "set": 1, "total": 1}
monkeypatch.setattr(si, "read_site_records", lambda d: list(RECORDS))
monkeypatch.setattr(si.ws_manager, "send_browser_command", fake_send)
result = await si.import_signin("https://x.com/compose/post", "b1")
assert result.outcome == "imported"
assert result.ok is True
assert result.entries_applied == 1
assert result.domain == "x.com"
@pytest.mark.asyncio
async def test_bridge_error_is_a_result_not_an_exception(monkeypatch):
"""Every failure has to degrade into something the caller can fall back from, because the
fallback (ask the user to sign in) is the behaviour that existed before this did."""
async def fake_send(*a, **k):
return {"error": "No dashboard is connected."}
monkeypatch.setattr(si, "read_site_records", lambda d: list(RECORDS))
monkeypatch.setattr(si.ws_manager, "send_browser_command", fake_send)
result = await si.import_signin("x.com", "b1")
assert result.outcome == "bridge_failed"
assert result.ok is False
@pytest.mark.asyncio
async def test_applied_nothing_is_not_success(monkeypatch):
"""The bridge answering 'ok' while applying zero entries must NOT read as signed in, or the run
skips the pause and then fails on a page it still cannot use."""
async def fake_send(*a, **k):
return {"ok": True, "set": 0, "total": 4}
monkeypatch.setattr(si, "read_site_records", lambda d: list(RECORDS))
monkeypatch.setattr(si.ws_manager, "send_browser_command", fake_send)
assert (await si.import_signin("x.com", "b1")).ok is False
def test_expiry_is_translated_out_of_chromium_time(monkeypatch):
"""Chromium counts microseconds from 1601; Electron wants unix seconds. Get this wrong and every
borrowed entry is either already expired or session-scoped, so the sign-in dies on the next quit
and the user quietly stops believing the feature works."""
monkeypatch.setattr(si.browser_cookies, "read_provider_cookie_records",
lambda d: [{"name": "sid", "value": "opaque", "expires_utc": 13400000000000000},
{"name": "tmp", "value": "opaque", "expires_utc": 0}])
out = si.read_site_records("x.com")
assert out[0]["expires"] == pytest.approx(1755526400.0)
assert out[1]["expires"] == 0.0, "a session entry must stay session-scoped, not become 1601"
def test_google_reads_go_through_the_named_sso_scope(monkeypatch):
"""A Gmail borrow must use the reader's named SSO set, never a general sweep of the user's
google entries."""
monkeypatch.setattr(si.browser_cookies, "read_google_session_records",
lambda: [{"name": "SID", "value": "opaque", "expires_utc": 0}])
monkeypatch.setattr(si.browser_cookies, "read_provider_cookie_records",
lambda d: pytest.fail("google must not go through the generic read"))
assert [r["name"] for r in si.read_site_records("mail.google.com")] == ["SID"]
def test_unreadable_browser_degrades_instead_of_crashing(monkeypatch):
"""A locked keychain, a v20 app-bound store, a browser that isn't installed: all of it is a
fallback, never an exception that kills the run."""
def boom(d):
raise RuntimeError("read denied")
monkeypatch.setattr(si.browser_cookies, "read_provider_cookie_records", boom)
assert si.read_site_records("x.com") == []
monkeypatch.setattr(si.browser_cookies, "p_best_store", boom)
assert si.has_importable_session("x.com") is False
@pytest.mark.asyncio
async def test_a_broken_borrow_can_never_break_the_run(monkeypatch):
"""The class this seals: borrowing is a convenience bolted onto the critical path, so ANY
failure inside it must cost at most the pause we were going to show anyway. Caught for real by
the suite, where a loose settings double made the helper raise and killed the whole browser run
before it could even reach the sign-in prompt."""
from backend.apps.agents.browser import browser_agent
def boom(*a, **k):
raise TypeError("settings double is not the real thing")
monkeypatch.setattr(browser_agent.browser_session_import, "is_enabled", boom)
assert await browser_agent.try_borrow_signin("acme.example", "b1", "", "") is False
def test_agent_checks_the_opt_in_before_reading_anything():
"""INVARIANT: the borrow helper must consult the setting FIRST. Pinned by source because the
ordering is the whole consent story, and an innocent-looking reorder would start reading the
user's browser before asking whether they wanted that."""
import inspect
from backend.apps.agents.browser import browser_agent
src = inspect.getsource(browser_agent.try_borrow_signin)
gate = src.index("is_enabled")
assert gate < src.index("has_importable_session"), "opt-in must be checked before probing"
assert gate < src.index("import_signin"), "opt-in must be checked before importing"
def test_partition_write_confines_entries_to_the_requested_domain():
"""INVARIANT on the Electron side: importing one site must never plant another site's session
in the partition. Pinned by source since main.js needs a live Electron to execute."""
with open(P_MAIN_JS, encoding="utf-8") as fh:
src = fh.read()
body = src[src.index("async function writePartitionCookies"):]
body = body[:body.index("ipcMain.handle('set-partition-cookies'")]
assert re.search(r"if \(host !== d && !host\.endsWith\(`\.\$\{d\}`\)\) continue;", body), \
"the per-entry domain confinement guard is gone from writePartitionCookies"