项目文件夹

文件
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

605 行
22 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
"""Unit tests for the layer-3 headless re-auth decision layer.
Covers :mod:`notebooklm._auth.headless_reauth`:
* the opt-in × profile-present × failure-class decision matrix,
* the three typed honest outcomes (UNAVAILABLE / FAILED / SUCCESS) and that
SUCCESS is never reported on a dead/redirected session,
* the env-var opt-in gate (``NOTEBOOKLM_HEADLESS_REAUTH=1``),
* the default-unchanged behavior (no opt-in + no profile → UNAVAILABLE, the
browser is never launched).
The browser drive is faked end-to-end via ``run_browser_capture`` so no real
Playwright / network is needed; ``playwright`` stays lazily imported.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import pytest
from notebooklm._auth import headless_reauth as hr
from notebooklm._auth.headless_reauth import (
HeadlessReauthResult,
HeadlessReauthStatus,
attempt_headless_reauth,
headless_reauth_env_enabled,
)
from notebooklm.exceptions import HeadlessLoginRequiredError
def _make_profile(tmp_path: Path) -> Path:
"""Create (idempotently) a non-empty browser-profile dir on disk."""
profile = tmp_path / "browser_profile"
profile.mkdir(exist_ok=True)
(profile / "Default").mkdir(exist_ok=True) # a populated profile dir
return profile
# ---------------------------------------------------------------------------
# env opt-in gate
# ---------------------------------------------------------------------------
def test_env_enabled_only_for_exact_one() -> None:
assert headless_reauth_env_enabled({"NOTEBOOKLM_HEADLESS_REAUTH": "1"}) is True
assert headless_reauth_env_enabled({"NOTEBOOKLM_HEADLESS_REAUTH": "0"}) is False
assert headless_reauth_env_enabled({"NOTEBOOKLM_HEADLESS_REAUTH": "true"}) is False
assert headless_reauth_env_enabled({}) is False
# ---------------------------------------------------------------------------
# Decision matrix: opt-in OFF → UNAVAILABLE, never launches a browser
# ---------------------------------------------------------------------------
def test_optin_off_is_unavailable_and_never_launches(tmp_path: Path, monkeypatch) -> None:
"""No opt-in + no env → UNAVAILABLE; the capture core is never reached.
This pins the locked design decision: L3 NEVER fires by default.
"""
profile = _make_profile(tmp_path)
def _boom(*_a, **_k): # pragma: no cover - must not be called
raise AssertionError("run_browser_capture must not be called when opt-in is off")
monkeypatch.setattr(hr, "run_browser_capture", _boom)
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=False,
browser_profile=profile,
env={},
)
assert result.status is HeadlessReauthStatus.UNAVAILABLE
assert result.succeeded is False
assert "not enabled" in result.reason
def test_optin_off_even_with_profile_is_unavailable(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setattr(hr, "run_browser_capture", lambda *a, **k: None)
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=False,
browser_profile=_make_profile(tmp_path),
env={"NOTEBOOKLM_HEADLESS_REAUTH": "0"},
)
assert result.status is HeadlessReauthStatus.UNAVAILABLE
# ---------------------------------------------------------------------------
# Decision matrix: opt-in ON but no reusable profile → UNAVAILABLE
# ---------------------------------------------------------------------------
def test_optin_on_no_profile_dir_is_unavailable(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setattr(hr, "run_browser_capture", lambda *a, **k: None)
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=True,
browser_profile=tmp_path / "does_not_exist",
env={},
)
assert result.status is HeadlessReauthStatus.UNAVAILABLE
assert "no reusable browser profile" in result.reason
def test_optin_on_empty_profile_dir_is_unavailable(tmp_path: Path, monkeypatch) -> None:
"""A freshly-mkdir'd but empty profile holds no Google session → decline."""
monkeypatch.setattr(hr, "run_browser_capture", lambda *a, **k: None)
empty = tmp_path / "browser_profile"
empty.mkdir()
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=True,
browser_profile=empty,
env={},
)
assert result.status is HeadlessReauthStatus.UNAVAILABLE
# ---------------------------------------------------------------------------
# Decision matrix: opt-in ON + profile present → drives the browser
# ---------------------------------------------------------------------------
def test_success_when_capture_succeeds(tmp_path: Path, monkeypatch) -> None:
"""Capture returns normally → SUCCESS, storage_path carried out."""
storage = tmp_path / "storage_state.json"
captured: dict[str, object] = {}
def _fake_capture(plan, io, *, headless, interactive):
captured["headless"] = headless
captured["interactive"] = interactive
captured["profile"] = plan.browser_profile
return None
profile = _make_profile(tmp_path)
monkeypatch.setattr(hr, "run_browser_capture", _fake_capture)
# Ensure the playwright-import probe passes by faking it present.
monkeypatch.setitem(__import__("sys").modules, "playwright", _DummyModule())
monkeypatch.setitem(__import__("sys").modules, "playwright.sync_api", _DummyModule())
result = attempt_headless_reauth(
storage_path=storage,
allow_headless=True,
browser_profile=profile,
env={},
)
assert result.status is HeadlessReauthStatus.SUCCESS
assert result.succeeded is True
assert result.storage_path == storage
# The headless arm must be driven non-interactively, headless.
assert captured == {
"headless": True,
"interactive": False,
"profile": profile,
}
def test_failed_when_profile_session_also_dead(tmp_path: Path, monkeypatch) -> None:
"""Headless landed on the Google login page → FAILED, NEVER success."""
def _redirected(plan, io, *, headless, interactive):
raise HeadlessLoginRequiredError("redirected to login")
monkeypatch.setattr(hr, "run_browser_capture", _redirected)
monkeypatch.setitem(__import__("sys").modules, "playwright", _DummyModule())
monkeypatch.setitem(__import__("sys").modules, "playwright.sync_api", _DummyModule())
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=True,
browser_profile=_make_profile(tmp_path),
env={},
)
assert result.status is HeadlessReauthStatus.FAILED
assert result.succeeded is False
assert result.storage_path is None
assert "expired" in result.reason
def test_failed_on_unexpected_capture_error(tmp_path: Path, monkeypatch) -> None:
"""An unexpected capture exception → FAILED (best-effort recovery)."""
def _boom(plan, io, *, headless, interactive):
raise RuntimeError("launch blew up")
monkeypatch.setattr(hr, "run_browser_capture", _boom)
monkeypatch.setitem(__import__("sys").modules, "playwright", _DummyModule())
monkeypatch.setitem(__import__("sys").modules, "playwright.sync_api", _DummyModule())
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=True,
browser_profile=_make_profile(tmp_path),
env={},
)
assert result.status is HeadlessReauthStatus.FAILED
# Error TYPE only — never a cookie value.
assert "RuntimeError" in result.reason
def test_env_optin_drives_browser_without_explicit_flag(tmp_path: Path, monkeypatch) -> None:
"""``NOTEBOOKLM_HEADLESS_REAUTH=1`` enables L3 even with allow_headless=False."""
monkeypatch.setattr(hr, "run_browser_capture", lambda *a, **k: None)
monkeypatch.setitem(__import__("sys").modules, "playwright", _DummyModule())
monkeypatch.setitem(__import__("sys").modules, "playwright.sync_api", _DummyModule())
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=False,
browser_profile=_make_profile(tmp_path),
env={"NOTEBOOKLM_HEADLESS_REAUTH": "1"},
)
assert result.status is HeadlessReauthStatus.SUCCESS
def test_unavailable_when_playwright_missing(tmp_path: Path, monkeypatch) -> None:
"""Opt-in + profile, but the ``browser`` extra is absent → UNAVAILABLE.
Distinct from FAILED: there is nothing to drive, not a dead session.
"""
import builtins
real_import = builtins.__import__
def _no_playwright(name, *args, **kwargs):
if name == "playwright.sync_api" or name == "playwright":
raise ImportError("No module named 'playwright'")
return real_import(name, *args, **kwargs)
def _must_not_run(*_a, **_k): # pragma: no cover
raise AssertionError("capture must not run when playwright is missing")
monkeypatch.setattr(hr, "run_browser_capture", _must_not_run)
monkeypatch.setattr(builtins, "__import__", _no_playwright)
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=True,
browser_profile=_make_profile(tmp_path),
env={},
)
assert result.status is HeadlessReauthStatus.UNAVAILABLE
assert "playwright" in result.reason
# ---------------------------------------------------------------------------
# HeadlessReauthResult convenience
# ---------------------------------------------------------------------------
def test_result_succeeded_property() -> None:
assert HeadlessReauthResult(HeadlessReauthStatus.SUCCESS, "ok").succeeded is True
assert HeadlessReauthResult(HeadlessReauthStatus.FAILED, "no").succeeded is False
assert HeadlessReauthResult(HeadlessReauthStatus.UNAVAILABLE, "no").succeeded is False
# ---------------------------------------------------------------------------
# Explicit-path coalescing: concurrent attempts → ONE browser per profile
# ---------------------------------------------------------------------------
def test_concurrent_explicit_attempts_coalesce_to_one_browser(tmp_path: Path, monkeypatch) -> None:
"""N concurrent ``attempt_headless_reauth`` calls drive ONE browser.
The explicit ``refresh_auth(allow_headless=True)`` entry bypasses the
mid-RPC coordinator's single-flight, so the per-storage-path drive lock +
freshness skip in ``attempt_headless_reauth`` is what prevents redundant
browsers. The leader writes the storage file (advancing its mtime); waiting
followers observe the fresh file and coalesce.
"""
import threading
import time
storage = tmp_path / "storage_state.json"
profile = _make_profile(tmp_path)
drives = {"count": 0}
barrier = threading.Barrier(6)
def _slow_capture(plan, io, *, headless, interactive):
drives["count"] += 1
time.sleep(0.05) # hold the lock so followers pile up behind it
# Simulate the real capture writing fresh storage (advances mtime).
plan.storage_path.write_text('{"cookies": [], "origins": []}', encoding="utf-8")
monkeypatch.setattr(hr, "run_browser_capture", _slow_capture)
monkeypatch.setitem(__import__("sys").modules, "playwright", _DummyModule())
monkeypatch.setitem(__import__("sys").modules, "playwright.sync_api", _DummyModule())
results: list[HeadlessReauthResult] = []
results_lock = threading.Lock()
def _worker() -> None:
barrier.wait()
res = attempt_headless_reauth(
storage_path=storage, allow_headless=True, browser_profile=profile, env={}
)
with results_lock:
results.append(res)
threads = [threading.Thread(target=_worker) for _ in range(6)]
for t in threads:
t.start()
for t in threads:
t.join()
# Exactly one real browser drive; all six callers report SUCCESS (the
# leader drove, the followers coalesced onto the fresh storage).
assert drives["count"] == 1
assert len(results) == 6
assert all(r.status is HeadlessReauthStatus.SUCCESS for r in results)
class _DummyModule:
"""Stand-in for the ``playwright`` / ``playwright.sync_api`` modules.
Only used to satisfy the function-local ``import playwright.sync_api``
availability probe in :func:`attempt_headless_reauth` without installing
the real extra; the actual capture is faked via ``run_browser_capture``.
"""
# ---------------------------------------------------------------------------
# Readiness probe (doctor diagnostics): credential-free, launches nothing
# ---------------------------------------------------------------------------
def test_readiness_ready_when_profile_present_and_playwright(tmp_path: Path, monkeypatch) -> None:
"""Profile present + playwright importable → available, ready detail."""
profile = _make_profile(tmp_path)
monkeypatch.setattr(hr, "_playwright_installed", lambda: True)
readiness = hr.headless_reauth_readiness(browser_profile=profile)
assert readiness.profile_present is True
assert readiness.playwright_installed is True
assert readiness.available is True
assert "ready" in readiness.detail
assert "NOTEBOOKLM_HEADLESS_REAUTH" in readiness.detail
def test_readiness_unavailable_without_profile(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setattr(hr, "_playwright_installed", lambda: True)
readiness = hr.headless_reauth_readiness(browser_profile=tmp_path / "nope")
assert readiness.profile_present is False
assert readiness.available is False
assert "no reusable browser profile" in readiness.detail
def test_readiness_unavailable_without_playwright(tmp_path: Path, monkeypatch) -> None:
profile = _make_profile(tmp_path)
monkeypatch.setattr(hr, "_playwright_installed", lambda: False)
readiness = hr.headless_reauth_readiness(browser_profile=profile)
assert readiness.profile_present is True
assert readiness.playwright_installed is False
assert readiness.available is False
assert "playwright not installed" in readiness.detail
def test_readiness_reports_both_missing(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setattr(hr, "_playwright_installed", lambda: False)
readiness = hr.headless_reauth_readiness(browser_profile=tmp_path / "nope")
assert readiness.available is False
assert "no reusable browser profile" in readiness.detail
assert "playwright not installed" in readiness.detail
def test_readiness_never_drives_a_browser(tmp_path: Path, monkeypatch) -> None:
"""The readiness probe must never launch the capture core."""
def _boom(*_a, **_k): # pragma: no cover - must not be called
raise AssertionError("headless_reauth_readiness must not drive a browser")
monkeypatch.setattr(hr, "run_browser_capture", _boom)
monkeypatch.setattr(hr, "_playwright_installed", lambda: True)
readiness = hr.headless_reauth_readiness(browser_profile=_make_profile(tmp_path))
assert readiness.available is True
def test_playwright_installed_true_with_extra() -> None:
"""The browser extra IS installed in the test env, so the probe is True."""
assert hr._playwright_installed() is True
# ---------------------------------------------------------------------------
# CDP attach arm (alternative credential source): resolution + routing
# ---------------------------------------------------------------------------
def test_resolve_cdp_url_explicit_arg_wins() -> None:
assert (
hr.resolve_cdp_url(
"http://127.0.0.1:9222", {"NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL": "http://x"}
)
== "http://127.0.0.1:9222"
)
def test_resolve_cdp_url_falls_back_to_env() -> None:
assert (
hr.resolve_cdp_url(None, {"NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL": "http://127.0.0.1:9222"})
== "http://127.0.0.1:9222"
)
def test_resolve_cdp_url_blank_is_unset() -> None:
assert hr.resolve_cdp_url(None, {"NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL": " "}) is None
assert hr.resolve_cdp_url("", {}) is None
assert hr.resolve_cdp_url(None, {}) is None
@pytest.mark.parametrize(
"url",
[
"http://127.0.0.1:9222",
"http://localhost:9222",
"http://[::1]:9222",
"http://127.5.6.7:9222", # anywhere in 127.0.0.0/8
"ws://127.0.0.1:9222/devtools/browser/abc",
"127.0.0.1:9222", # bare host:port (no scheme)
],
)
def test_resolve_cdp_url_allows_loopback(url: str) -> None:
"""Loopback endpoints are the only sanctioned LOCAL-ONLY targets."""
assert hr.resolve_cdp_url(url, {}) == url
@pytest.mark.parametrize(
"url",
[
"http://remote-host:9222",
"http://10.0.0.5:9222", # private LAN, NOT loopback
"http://192.168.1.10:9222",
"http://0.0.0.0:9222", # wildcard bind is not loopback
"http://example.com:9222",
"not a url",
],
)
def test_resolve_cdp_url_rejects_non_loopback(url: str, caplog) -> None:
"""Non-loopback (remote / LAN / wildcard / junk) endpoints are rejected.
This is the LOCAL-UNATTENDED-ONLY boundary: a remote CDP endpoint must
never reach ``connect_over_cdp``. The rejection must NOT log the endpoint.
"""
import logging
with caplog.at_level(logging.WARNING, logger="notebooklm.auth"):
assert hr.resolve_cdp_url(url, {}) is None
# The endpoint value must not appear in any log record.
assert url not in caplog.text
def test_cdp_path_skips_profile_gate_and_drives_cdp(tmp_path: Path, monkeypatch) -> None:
"""A resolved CDP URL routes to run_cdp_capture WITHOUT requiring a profile.
The dedicated profile is intentionally NOT created here: the CDP arm must
not decline for a missing profile (the live browser is the source).
"""
storage = tmp_path / "storage_state.json"
calls: dict[str, Any] = {}
def _fake_cdp(plan, io, *, cdp_url):
calls["cdp_url"] = cdp_url
storage.write_text("{}", encoding="utf-8")
return None
def _no_launch(*_a, **_k): # pragma: no cover - must not be called
raise AssertionError("the CDP arm must not launch the dedicated profile")
monkeypatch.setattr(hr, "run_cdp_capture", _fake_cdp)
monkeypatch.setattr(hr, "run_browser_capture", _no_launch)
monkeypatch.setattr(hr, "_playwright_installed", lambda: True)
result = attempt_headless_reauth(
storage_path=storage,
allow_headless=True,
cdp_url="http://127.0.0.1:9222",
env={},
)
assert result.status is HeadlessReauthStatus.SUCCESS
assert calls["cdp_url"] == "http://127.0.0.1:9222"
def test_cdp_url_from_env_routes_to_cdp(tmp_path: Path, monkeypatch) -> None:
storage = tmp_path / "storage_state.json"
used: dict[str, Any] = {}
def _fake_cdp(plan, io, *, cdp_url):
used["cdp_url"] = cdp_url
storage.write_text("{}", encoding="utf-8")
monkeypatch.setattr(hr, "run_cdp_capture", _fake_cdp)
monkeypatch.setattr(hr, "_playwright_installed", lambda: True)
result = attempt_headless_reauth(
storage_path=storage,
allow_headless=True,
env={"NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL": "http://127.0.0.1:9333"},
)
assert result.status is HeadlessReauthStatus.SUCCESS
assert used["cdp_url"] == "http://127.0.0.1:9333"
def test_cdp_off_host_maps_to_failed(tmp_path: Path, monkeypatch) -> None:
"""A HeadlessLoginRequiredError from the CDP arm → honest FAILED."""
storage = tmp_path / "storage_state.json"
def _fake_cdp(plan, io, *, cdp_url):
raise HeadlessLoginRequiredError("attached browser cannot reach NotebookLM")
monkeypatch.setattr(hr, "run_cdp_capture", _fake_cdp)
monkeypatch.setattr(hr, "_playwright_installed", lambda: True)
result = attempt_headless_reauth(
storage_path=storage,
allow_headless=True,
cdp_url="http://127.0.0.1:9222",
env={},
)
assert result.status is HeadlessReauthStatus.FAILED
assert "attached browser" in result.reason
assert not storage.exists()
def test_cdp_opt_in_still_required(tmp_path: Path, monkeypatch) -> None:
"""Even with a CDP URL, opt-in is required — never fires by default."""
def _boom(*_a, **_k): # pragma: no cover - must not be called
raise AssertionError("CDP arm must not run without opt-in")
monkeypatch.setattr(hr, "run_cdp_capture", _boom)
monkeypatch.setattr(hr, "_playwright_installed", lambda: True)
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=False,
cdp_url="http://127.0.0.1:9222",
env={},
)
assert result.status is HeadlessReauthStatus.UNAVAILABLE
assert "not enabled" in result.reason
def test_remote_cdp_url_does_not_route_to_cdp(tmp_path: Path, monkeypatch) -> None:
"""A remote CDP endpoint must NOT drive the CDP arm (local-only boundary).
With a remote URL and no reusable profile, the attempt declines as
UNAVAILABLE (the remote URL was rejected by ``resolve_cdp_url``, then the
profile arm found no profile) — and ``run_cdp_capture`` is never called.
"""
def _boom(*_a, **_k): # pragma: no cover - must not be called
raise AssertionError("a remote CDP endpoint must never reach run_cdp_capture")
monkeypatch.setattr(hr, "run_cdp_capture", _boom)
monkeypatch.setattr(hr, "run_browser_capture", lambda *a, **k: None)
monkeypatch.setattr(hr, "_playwright_installed", lambda: True)
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=True,
browser_profile=tmp_path / "no_profile",
env={"NOTEBOOKLM_HEADLESS_REAUTH_CDP_URL": "http://remote-host:9222"},
)
# Fell through to the profile arm, which declined (no profile) → UNAVAILABLE.
assert result.status is HeadlessReauthStatus.UNAVAILABLE
assert "no reusable browser profile" in result.reason
def test_cdp_playwright_missing_is_unavailable(tmp_path: Path, monkeypatch) -> None:
def _boom(*_a, **_k): # pragma: no cover - must not be called
raise AssertionError("CDP arm must not run without playwright")
monkeypatch.setattr(hr, "run_cdp_capture", _boom)
monkeypatch.setattr(hr, "_playwright_installed", lambda: False)
result = attempt_headless_reauth(
storage_path=tmp_path / "storage_state.json",
allow_headless=True,
cdp_url="http://127.0.0.1:9222",
env={},
)
assert result.status is HeadlessReauthStatus.UNAVAILABLE
assert "playwright is not installed" in result.reason
if __name__ == "__main__": # pragma: no cover
raise SystemExit(pytest.main([__file__, "-v"]))