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
132 lines
4.8 KiB
Python
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
|