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

132 lines
4.8 KiB
Python

"""Tests for the ``ctx.obj`` client-factory seam (issue #1481, U1).
These cover the dual-path resolver (``cli.auth_runtime.resolve_client_factory``)
and the ``inject_client`` test helper that replaces the per-command-module
``patch("...X_cmd.NotebookLMClient")`` seam. The end-to-end test drives a real
command through ``CliRunner.invoke(obj=...)`` -- no module patching -- to prove
the injected factory reaches client construction.
"""
from __future__ import annotations
import json
from unittest.mock import AsyncMock
import click
from notebooklm.cli.auth_runtime import resolve_client_factory
from notebooklm.client import NotebookLMClient
from notebooklm.notebooklm_cli import cli
from notebooklm.types import Label, Source
from .conftest import create_mock_client, inject_client
def _ctx(obj):
ctx = click.Context(click.Command("x"))
ctx.obj = obj
return ctx
# ---------------------------------------------------------------------------
# resolve_client_factory: resolution order
# ---------------------------------------------------------------------------
def test_resolve_prefers_injected_factory():
sentinel = lambda *a, **k: "client" # noqa: E731 - identity marker
assert resolve_client_factory(_ctx({"client_factory": sentinel}), default=str) is sentinel
def test_resolve_falls_back_to_call_site_default():
# No injected factory -> the module-level default (still-patchable name).
assert resolve_client_factory(_ctx({}), default=str) is str
def test_resolve_lazy_real_client_when_no_default():
# Post-cleanup shape: no default supplied -> lazy real NotebookLMClient.
assert resolve_client_factory(_ctx({}), default=None) is NotebookLMClient
def test_resolve_null_safe_when_ctx_obj_none():
# A bare ``click.Context`` has ``obj is None`` -- must not raise.
assert resolve_client_factory(_ctx(None), default=str) is str
def test_resolve_null_safe_when_ctx_none():
assert resolve_client_factory(None, default=str) is str
def test_resolve_ignores_none_injected_value():
# The root group seeds ``client_factory=None`` via setdefault; that must fall
# through to the default, not be returned as the factory.
assert resolve_client_factory(_ctx({"client_factory": None}), default=str) is str
# ---------------------------------------------------------------------------
# inject_client helper
# ---------------------------------------------------------------------------
def test_inject_client_returns_obj_payload_with_factory():
client = object()
obj = inject_client(client)
assert obj["client_factory"](AsyncMock(), timeout=5) is client
def test_inject_client_records_calls():
client = object()
calls: list = []
factory = inject_client(client, recorder=calls)["client_factory"]
assert factory("auth-token", timeout=5, chat_timeout=5) is client
assert calls == [("auth-token", {"timeout": 5, "chat_timeout": 5})]
# ---------------------------------------------------------------------------
# End-to-end: the injected factory reaches construction with no module patch
# ---------------------------------------------------------------------------
def test_inject_client_reaches_command_without_module_patch(
runner, mock_auth, mock_fetch_tokens
) -> None:
client = create_mock_client()
client.labels = AsyncMock()
client.labels.list = AsyncMock(
return_value=[Label(id="lblaaa111", name="Papers", emoji="📄", source_ids=["s1"])]
)
client.sources.list = AsyncMock(return_value=[Source(id="s1", title="First")])
result = runner.invoke(
cli,
["label", "list", "-n", "nb_123", "--json"],
obj=inject_client(client),
catch_exceptions=False,
)
assert result.exit_code == 0, result.output
payload = json.loads(result.stdout)
assert payload["count"] == 1
assert payload["labels"][0]["id"] == "lblaaa111"
# The injected client -- not a real NotebookLMClient -- did the RPC.
client.labels.list.assert_awaited()
def test_inject_client_records_kwargs_passthrough(runner, mock_auth, mock_fetch_tokens) -> None:
"""``source add`` builds ``client_kwargs={'timeout': ...}``; the factory sees it."""
calls: list = []
client = create_mock_client()
client.sources.add_url = AsyncMock(return_value=None)
# The downstream result handling is irrelevant here -- we only assert that the
# factory received the timeout the command assembled into ``client_kwargs``
# (construction happens before result processing, so the recorder is populated).
runner.invoke(
cli,
["source", "add", "https://example.com/a", "-n", "nb_123", "--timeout", "7", "--json"],
obj=inject_client(client, recorder=calls),
catch_exceptions=False,
)
assert any(kwargs.get("timeout") == 7 for _auth, kwargs in calls), calls