Files
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

226 lines
8.3 KiB
Python

"""Unit tests for the core cookie persistence collaborator."""
from __future__ import annotations
import threading
from pathlib import Path
from typing import Any
import httpx
import pytest
import notebooklm._cookie_persistence as persistence_module
import notebooklm._runtime.lifecycle as lifecycle_module
from notebooklm._auth.storage import (
CookieSaveResult,
CookieSnapshot,
CookieSnapshotKey,
snapshot_cookie_jar,
)
from notebooklm._cookie_persistence import CookiePersistence
from notebooklm.auth import (
AuthTokens,
)
from tests._helpers.client_factory import build_client_shell_for_tests
def _auth_tokens(storage_path: Path | None = None) -> AuthTokens:
return AuthTokens(
cookies={"SID": "sid", "__Secure-1PSIDTS": "psidts"},
csrf_token="csrf",
session_id="session",
storage_path=storage_path,
)
def _jar(sid: str = "sid", psidts: str = "psidts") -> httpx.Cookies:
jar = httpx.Cookies()
jar.set("SID", sid, domain=".google.com", path="/")
jar.set("__Secure-1PSIDTS", psidts, domain=".google.com", path="/")
return jar
def test_client_core_exposes_cookie_persistence(tmp_path: Path) -> None:
core = build_client_shell_for_tests(_auth_tokens(tmp_path / "storage_state.json"))
baseline = snapshot_cookie_jar(_jar())
# The ``Session._save_lock`` + ``Session._loaded_cookie_snapshot`` compat
# bridges were retired in the session-shrink arc; tests read on the
# collaborator directly.
core._collaborators.cookie_persistence.loaded_cookie_snapshot = baseline
assert isinstance(core._collaborators.cookie_persistence, CookiePersistence)
assert core._collaborators.cookie_persistence.loaded_cookie_snapshot is baseline
# The save lock is a stock ``threading.Lock`` owned by the collaborator.
assert isinstance(core._collaborators.cookie_persistence.save_lock, type(threading.Lock()))
@pytest.mark.asyncio
async def test_client_core_save_cookies_routes_through_injected_seam_and_to_thread(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""``Session.save_cookies`` routes the write through
``asyncio.to_thread`` (off the loop) and then invokes the
constructor-injected ``cookie_saver``.
Both halves of this test avoid the legacy ``_core`` indirection:
- ``save_cookies_to_storage`` is injected at construction via the
``cookie_saver`` seam (Wave 1's :class:`ClientLifecycle` change).
- ``asyncio.to_thread`` is patched on its canonical importing
module :mod:`notebooklm._runtime.lifecycle` (where
``ClientLifecycle.save_cookies`` sources it via
``cookie_persistence.save(to_thread=asyncio.to_thread)``).
"""
calls: list[str] = []
def fake_save(cookie_jar: httpx.Cookies, path: Path | None = None, **kwargs: Any) -> bool:
calls.append("save")
assert path == tmp_path / "storage_state.json"
assert "original_snapshot" in kwargs
return True
async def fake_to_thread(func, /, *args, **kwargs): # type: ignore[no-untyped-def]
calls.append("to_thread")
return func(*args, **kwargs)
monkeypatch.setattr(lifecycle_module.asyncio, "to_thread", fake_to_thread)
core = build_client_shell_for_tests(
_auth_tokens(tmp_path / "storage_state.json"), cookie_saver=fake_save
)
await core._collaborators.lifecycle.save_cookies(core._collaborators.cookie_persistence, _jar())
assert calls == ["to_thread", "save"]
@pytest.mark.asyncio
async def test_cookie_persistence_snapshots_on_loop_and_writes_off_thread(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
auth = _auth_tokens(tmp_path / "storage_state.json")
persistence = CookiePersistence(auth, tmp_path / "storage_state.json")
baseline = snapshot_cookie_jar(_jar())
persistence.loaded_cookie_snapshot = baseline
auth.cookie_snapshot = baseline
loop_thread = threading.current_thread()
snapshot_threads: list[threading.Thread] = []
writer_thread: threading.Thread | None = None
real_snapshot = persistence_module.snapshot_cookie_jar
def snapshot_spy(cookie_jar: httpx.Cookies) -> CookieSnapshot:
snapshot_threads.append(threading.current_thread())
return real_snapshot(cookie_jar)
async def worker_to_thread(func, /, *args, **kwargs): # type: ignore[no-untyped-def]
assert snapshot_threads == [loop_thread]
raised: list[BaseException] = []
def run() -> None:
try:
func(*args, **kwargs)
except BaseException as exc: # pragma: no cover - re-raised on loop thread
raised.append(exc)
worker = threading.Thread(target=run, name="cookie-save-test-worker")
worker.start()
worker.join(timeout=5.0)
assert not worker.is_alive()
if raised:
raise raised[0]
def fake_save(
cookie_jar: httpx.Cookies,
path: Path | None = None,
*,
original_snapshot: CookieSnapshot | None = None,
return_result: bool = False,
) -> bool | CookieSaveResult:
nonlocal writer_thread
writer_thread = threading.current_thread()
assert path == tmp_path / "storage_state.json"
assert original_snapshot is baseline
assert return_result is True
assert persistence.save_lock.locked()
return CookieSaveResult(True)
monkeypatch.setattr(persistence_module, "snapshot_cookie_jar", snapshot_spy)
await persistence.save(
_jar(psidts="rotated"),
save_cookies_to_storage=fake_save,
to_thread=worker_to_thread,
)
psidts_key = CookieSnapshotKey("__Secure-1PSIDTS", ".google.com", "/")
assert snapshot_threads == [loop_thread]
assert writer_thread is not None and writer_thread is not loop_thread
assert persistence.loaded_cookie_snapshot is not baseline
assert persistence.loaded_cookie_snapshot is auth.cookie_snapshot
assert persistence.loaded_cookie_snapshot[psidts_key].value == "rotated"
@pytest.mark.asyncio
async def test_cookie_persistence_advances_baseline_only_on_accepted_saves(
tmp_path: Path,
) -> None:
auth = _auth_tokens(tmp_path / "storage_state.json")
persistence = CookiePersistence(auth, tmp_path / "storage_state.json")
baseline = snapshot_cookie_jar(_jar(sid="sid-old", psidts="psidts-old"))
persistence.loaded_cookie_snapshot = baseline
auth.cookie_snapshot = baseline
sid_key = CookieSnapshotKey("SID", ".google.com", "/")
psidts_key = CookieSnapshotKey("__Secure-1PSIDTS", ".google.com", "/")
results: list[CookieSaveResult] = [
CookieSaveResult(False),
CookieSaveResult(False, frozenset({psidts_key})),
CookieSaveResult(True),
]
async def inline_to_thread(func, /, *args, **kwargs): # type: ignore[no-untyped-def]
return func(*args, **kwargs)
def fake_save(
cookie_jar: httpx.Cookies,
path: Path | None = None,
*,
original_snapshot: CookieSnapshot | None = None,
return_result: bool = False,
) -> bool | CookieSaveResult:
assert original_snapshot is persistence.loaded_cookie_snapshot
assert return_result is True
return results.pop(0)
await persistence.save(
_jar(sid="sid-silent", psidts="psidts-silent"),
save_cookies_to_storage=fake_save,
to_thread=inline_to_thread,
)
assert persistence.loaded_cookie_snapshot is baseline
assert auth.cookie_snapshot is baseline
await persistence.save(
_jar(sid="sid-accepted", psidts="psidts-rejected"),
save_cookies_to_storage=fake_save,
to_thread=inline_to_thread,
)
assert persistence.loaded_cookie_snapshot is not None
assert persistence.loaded_cookie_snapshot is auth.cookie_snapshot
assert persistence.loaded_cookie_snapshot[sid_key].value == "sid-accepted"
assert persistence.loaded_cookie_snapshot[psidts_key].value == "psidts-old"
await persistence.save(
_jar(sid="sid-final", psidts="psidts-final"),
save_cookies_to_storage=fake_save,
to_thread=inline_to_thread,
)
assert persistence.loaded_cookie_snapshot is not None
assert persistence.loaded_cookie_snapshot is auth.cookie_snapshot
assert persistence.loaded_cookie_snapshot[sid_key].value == "sid-final"
assert persistence.loaded_cookie_snapshot[psidts_key].value == "psidts-final"