Files
nanocode/skills/xiaohongshu-cli/tests/test_client.py
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

279 lines
10 KiB
Python

"""Unit tests for XHS client request payloads, cookies, and endpoint selection."""
from collections import OrderedDict
import httpx
import pytest
from xhs_cli.client import XhsClient
from xhs_cli.cookies import cache_note_context, get_cached_note_context
from xhs_cli.exceptions import UnsupportedOperationError, XhsApiError
class TestFavorites:
def test_unfavorite_uses_note_ids_payload(self, monkeypatch):
captured = {}
def fake_post(self, uri, data, header_overrides=None):
captured["uri"] = uri
captured["data"] = data
return True
monkeypatch.setattr(XhsClient, "_main_api_post", fake_post)
client = XhsClient({"a1": "cookie"})
try:
client.unfavorite_note("note-123")
finally:
client.close()
assert captured["uri"] == "/api/sns/web/v1/note/uncollect"
assert captured["data"] == {"note_ids": "note-123"}
class TestCreatorEndpoints:
def test_creator_note_list_uses_v2_endpoint(self, monkeypatch):
captured = {}
def fake_get(self, uri, params=None):
captured["uri"] = uri
captured["params"] = params
return {"notes": [], "page": -1}
monkeypatch.setattr(XhsClient, "_creator_get", fake_get)
client = XhsClient({"a1": "cookie"})
try:
result = client.get_creator_note_list(page=2)
finally:
client.close()
assert result["page"] == -1
assert captured["uri"] == "/api/galaxy/v2/creator/note/user/posted"
assert captured["params"] == {"tab": 0, "page": 2}
def test_delete_note_raises_unsupported_for_404(self, monkeypatch):
def fake_post(self, uri, data):
raise XhsApiError(
"API error: {\"status\": 404}",
response={"status": 404},
)
monkeypatch.setattr(XhsClient, "_creator_post", fake_post)
client = XhsClient({"a1": "cookie"})
try:
with pytest.raises(UnsupportedOperationError, match="Delete note is currently unavailable"):
client.delete_note("note-123")
finally:
client.close()
class TestTransportCookies:
def test_request_with_retry_merges_response_cookies(self, monkeypatch):
request = httpx.Request("POST", "https://edith.xiaohongshu.com/api/test")
response = httpx.Response(
200,
headers=[
("set-cookie", "web_session=real-session; Path=/; Domain=.xiaohongshu.com"),
("set-cookie", "web_session_sec=real-sec; Path=/; Domain=.xiaohongshu.com"),
],
json={"success": True, "data": {"ok": True}},
request=request,
)
class _FakeHttpClient:
def request(self, method, url, **kwargs):
return response
def close(self):
return None
client = XhsClient({"a1": "cookie", "web_session": "guest-session"}, request_delay=0)
client._http = _FakeHttpClient()
try:
resp = client._request_with_retry("POST", "https://edith.xiaohongshu.com/api/test")
finally:
client.close()
assert resp is response
assert client.cookies["web_session"] == "real-session"
assert client.cookies["web_session_sec"] == "real-sec"
class TestReadingEndpointBehavior:
def test_get_note_detail_prefers_cached_xsec_source(self, monkeypatch):
captured = {}
def fake_get_note_by_id(self, note_id, xsec_token="", xsec_source="pc_feed"):
captured["note_id"] = note_id
captured["token"] = xsec_token
captured["source"] = xsec_source
return {"items": [{"note_card": {"title": "ok"}}]}
monkeypatch.setattr(XhsClient, "get_note_by_id", fake_get_note_by_id)
cache_note_context("note-123", "token-xyz", "pc_search")
client = XhsClient({"a1": "cookie"})
try:
client.get_note_detail("note-123")
finally:
client.close()
assert captured == {
"note_id": "note-123",
"token": "token-xyz",
"source": "pc_search",
}
def test_search_notes_uses_browser_like_prewarm_sequence(self, monkeypatch):
calls = []
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE", OrderedDict())
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE_LOADED", True)
def fake_get(self, uri, params=None):
calls.append(("GET", uri, params))
return {"ok": True}
def fake_post(self, uri, data, header_overrides=None):
calls.append(("POST", uri, data))
if uri == "/api/sns/web/v1/search/notes":
return {"items": [], "has_more": False}
return {"ok": True}
monkeypatch.setattr(XhsClient, "_main_api_get", fake_get)
monkeypatch.setattr(XhsClient, "_main_api_post", fake_post)
client = XhsClient({"a1": "cookie"})
try:
client.search_notes("openclaw prompt", page=2)
finally:
client.close()
assert [call[1] for call in calls] == [
"/api/sns/web/v1/search/onebox",
"/api/sns/web/v1/search/filter",
"/api/sns/web/v1/search/notes",
"/api/sns/web/v1/search/recommend",
]
notes_payload = calls[2][2]
assert notes_payload["page"] == 2
assert notes_payload["filters"][0]["type"] == "sort_type"
assert notes_payload["filters"][1]["type"] == "filter_note_type"
def test_search_notes_reuses_search_id_across_pages(self, monkeypatch):
calls = []
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE", OrderedDict())
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE_LOADED", True)
def fake_get(self, uri, params=None):
calls.append(("GET", uri, params))
return {"ok": True}
def fake_post(self, uri, data, header_overrides=None):
calls.append(("POST", uri, data))
if uri == "/api/sns/web/v1/search/notes":
return {"items": [], "has_more": False}
return {"ok": True}
monkeypatch.setattr(XhsClient, "_main_api_get", fake_get)
monkeypatch.setattr(XhsClient, "_main_api_post", fake_post)
client = XhsClient({"a1": "cookie"})
try:
client.search_notes("openclaw", page=1)
client.search_notes("openclaw", page=2)
finally:
client.close()
notes_calls = [call for call in calls if call[1] == "/api/sns/web/v1/search/notes"]
assert len(notes_calls) == 2
assert notes_calls[0][2]["search_id"] == notes_calls[1][2]["search_id"]
onebox_calls = [call for call in calls if call[1] == "/api/sns/web/v1/search/onebox"]
filter_calls = [call for call in calls if call[1] == "/api/sns/web/v1/search/filter"]
recommend_calls = [call for call in calls if call[1] == "/api/sns/web/v1/search/recommend"]
assert len(onebox_calls) == 1
assert len(filter_calls) == 1
assert len(recommend_calls) == 1
def test_search_notes_reuses_search_id_after_cache_reload(self, monkeypatch, tmp_path):
first_calls = []
second_calls = []
monkeypatch.setattr("xhs_cli.client_mixins.get_config_dir", lambda: tmp_path)
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE", OrderedDict())
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE_LOADED", False)
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE_PATH", None)
def fake_get_first(self, uri, params=None):
first_calls.append(("GET", uri, params))
return {"ok": True}
def fake_post_first(self, uri, data, header_overrides=None):
first_calls.append(("POST", uri, data))
if uri == "/api/sns/web/v1/search/notes":
return {"items": [], "has_more": False}
return {"ok": True}
monkeypatch.setattr(XhsClient, "_main_api_get", fake_get_first)
monkeypatch.setattr(XhsClient, "_main_api_post", fake_post_first)
client = XhsClient({"a1": "cookie"})
try:
client.search_notes("openclaw", page=1)
finally:
client.close()
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE", OrderedDict())
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE_LOADED", False)
monkeypatch.setattr("xhs_cli.client_mixins._SEARCH_SESSION_CACHE_PATH", None)
def fake_get_second(self, uri, params=None):
second_calls.append(("GET", uri, params))
return {"ok": True}
def fake_post_second(self, uri, data, header_overrides=None):
second_calls.append(("POST", uri, data))
if uri == "/api/sns/web/v1/search/notes":
return {"items": [], "has_more": False}
return {"ok": True}
monkeypatch.setattr(XhsClient, "_main_api_get", fake_get_second)
monkeypatch.setattr(XhsClient, "_main_api_post", fake_post_second)
client = XhsClient({"a1": "cookie"})
try:
client.search_notes("openclaw", page=2)
finally:
client.close()
first_notes_call = next(call for call in first_calls if call[1] == "/api/sns/web/v1/search/notes")
second_notes_call = next(call for call in second_calls if call[1] == "/api/sns/web/v1/search/notes")
assert first_notes_call[2]["search_id"] == second_notes_call[2]["search_id"]
assert [call[1] for call in second_calls] == ["/api/sns/web/v1/search/notes"]
def test_get_note_detail_invalidates_cached_context_after_feed_failure(self, monkeypatch):
cache_note_context("note-123", "stale-token", "pc_search")
def fake_get_note_by_id(self, note_id, xsec_token="", xsec_source="pc_feed"):
raise XhsApiError("stale token")
def fake_get_note_from_html(self, note_id, xsec_token="", xsec_source="pc_feed"):
return {"items": [{"note_card": {"title": "html-ok"}}]}
monkeypatch.setattr(XhsClient, "get_note_by_id", fake_get_note_by_id)
monkeypatch.setattr(XhsClient, "get_note_from_html", fake_get_note_from_html)
client = XhsClient({"a1": "cookie"})
try:
result = client.get_note_detail("note-123")
finally:
client.close()
assert result["items"][0]["note_card"]["title"] == "html-ok"
assert get_cached_note_context("note-123") == {}