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

215 lines
7.8 KiB
Python

"""Tests for ``notebooklm._app.profile`` — the profile-management core.
Covers the Click-free profile logic backing ``profile list`` / ``delete`` /
``switch`` / ``rename``:
* :func:`gather_profile_list` — name → active/authenticated/account rows, with
the profile/storage/account collaborators injected (no ``notebooklm.paths`` /
``notebooklm.auth`` reach-in); the empty-profiles case; account-email coercion.
* :func:`is_protected_profile` — the active/configured-default delete guard.
* :func:`set_default_profile_mutator` / :func:`retarget_default_profile_mutator`
— the ``config.json`` mutator closures, the "missing default ⇒ 'default'" rule,
the ``was_updated`` predicate, and corrupt-config recovery (the mutator runs
under the adapter's lock against whatever dict it is handed).
Direct ``_app`` calls only — collaborators injected as plain callables /
``MagicMock``s, no Click / CliRunner.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
from notebooklm._app.profile import (
ProfileEntry,
gather_profile_list,
is_protected_profile,
retarget_default_profile_mutator,
set_default_profile_mutator,
)
def _fake_storage(*, exists: bool) -> MagicMock:
storage = MagicMock(spec=Path)
storage.exists.return_value = exists
return storage
# ---------------------------------------------------------------------------
# gather_profile_list
# ---------------------------------------------------------------------------
def test_gather_profile_list_builds_rows() -> None:
storages = {
"default": _fake_storage(exists=True),
"work": _fake_storage(exists=False),
}
accounts = {
"default": {"email": "me@example.com"},
"work": {},
}
def _get_storage_path(*, profile: str) -> Path:
return storages[profile]
def _read_account_metadata(path: Path) -> dict[str, Any]:
# Look up by identity to map the injected storage back to its account.
for name, storage in storages.items():
if storage is path:
return accounts[name]
raise AssertionError("unexpected storage path")
entries, active = gather_profile_list(
list_profiles=lambda: ["default", "work"],
resolve_profile=lambda: "default",
get_storage_path=_get_storage_path,
read_account_metadata=_read_account_metadata,
)
assert active == "default"
assert entries == [
ProfileEntry(name="default", active=True, authenticated=True, account="me@example.com"),
ProfileEntry(name="work", active=False, authenticated=False, account=None),
]
def test_gather_profile_list_empty() -> None:
"""No profiles → empty rows but the active name still resolves."""
entries, active = gather_profile_list(
list_profiles=lambda: [],
resolve_profile=lambda: "default",
get_storage_path=lambda *, profile: _fake_storage(exists=False),
read_account_metadata=lambda path: {},
)
assert entries == []
assert active == "default"
def test_gather_profile_list_coerces_non_str_email_to_none() -> None:
"""A non-string ``email`` field is coerced to ``None`` (defensive)."""
entries, _ = gather_profile_list(
list_profiles=lambda: ["default"],
resolve_profile=lambda: "default",
get_storage_path=lambda *, profile: _fake_storage(exists=True),
read_account_metadata=lambda path: {"email": 12345},
)
assert entries[0].account is None
def test_gather_profile_list_get_storage_called_per_profile() -> None:
get_storage_path = MagicMock(side_effect=lambda *, profile: _fake_storage(exists=True))
gather_profile_list(
list_profiles=lambda: ["a", "b", "c"],
resolve_profile=lambda: "a",
get_storage_path=get_storage_path,
read_account_metadata=lambda path: {},
)
assert get_storage_path.call_count == 3
get_storage_path.assert_any_call(profile="a")
get_storage_path.assert_any_call(profile="c")
# ---------------------------------------------------------------------------
# is_protected_profile
# ---------------------------------------------------------------------------
def test_is_protected_profile_blocks_active() -> None:
assert (
is_protected_profile("work", configured_default="default", effective_active="work") is True
)
def test_is_protected_profile_blocks_configured_default() -> None:
assert (
is_protected_profile("default", configured_default="default", effective_active="work")
is True
)
def test_is_protected_profile_allows_other() -> None:
assert (
is_protected_profile("old", configured_default="default", effective_active="work") is False
)
# ---------------------------------------------------------------------------
# set_default_profile_mutator
# ---------------------------------------------------------------------------
def test_set_default_profile_mutator_sets_key() -> None:
mutator = set_default_profile_mutator("work")
out = mutator({"default_profile": "default", "other": 1})
assert out["default_profile"] == "work"
# Other keys are preserved.
assert out["other"] == 1
def test_set_default_profile_mutator_on_empty_config() -> None:
mutator = set_default_profile_mutator("work")
assert mutator({}) == {"default_profile": "work"}
# ---------------------------------------------------------------------------
# retarget_default_profile_mutator
# ---------------------------------------------------------------------------
def test_retarget_rewrites_matching_default() -> None:
mutator, was_updated = retarget_default_profile_mutator(old_name="work", new_name="work-old")
out = mutator({"default_profile": "work"})
assert out["default_profile"] == "work-old"
assert was_updated() is True
def test_retarget_missing_default_treated_as_default() -> None:
"""A config with no ``default_profile`` key implies the literal 'default'."""
mutator, was_updated = retarget_default_profile_mutator(old_name="default", new_name="primary")
out = mutator({}) # missing key ⇒ implicit "default"
assert out["default_profile"] == "primary"
assert was_updated() is True
def test_retarget_blank_default_treated_as_default() -> None:
"""An empty-string ``default_profile`` is also treated as 'default'."""
mutator, was_updated = retarget_default_profile_mutator(old_name="default", new_name="primary")
out = mutator({"default_profile": ""})
assert out["default_profile"] == "primary"
assert was_updated() is True
def test_retarget_noop_when_default_is_other() -> None:
"""Renaming a non-default profile leaves the config + predicate untouched."""
mutator, was_updated = retarget_default_profile_mutator(old_name="work", new_name="work-old")
out = mutator({"default_profile": "primary"})
assert out["default_profile"] == "primary"
assert was_updated() is False
def test_retarget_was_updated_false_before_run() -> None:
"""The predicate reports False until the mutator has actually fired."""
_mutator, was_updated = retarget_default_profile_mutator(old_name="work", new_name="w2")
assert was_updated() is False
def test_retarget_recovers_against_corrupt_recovered_dict() -> None:
"""Under corrupt-config recovery the adapter hands the mutator a fresh ``{}``.
The locked writer recovers a corrupt config to an empty dict before applying
the mutator; the missing-key ⇒ 'default' rule then still retargets a renamed
'default' profile.
"""
mutator, was_updated = retarget_default_profile_mutator(old_name="default", new_name="renamed")
recovered: dict[str, Any] = {} # what _atomic_write_config(recover_from_corrupt=True) passes
out = mutator(recovered)
assert out["default_profile"] == "renamed"
assert was_updated() is True