183 lines
5.8 KiB
Python
183 lines
5.8 KiB
Python
"""Unit tests for cookie management (no network required)."""
|
|
|
|
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from xhs_cli.cookies import (
|
|
NOTE_CONTEXT_TTL_SECONDS,
|
|
cache_note_context,
|
|
clear_cookies,
|
|
cookies_to_string,
|
|
get_cached_note_context,
|
|
get_cached_xsec_token,
|
|
get_cookies,
|
|
get_index_cache_path,
|
|
get_note_by_index,
|
|
get_token_cache_path,
|
|
load_saved_cookies,
|
|
load_token_cache,
|
|
save_cookies,
|
|
save_note_index,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_config_dir(tmp_path, monkeypatch):
|
|
"""Override config dir to use temp directory."""
|
|
monkeypatch.setattr("xhs_cli.cookies.get_config_dir", lambda: tmp_path)
|
|
monkeypatch.setattr("xhs_cli.cookies.get_cookie_path", lambda: tmp_path / "cookies.json")
|
|
monkeypatch.setattr("xhs_cli.cookies._TOKEN_CACHE_MEMORY", None)
|
|
monkeypatch.setattr("xhs_cli.cookies._TOKEN_CACHE_PATH", None)
|
|
return tmp_path
|
|
|
|
|
|
class TestSaveCookies:
|
|
def test_save_and_load(self, tmp_config_dir):
|
|
cookies = {"a1": "test_value", "web_session": "sess123"}
|
|
save_cookies(cookies)
|
|
|
|
loaded = load_saved_cookies()
|
|
assert loaded is not None
|
|
assert loaded["a1"] == "test_value"
|
|
assert loaded["web_session"] == "sess123"
|
|
|
|
def test_file_permissions(self, tmp_config_dir):
|
|
cookies = {"a1": "test"}
|
|
save_cookies(cookies)
|
|
|
|
cookie_file = tmp_config_dir / "cookies.json"
|
|
stat = cookie_file.stat()
|
|
assert stat.st_mode & 0o777 == 0o600
|
|
|
|
|
|
class TestLoadSavedCookies:
|
|
def test_no_file(self, tmp_config_dir):
|
|
assert load_saved_cookies() is None
|
|
|
|
def test_invalid_json(self, tmp_config_dir):
|
|
(tmp_config_dir / "cookies.json").write_text("not json")
|
|
assert load_saved_cookies() is None
|
|
|
|
def test_missing_a1(self, tmp_config_dir):
|
|
(tmp_config_dir / "cookies.json").write_text('{"web_session": "x"}')
|
|
assert load_saved_cookies() is None
|
|
|
|
|
|
class TestClearCookies:
|
|
def test_clear_existing(self, tmp_config_dir):
|
|
save_cookies({"a1": "test"})
|
|
clear_cookies()
|
|
assert load_saved_cookies() is None
|
|
|
|
def test_clear_nonexistent(self, tmp_config_dir):
|
|
# Should not raise
|
|
clear_cookies()
|
|
|
|
|
|
class TestCookiesToString:
|
|
def test_format(self):
|
|
result = cookies_to_string({"a1": "v1", "web_session": "v2"})
|
|
assert "a1=v1" in result
|
|
assert "web_session=v2" in result
|
|
assert "; " in result
|
|
|
|
|
|
class TestGetCookies:
|
|
def test_prefers_saved_cookies_by_default(self, monkeypatch):
|
|
monkeypatch.setattr("xhs_cli.cookies.load_saved_cookies", lambda: {"a1": "saved"})
|
|
monkeypatch.setattr(
|
|
"xhs_cli.cookies.extract_browser_cookies",
|
|
lambda source: ("chrome", {"a1": "fresh"}),
|
|
)
|
|
|
|
browser, cookies = get_cookies("chrome")
|
|
assert browser == "saved"
|
|
assert cookies == {"a1": "saved"}
|
|
|
|
def test_force_refresh_bypasses_saved_cookies(self, monkeypatch):
|
|
monkeypatch.setattr("xhs_cli.cookies.load_saved_cookies", lambda: {"a1": "saved"})
|
|
monkeypatch.setattr(
|
|
"xhs_cli.cookies.extract_browser_cookies",
|
|
lambda source: ("chrome", {"a1": "fresh"}),
|
|
)
|
|
saved = []
|
|
monkeypatch.setattr("xhs_cli.cookies.save_cookies", lambda cookies: saved.append(cookies))
|
|
|
|
browser, cookies = get_cookies("chrome", force_refresh=True)
|
|
assert browser == "chrome"
|
|
assert cookies == {"a1": "fresh"}
|
|
assert saved == [{"a1": "fresh"}]
|
|
|
|
|
|
class TestNoteContextCache:
|
|
def test_cache_persists_token_and_source(self, tmp_config_dir):
|
|
cache_note_context("note-1", "token-1", "pc_search", context="search")
|
|
|
|
assert get_cached_xsec_token("note-1") == "token-1"
|
|
context = get_cached_note_context("note-1")
|
|
assert context["token"] == "token-1"
|
|
assert context["source"] == "pc_search"
|
|
assert context["context"] == "search"
|
|
|
|
def test_load_token_cache_keeps_legacy_entries_compatible(self, tmp_config_dir):
|
|
get_token_cache_path().write_text('{"note-1":"legacy-token"}')
|
|
|
|
cache = load_token_cache()
|
|
assert cache["note-1"]["token"] == "legacy-token"
|
|
assert cache["note-1"]["source"] == ""
|
|
|
|
def test_expired_note_context_is_not_returned(self, tmp_config_dir):
|
|
stale_ts = time.time() - NOTE_CONTEXT_TTL_SECONDS - 10
|
|
get_token_cache_path().write_text(
|
|
f'{{"note-1":{{"token":"stale-token","source":"pc_search","ts":{stale_ts}}}}}'
|
|
)
|
|
|
|
assert get_cached_note_context("note-1") == {}
|
|
|
|
|
|
class TestNoteIndexCache:
|
|
def test_save_and_resolve_index_with_source(self, tmp_config_dir):
|
|
save_note_index([
|
|
{
|
|
"note_id": "note-1",
|
|
"xsec_token": "token-1",
|
|
"xsec_source": "pc_search",
|
|
}
|
|
])
|
|
|
|
assert get_note_by_index(1) == {
|
|
"note_id": "note-1",
|
|
"xsec_token": "token-1",
|
|
"xsec_source": "pc_search",
|
|
}
|
|
|
|
def test_save_empty_index_clears_previous_entries(self, tmp_config_dir):
|
|
save_note_index([
|
|
{
|
|
"note_id": "note-1",
|
|
"xsec_token": "token-1",
|
|
"xsec_source": "pc_search",
|
|
}
|
|
])
|
|
save_note_index([])
|
|
|
|
assert get_note_by_index(1) is None
|
|
assert get_index_cache_path().read_text() == "[]"
|
|
|
|
def test_index_file_permissions(self, tmp_config_dir):
|
|
save_note_index([{"note_id": "note-1", "xsec_token": "", "xsec_source": ""}])
|
|
|
|
stat = get_index_cache_path().stat()
|
|
assert stat.st_mode & 0o777 == 0o600
|
|
|
|
def test_index_normalizes_missing_optional_fields(self, tmp_config_dir):
|
|
get_index_cache_path().write_text('[{"note_id":"note-1"}]')
|
|
|
|
assert get_note_by_index(1) == {
|
|
"note_id": "note-1",
|
|
"xsec_token": "",
|
|
"xsec_source": "",
|
|
}
|