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

161 lines
6.6 KiB
Python

"""Unit tests for ``_enumerate_one_jar`` failure-outcome branches.
``_enumerate_one_jar`` probes one rookiepy cookie set against
``?authuser=N`` and returns either a list of :class:`Account` records or a
:class:`BrowserCookieOutcome` subclass on failure. The CLI-level fan-out
tests exercise the happy paths and the quiet network branch; these direct
unit tests pin the remaining validation-failure and stale-cookie outcome
shapes (both quiet and loud).
"""
from __future__ import annotations
from contextlib import ExitStack, contextmanager
from unittest.mock import MagicMock, patch
import httpx
import pytest
import notebooklm.auth as auth_module
from notebooklm.cli.services.login import cookie_jar
from notebooklm.cli.services.login.outcomes import (
CookieValidationFailure,
NetworkFailure,
StaleCookies,
)
from tests._fixtures.login_io import RecordingLoginIO
class _FakeAccount:
def __init__(self, authuser, email, is_default):
self.authuser = authuser
self.email = email
self.is_default = is_default
@contextmanager
def _enumerate_env(*, validate_return, run_async_side_effect=None, run_async_return=None):
"""Patch the collaborators ``_enumerate_one_jar`` reaches at call time.
``validate_with_recovery`` is a module-level import in ``cookie_jar``
(patched via ``patch.object``); ``build_cookie_jar`` /
``extract_cookies_with_domains`` / ``enumerate_accounts`` are *function-local*
``from ....auth import ...`` lookups, so they're patched on ``notebooklm.auth``.
``enumerate_accounts`` is async, so an auto-specced ``patch`` would hand back
an un-awaited coroutine (the ``io.run_async`` stub never awaits it); an
explicit sync ``MagicMock`` returns a plain sentinel instead.
The async bridge is no longer a module-level ``run_async`` (#1393 inverted
it behind the injected ``LoginIO`` sink). This helper yields a
:class:`RecordingLoginIO` whose ``run_async`` is the configured stub; tests
pass it as ``io=`` so the probe result / side-effect is controlled exactly
as before.
"""
run_async = MagicMock(side_effect=run_async_side_effect, return_value=run_async_return)
with ExitStack() as stack:
stack.enter_context(
patch.object(cookie_jar, "validate_with_recovery", return_value=validate_return)
)
stack.enter_context(
patch.object(auth_module, "extract_cookies_with_domains", return_value={})
)
stack.enter_context(patch.object(auth_module, "build_cookie_jar", return_value=object()))
stack.enter_context(
patch.object(auth_module, "enumerate_accounts", MagicMock(return_value=object()))
)
yield RecordingLoginIO(run_async=run_async)
class TestValidationFailure:
def test_quiet_returns_collapsed_validation_failure(self):
with _enumerate_env(validate_return=({"cookies": []}, ValueError("missing SID"))) as io:
out = cookie_jar._enumerate_one_jar([], "chrome", None, quiet=True, io=io)
assert isinstance(out, CookieValidationFailure)
assert out.code == "COOKIE_VALIDATION_FAILED"
# Quiet mode collapses to the single-line message naming the browser.
assert "chrome" in out.message
assert "\n" not in out.message
def test_loud_returns_validation_failure_with_hint(self):
with _enumerate_env(validate_return=({"cookies": []}, ValueError("missing SID"))) as io:
out = cookie_jar._enumerate_one_jar([], "chrome", None, quiet=False, io=io)
assert isinstance(out, CookieValidationFailure)
assert out.code == "COOKIE_VALIDATION_FAILED"
# Loud mode includes the underlying error and a multi-line hint body.
assert "missing SID" in out.message
assert "No valid Google authentication cookies" in out.message
class TestStaleCookies:
def test_quiet_returns_collapsed_stale_outcome(self):
with _enumerate_env(
validate_return=({"cookies": [1]}, None),
run_async_side_effect=ValueError("rejected"),
) as io:
out = cookie_jar._enumerate_one_jar([{"x": 1}], "firefox", None, quiet=True, io=io)
assert isinstance(out, StaleCookies)
assert out.code == "STALE_COOKIES"
assert "firefox" in out.message
assert "too stale" in out.message
def test_loud_returns_detailed_stale_outcome(self):
with _enumerate_env(
validate_return=({"cookies": [1]}, None),
run_async_side_effect=ValueError("rejected"),
) as io:
out = cookie_jar._enumerate_one_jar([{"x": 1}], "firefox", None, quiet=False, io=io)
assert isinstance(out, StaleCookies)
assert out.code == "STALE_COOKIES"
assert "Account discovery failed" in out.message
assert "notebooklm login" in out.message
class TestNetworkFailure:
def test_quiet_reraises_network_error(self):
with (
_enumerate_env(
validate_return=({"cookies": [1]}, None),
run_async_side_effect=httpx.ConnectError("no route"),
) as io,
pytest.raises(httpx.RequestError),
):
cookie_jar._enumerate_one_jar([{"x": 1}], "chrome", None, quiet=True, io=io)
def test_loud_returns_network_failure_outcome(self):
with _enumerate_env(
validate_return=({"cookies": [1]}, None),
run_async_side_effect=httpx.ConnectError("no route"),
) as io:
out = cookie_jar._enumerate_one_jar([{"x": 1}], "chrome", None, quiet=False, io=io)
assert isinstance(out, NetworkFailure)
assert out.code == "NETWORK_ERROR"
class TestSuccessPath:
def test_legacy_single_jar_returns_accounts_unchanged(self):
accounts = [_FakeAccount(0, "a@gmail.com", True)]
with _enumerate_env(
validate_return=({"cookies": [1]}, None), run_async_return=accounts
) as io:
out = cookie_jar._enumerate_one_jar([{"x": 1}], "chrome", None, io=io)
assert out == accounts
def test_fanout_tags_accounts_with_browser_profile(self):
# Account is reconstructed inside the function from notebooklm.auth, so
# leave the real class in place here (only the probe is stubbed).
accounts = [_FakeAccount(0, "a@gmail.com", True)]
with _enumerate_env(
validate_return=({"cookies": [1]}, None), run_async_return=accounts
) as io:
out = cookie_jar._enumerate_one_jar([{"x": 1}], "chrome", "Profile 1", io=io)
assert len(out) == 1
assert out[0].browser_profile == "Profile 1"
assert out[0].email == "a@gmail.com"