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
586 lines
24 KiB
Python
586 lines
24 KiB
Python
"""Characterization tests for ``notebooklm`` session/auth CLI commands.
|
|
|
|
These tests pin observable CLI behavior across the session command surface
|
|
BEFORE the P3.T3 service extraction. They are end-to-end at the ``CliRunner``
|
|
level so they capture exit codes, stdout/stderr structure, and JSON envelope
|
|
shape that lower-level service-unit tests do not cover holistically.
|
|
|
|
They MUST pass identically on ``main`` HEAD before the extraction commit
|
|
lands, and they MUST continue to pass byte-for-byte after the extraction.
|
|
Any divergence is a behavior regression — not an opportunity to "fix" the
|
|
old behavior.
|
|
|
|
Coverage matrix (commands x output modes from the P3.T3 spec):
|
|
|
|
| Command | text | json |
|
|
|--------------------------|------|------|
|
|
| login --browser chrome | yes | - |
|
|
| login --no-browser path | yes | - |
|
|
| status | yes | yes |
|
|
| use | yes | yes |
|
|
| clear | yes | - |
|
|
| auth check | yes | yes |
|
|
| auth refresh | yes | - |
|
|
| auth inspect | yes | yes |
|
|
|
|
Each test pins one (command, mode) cell of the matrix to a specific
|
|
expected-text fragment or JSON envelope. The fragments are intentionally
|
|
narrow (one or two structural lines) so the snapshots survive cosmetic
|
|
changes (e.g. Rich color codes) while still catching shape regressions.
|
|
|
|
P1.T3 regression coverage (must survive the extraction unchanged):
|
|
- ``use --json`` exit code matches text mode on error
|
|
- ``auth check`` OSError handling (storage unreadable)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
import notebooklm.auth as auth_module
|
|
import notebooklm.cli.context as context_module
|
|
import notebooklm.cli.helpers as helpers_module
|
|
import notebooklm.cli.resolve as resolve_module
|
|
import notebooklm.cli.services.auth_source as auth_source_module
|
|
import notebooklm.cli.services.playwright_login as playwright_login_module
|
|
import notebooklm.cli.services.session_context as session_context_module
|
|
import notebooklm.cli.session_cmd as session_cmd
|
|
import notebooklm.paths as paths_module
|
|
from notebooklm.notebooklm_cli import cli
|
|
from notebooklm.types import Notebook
|
|
from tests._fixtures import patch_session_login_dual
|
|
|
|
from .conftest import create_mock_client, inject_client
|
|
|
|
pytestmark = pytest.mark.characterization
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def char_runner() -> CliRunner:
|
|
return CliRunner()
|
|
|
|
|
|
@pytest.fixture
|
|
def char_context_file(tmp_path, monkeypatch):
|
|
"""Provide a temporary context file path with full ``get_context_path`` patching.
|
|
|
|
Patches every binding of ``get_context_path`` the CLI surface uses,
|
|
including the P3.T3 service-layer consumer
|
|
``cli.services.session_context.get_context_path`` so ``read_status``
|
|
reads from the test tmp file rather than the real
|
|
``~/.notebooklm/context.json``.
|
|
|
|
The ``session_context`` binding is patched in object form
|
|
(``monkeypatch.setattr(_session_context, ...)``) because that module is
|
|
the real consumer ``read_status`` / ``verify_and_set_notebook`` look the
|
|
symbol up in; the old ``notebooklm.cli.session_cmd.get_context_path``
|
|
string-patch was a dead pure-surface re-export (#1367) — nothing in
|
|
``session_cmd``'s body resolves ``get_context_path`` anymore, so that
|
|
patch never bit. Removing it and repointing onto the live consumer keeps
|
|
the ``status`` snapshots load-bearing (verified disable->red).
|
|
"""
|
|
import notebooklm.cli.services.session_context as _session_context
|
|
|
|
context_file = tmp_path / "context.json"
|
|
|
|
def _return_context_file(*_args, **_kwargs):
|
|
return context_file
|
|
|
|
monkeypatch.setattr(_session_context, "get_context_path", _return_context_file)
|
|
with (
|
|
patch.object(helpers_module, "get_context_path", return_value=context_file),
|
|
patch.object(context_module, "get_context_path", return_value=context_file),
|
|
patch.object(resolve_module, "get_context_path", return_value=context_file),
|
|
):
|
|
yield context_file
|
|
|
|
|
|
@pytest.fixture
|
|
def char_storage_file(tmp_path, monkeypatch):
|
|
"""Provide a storage_state.json path and patch get_storage_path to return it.
|
|
|
|
Patches every storage-path resolution seam used by the auth commands:
|
|
the canonical ``notebooklm.paths.get_storage_path`` source, the
|
|
session_cmd legacy binding, and the P3.T3 service-layer bindings.
|
|
"""
|
|
storage_file = tmp_path / "storage_state.json"
|
|
with (
|
|
patch.object(paths_module, "get_storage_path", return_value=storage_file),
|
|
patch.object(session_cmd, "get_storage_path", return_value=storage_file),
|
|
patch.object(
|
|
auth_source_module,
|
|
"get_storage_path",
|
|
return_value=storage_file,
|
|
),
|
|
patch.object(
|
|
session_context_module,
|
|
"get_storage_path",
|
|
return_value=storage_file,
|
|
create=True,
|
|
),
|
|
patch.object(
|
|
playwright_login_module,
|
|
"get_storage_path",
|
|
return_value=storage_file,
|
|
),
|
|
):
|
|
yield storage_file
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# login
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestLoginCharacterization:
|
|
"""Golden snapshots for ``notebooklm login`` paths."""
|
|
|
|
def test_login_rejects_when_auth_json_env_set(self, char_runner, monkeypatch):
|
|
"""``login`` refuses to run when NOTEBOOKLM_AUTH_JSON is set."""
|
|
monkeypatch.setenv("NOTEBOOKLM_AUTH_JSON", '{"cookies": []}')
|
|
result = char_runner.invoke(cli, ["login"])
|
|
assert result.exit_code != 0
|
|
assert "NOTEBOOKLM_AUTH_JSON" in result.output
|
|
assert "Cannot run 'login'" in result.output
|
|
|
|
def test_login_browser_chrome_invokes_playwright(self, char_runner, tmp_path):
|
|
"""``login --browser chrome`` reaches the Playwright entry point."""
|
|
with (
|
|
patch.object(session_cmd, "run_login") as mock_run_login,
|
|
patch_session_login_dual("_sync_server_language_to_config") as mock_sync,
|
|
patch.object(
|
|
session_cmd,
|
|
"prepare_paths_or_exit",
|
|
return_value=(
|
|
tmp_path / "storage_state.json",
|
|
tmp_path / "browser_profile",
|
|
),
|
|
),
|
|
):
|
|
result = char_runner.invoke(cli, ["login", "--browser", "chrome"])
|
|
|
|
assert result.exit_code == 0
|
|
mock_run_login.assert_called_once()
|
|
plan = mock_run_login.call_args.args[0]
|
|
assert plan.browser == "chrome"
|
|
assert plan.storage_path == tmp_path / "storage_state.json"
|
|
assert plan.browser_profile == tmp_path / "browser_profile"
|
|
mock_sync.assert_called_once_with(
|
|
storage_path=tmp_path / "storage_state.json",
|
|
profile=None,
|
|
)
|
|
# The "Authentication saved to:" footer is the characterization signal
|
|
assert "Authentication saved to:" in result.output
|
|
|
|
def test_login_no_browser_via_browser_cookies(self, char_runner):
|
|
"""``login --browser-cookies`` skips Playwright (no-browser path)."""
|
|
with (
|
|
patch_session_login_dual("_login_browser_cookies_single") as mock_single,
|
|
patch_session_login_dual("_warn_missing_optional_domains"),
|
|
):
|
|
result = char_runner.invoke(cli, ["login", "--browser-cookies", "chrome"])
|
|
|
|
assert result.exit_code == 0
|
|
mock_single.assert_called_once()
|
|
# When --browser-cookies is used, no Playwright "Opening ..." banner
|
|
assert "Opening" not in result.output
|
|
|
|
def test_login_account_without_browser_cookies_errors(self, char_runner):
|
|
"""``login --account email`` without ``--browser-cookies`` is a usage error."""
|
|
result = char_runner.invoke(cli, ["login", "--account", "user@example.com"])
|
|
assert result.exit_code != 0
|
|
assert "--browser-cookies" in result.output
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# use
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestUseCharacterization:
|
|
"""Golden snapshots for ``notebooklm use``."""
|
|
|
|
def test_use_text_success(self, char_runner, char_context_file, mock_auth):
|
|
"""``use <id>`` (text) prints the resolved notebook in a table."""
|
|
mock_client = create_mock_client()
|
|
mock_client.notebooks.get = AsyncMock(
|
|
return_value=Notebook(
|
|
id="nb_char_001",
|
|
title="Characterization Notebook",
|
|
created_at=datetime(2024, 1, 15),
|
|
is_owner=True,
|
|
)
|
|
)
|
|
with (
|
|
patch.object(
|
|
auth_module, "fetch_tokens_with_domains", new_callable=AsyncMock
|
|
) as mock_fetch,
|
|
patch.object(
|
|
session_cmd,
|
|
"resolve_notebook_id",
|
|
new_callable=AsyncMock,
|
|
) as mock_resolve,
|
|
):
|
|
mock_fetch.return_value = ("csrf", "session")
|
|
mock_resolve.return_value = "nb_char_001"
|
|
result = char_runner.invoke(cli, ["use", "nb_char_001"], obj=inject_client(mock_client))
|
|
|
|
assert result.exit_code == 0
|
|
assert "nb_char_001" in result.output
|
|
assert "Characterization Notebook" in result.output
|
|
|
|
def test_use_json_success_envelope(self, char_runner, char_context_file, mock_auth):
|
|
"""``use --json`` emits the documented envelope shape."""
|
|
mock_client = create_mock_client()
|
|
mock_client.notebooks.get = AsyncMock(
|
|
return_value=Notebook(
|
|
id="nb_json_001",
|
|
title="JSON Char Notebook",
|
|
created_at=datetime(2024, 2, 1),
|
|
is_owner=False,
|
|
)
|
|
)
|
|
with (
|
|
patch.object(
|
|
auth_module, "fetch_tokens_with_domains", new_callable=AsyncMock
|
|
) as mock_fetch,
|
|
patch.object(
|
|
session_cmd,
|
|
"resolve_notebook_id",
|
|
new_callable=AsyncMock,
|
|
) as mock_resolve,
|
|
):
|
|
mock_fetch.return_value = ("csrf", "session")
|
|
mock_resolve.return_value = "nb_json_001"
|
|
result = char_runner.invoke(
|
|
cli, ["use", "nb_json_001", "--json"], obj=inject_client(mock_client)
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
data = json.loads(result.output)
|
|
assert data == {
|
|
"active_notebook_id": "nb_json_001",
|
|
"success": True,
|
|
"verified": True,
|
|
"notebook": {
|
|
"id": "nb_json_001",
|
|
"title": "JSON Char Notebook",
|
|
"is_owner": False,
|
|
"created_at": "2024-02-01T00:00:00",
|
|
"modified_at": None,
|
|
},
|
|
}
|
|
|
|
def test_use_force_text_marks_not_verified(self, char_runner, char_context_file):
|
|
"""``use --force`` persists without verification and prints '(not verified ...)'."""
|
|
result = char_runner.invoke(cli, ["use", "nb_unverif", "--force"])
|
|
assert result.exit_code == 0
|
|
assert "nb_unverif" in result.output
|
|
assert "not verified" in result.output.lower()
|
|
|
|
def test_use_force_json_envelope(self, char_runner, char_context_file):
|
|
"""``use --force --json`` envelope reports ``verified: false``."""
|
|
result = char_runner.invoke(cli, ["use", "nb_unverif", "--force", "--json"])
|
|
assert result.exit_code == 0
|
|
data = json.loads(result.output)
|
|
assert data == {
|
|
"active_notebook_id": "nb_unverif",
|
|
"success": True,
|
|
"verified": False,
|
|
}
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# status
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestStatusCharacterization:
|
|
"""Golden snapshots for ``notebooklm status``."""
|
|
|
|
def test_status_no_context_text(self, char_runner, char_context_file):
|
|
if char_context_file.exists():
|
|
char_context_file.unlink()
|
|
result = char_runner.invoke(cli, ["status"])
|
|
assert result.exit_code == 0
|
|
assert "No notebook selected" in result.output
|
|
|
|
def test_status_no_context_json(self, char_runner, char_context_file):
|
|
if char_context_file.exists():
|
|
char_context_file.unlink()
|
|
result = char_runner.invoke(cli, ["status", "--json"])
|
|
assert result.exit_code == 0
|
|
data = json.loads(result.output)
|
|
assert data == {
|
|
"has_context": False,
|
|
"notebook": None,
|
|
"conversation_id": None,
|
|
}
|
|
|
|
def test_status_with_context_text(self, char_runner, char_context_file):
|
|
char_context_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"notebook_id": "nb_status_1",
|
|
"title": "Status Notebook",
|
|
"is_owner": True,
|
|
"created_at": "2024-03-04",
|
|
}
|
|
)
|
|
)
|
|
result = char_runner.invoke(cli, ["status"])
|
|
assert result.exit_code == 0
|
|
assert "nb_status_1" in result.output
|
|
assert "Status Notebook" in result.output
|
|
assert "Current Context" in result.output
|
|
|
|
def test_status_with_context_json(self, char_runner, char_context_file):
|
|
char_context_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"notebook_id": "nb_status_2",
|
|
"title": "Status Notebook 2",
|
|
"is_owner": False,
|
|
"created_at": "2024-04-05",
|
|
"conversation_id": "conv_xyz",
|
|
}
|
|
)
|
|
)
|
|
result = char_runner.invoke(cli, ["status", "--json"])
|
|
assert result.exit_code == 0
|
|
data = json.loads(result.output)
|
|
assert data == {
|
|
"has_context": True,
|
|
"notebook": {
|
|
"id": "nb_status_2",
|
|
"title": "Status Notebook 2",
|
|
"is_owner": False,
|
|
},
|
|
"conversation_id": "conv_xyz",
|
|
}
|
|
|
|
def test_status_paths_json(self, char_runner, char_context_file):
|
|
with patch.object(session_context_module, "get_path_info") as mock_path_info:
|
|
mock_path_info.return_value = {
|
|
"home_dir": "/tmp/.notebooklm",
|
|
"home_source": "default",
|
|
"storage_path": "/tmp/.notebooklm/storage_state.json",
|
|
"context_path": "/tmp/.notebooklm/context.json",
|
|
"browser_profile_dir": "/tmp/.notebooklm/browser_profile",
|
|
}
|
|
result = char_runner.invoke(cli, ["status", "--paths", "--json"])
|
|
assert result.exit_code == 0
|
|
data = json.loads(result.output)
|
|
assert "paths" in data
|
|
assert data["paths"]["home_dir"] == "/tmp/.notebooklm"
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# clear
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestClearCharacterization:
|
|
"""Golden snapshots for ``notebooklm clear``."""
|
|
|
|
def test_clear_text_when_context_present(self, char_runner, char_context_file):
|
|
char_context_file.write_text(json.dumps({"notebook_id": "nb_clear_1"}))
|
|
result = char_runner.invoke(cli, ["clear"])
|
|
assert result.exit_code == 0
|
|
assert "Context cleared" in result.output
|
|
|
|
def test_clear_text_when_no_context(self, char_runner, char_context_file):
|
|
if char_context_file.exists():
|
|
char_context_file.unlink()
|
|
result = char_runner.invoke(cli, ["clear"])
|
|
assert result.exit_code == 0
|
|
# The current behavior prints "Context cleared" unconditionally.
|
|
assert "Context cleared" in result.output
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# auth check (P1.T3 regression coverage included)
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestAuthCheckCharacterization:
|
|
"""Golden snapshots for ``notebooklm auth check``."""
|
|
|
|
def test_auth_check_text_storage_missing(self, char_runner, char_storage_file):
|
|
# storage file does not exist
|
|
result = char_runner.invoke(cli, ["auth", "check"])
|
|
assert result.exit_code == 1 # failed check ⇒ non-zero, matches --json (issue #1569)
|
|
assert "Authentication Check" in result.output
|
|
assert "Storage file not found" in result.output
|
|
|
|
def test_auth_check_json_storage_missing(self, char_runner, char_storage_file):
|
|
result = char_runner.invoke(cli, ["auth", "check", "--json"])
|
|
assert result.exit_code == 1
|
|
data = json.loads(result.output)
|
|
assert data["status"] == "error"
|
|
assert data["checks"]["storage_exists"] is False
|
|
|
|
def test_auth_check_json_valid_storage(self, char_runner, char_storage_file):
|
|
# Minimum cookie set that ``extract_cookies_from_storage`` accepts:
|
|
# both ``SID`` and ``__Secure-1PSIDTS`` are required by
|
|
# ``MINIMUM_REQUIRED_COOKIES``; HSID is the conventional companion.
|
|
char_storage_file.write_text(
|
|
json.dumps(
|
|
{
|
|
"cookies": [
|
|
{"name": "SID", "value": "abc", "domain": ".google.com"},
|
|
{
|
|
"name": "__Secure-1PSIDTS",
|
|
"value": "tts",
|
|
"domain": ".google.com",
|
|
},
|
|
{"name": "HSID", "value": "xyz", "domain": ".google.com"},
|
|
]
|
|
}
|
|
)
|
|
)
|
|
result = char_runner.invoke(cli, ["auth", "check", "--json"])
|
|
assert result.exit_code == 0
|
|
data = json.loads(result.output)
|
|
assert data["status"] == "ok"
|
|
assert data["checks"]["storage_exists"] is True
|
|
assert data["checks"]["json_valid"] is True
|
|
assert data["checks"]["sid_cookie"] is True
|
|
|
|
def test_auth_check_json_invalid_json(self, char_runner, char_storage_file):
|
|
char_storage_file.write_text("not valid json {")
|
|
result = char_runner.invoke(cli, ["auth", "check", "--json"])
|
|
assert result.exit_code == 1
|
|
data = json.loads(result.output)
|
|
assert data["status"] == "error"
|
|
assert data["checks"]["storage_exists"] is True
|
|
assert data["checks"]["json_valid"] is False
|
|
|
|
def test_auth_check_oserror_text_p1t3(self, char_runner, char_storage_file):
|
|
"""P1.T3 regression: OSError on read does not raise; reports error gracefully.
|
|
|
|
The read failure surfaces as a failed ``json_valid`` check, so text
|
|
mode exits non-zero (cleanly, no traceback) like --json (issue #1569).
|
|
"""
|
|
# File exists but read raises OSError (permission denied simulation).
|
|
char_storage_file.write_text("{}", encoding="utf-8")
|
|
with patch("pathlib.Path.read_text", side_effect=OSError("Permission denied")):
|
|
result = char_runner.invoke(cli, ["auth", "check"])
|
|
assert result.exit_code == 1
|
|
assert result.exception is None or isinstance(result.exception, SystemExit)
|
|
assert "Storage unreadable" in result.output or "Permission denied" in result.output
|
|
|
|
def test_auth_check_oserror_json_p1t3(self, char_runner, char_storage_file):
|
|
"""P1.T3 regression: ``auth check --json`` exits 1 on OSError."""
|
|
char_storage_file.write_text("{}")
|
|
with patch("pathlib.Path.read_text", side_effect=OSError("Permission denied")):
|
|
result = char_runner.invoke(cli, ["auth", "check", "--json"])
|
|
assert result.exit_code == 1
|
|
data = json.loads(result.output)
|
|
assert data["status"] == "error"
|
|
assert data["checks"]["json_valid"] is False
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# auth refresh
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestAuthRefreshCharacterization:
|
|
"""Golden snapshots for ``notebooklm auth refresh``."""
|
|
|
|
def test_auth_refresh_rejects_when_auth_json_env_set(self, char_runner, monkeypatch):
|
|
monkeypatch.setenv("NOTEBOOKLM_AUTH_JSON", '{"cookies":[]}')
|
|
result = char_runner.invoke(cli, ["auth", "refresh"])
|
|
assert result.exit_code != 0
|
|
assert "NOTEBOOKLM_AUTH_JSON" in result.output
|
|
|
|
def test_auth_refresh_default_path_success(self, char_runner, char_storage_file):
|
|
"""Default path (no --browser-cookies) calls ``fetch_tokens_with_domains``."""
|
|
char_storage_file.write_text(json.dumps({"cookies": [{"name": "SID", "value": "x"}]}))
|
|
with patch.object(
|
|
session_cmd,
|
|
"fetch_tokens_with_domains",
|
|
new_callable=AsyncMock,
|
|
) as mock_fetch:
|
|
mock_fetch.return_value = ("csrf", "session")
|
|
result = char_runner.invoke(cli, ["auth", "refresh"])
|
|
assert result.exit_code == 0
|
|
mock_fetch.assert_awaited()
|
|
assert "ok" in result.output
|
|
assert "refreshed" in result.output
|
|
|
|
def test_auth_refresh_quiet_suppresses_success_output(self, char_runner, char_storage_file):
|
|
with patch.object(
|
|
session_cmd,
|
|
"fetch_tokens_with_domains",
|
|
new_callable=AsyncMock,
|
|
) as mock_fetch:
|
|
mock_fetch.return_value = ("csrf", "session")
|
|
result = char_runner.invoke(cli, ["auth", "refresh", "--quiet"])
|
|
assert result.exit_code == 0
|
|
assert result.output.strip() == ""
|
|
|
|
def test_auth_refresh_browser_cookies_path(self, char_runner, char_storage_file):
|
|
with patch_session_login_dual("_refresh_from_browser_cookies") as mock_refresh:
|
|
result = char_runner.invoke(cli, ["auth", "refresh", "--browser-cookies", "chrome"])
|
|
assert result.exit_code == 0
|
|
mock_refresh.assert_called_once()
|
|
|
|
def test_auth_refresh_include_domains_without_browser_cookies_errors(self, char_runner):
|
|
"""``--include-domains`` without ``--browser-cookies`` is a usage error."""
|
|
result = char_runner.invoke(cli, ["auth", "refresh", "--include-domains", "youtube"])
|
|
assert result.exit_code != 0
|
|
assert "--include-domains" in result.output
|
|
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# auth inspect
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
class TestAuthInspectCharacterization:
|
|
"""Golden snapshots for ``notebooklm auth inspect``."""
|
|
|
|
def test_auth_inspect_text(self, char_runner):
|
|
"""``auth inspect`` (text) lists accounts in a table."""
|
|
fake_account_1 = MagicMock(email="a@example.com", is_default=True, browser_profile=None)
|
|
fake_account_2 = MagicMock(email="b@example.com", is_default=False, browser_profile=None)
|
|
with patch_session_login_dual("_enumerate_browser_accounts") as mock_enum:
|
|
mock_enum.return_value = ("chrome", [fake_account_1, fake_account_2])
|
|
result = char_runner.invoke(cli, ["auth", "inspect", "--browser", "chrome"])
|
|
assert result.exit_code == 0
|
|
assert "a@example.com" in result.output
|
|
assert "b@example.com" in result.output
|
|
assert "Browser:" in result.output
|
|
|
|
def test_auth_inspect_json(self, char_runner):
|
|
fake_account = MagicMock(
|
|
email="primary@example.com", is_default=True, browser_profile="Default"
|
|
)
|
|
with patch_session_login_dual("_enumerate_browser_accounts") as mock_enum:
|
|
mock_enum.return_value = ("chrome", [fake_account])
|
|
result = char_runner.invoke(cli, ["auth", "inspect", "--browser", "chrome", "--json"])
|
|
assert result.exit_code == 0
|
|
data = json.loads(result.output)
|
|
assert data == {
|
|
"browser": "chrome",
|
|
"accounts": [
|
|
{
|
|
"email": "primary@example.com",
|
|
"is_default": True,
|
|
"browser_profile": "Default",
|
|
}
|
|
],
|
|
}
|