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

258 lines
8.7 KiB
Python

"""Unit tests for QR code login flow."""
import pytest
from xhs_cli.command_normalizers import normalize_xhs_user_payload
from xhs_cli.exceptions import XhsApiError
from xhs_cli.qr_login import BrowserQrLoginUnavailable, _normalize_browser_cookies, qrcode_login
class _FakeQrClient:
instances = []
def __init__(self, cookies, request_delay=0, **kwargs):
self.cookies = dict(cookies)
self.activate_calls = 0
self.status_calls = 0
self.complete_calls = 0
self.create_seen_web_session = None
self.status_seen_web_session = None
self.complete_seen_web_sessions = []
self.self_info_calls = 0
type(self).instances.append(self)
def __enter__(self):
return self
def __exit__(self, *args):
return False
def login_activate(self):
self.activate_calls += 1
if self.activate_calls == 1:
return {"session": "guest-session", "secure_session": "guest-sec", "user_id": "guest-user"}
return {"session": "unexpected-session", "secure_session": "unexpected-sec", "user_id": "unexpected-user"}
def create_qr_login(self):
self.create_seen_web_session = self.cookies.get("web_session")
return {"qr_id": "qr-1", "code": "code-1", "url": "https://example.com/qr"}
def check_qr_status(self, qr_id, code):
self.status_calls += 1
self.status_seen_web_session = self.cookies.get("web_session")
return {"codeStatus": 2, "userId": "real-user"}
def complete_qr_login(self, qr_id, code):
self.complete_calls += 1
self.complete_seen_web_sessions.append(self.cookies.get("web_session"))
self.cookies["web_session"] = "real-session"
self.cookies["web_session_sec"] = "real-sec"
return {
"code_status": 2,
"login_info": {
"user_id": "real-user",
"session": "real-session",
"secure_session": "real-sec",
},
}
def get_self_info(self):
self.self_info_calls += 1
return {
"user_id": "real-user",
"basic_info": {
"user_id": "real-user",
"nickname": "Alice",
"red_id": "alice001",
},
}
class _MismatchQrClient(_FakeQrClient):
def complete_qr_login(self, qr_id, code):
self.complete_calls += 1
self.complete_seen_web_sessions.append(self.cookies.get("web_session"))
self.cookies["web_session"] = "wrong-session"
self.cookies["web_session_sec"] = "wrong-sec"
return {
"code_status": 2,
"login_info": {
"user_id": "guest-user",
"session": "wrong-session",
"secure_session": "wrong-sec",
},
}
def get_self_info(self):
self.self_info_calls += 1
return {
"user_id": "guest-user",
"basic_info": {
"user_id": "guest-user",
"nickname": "Guest",
"red_id": "",
},
}
class _SelfInfoFallbackQrClient(_FakeQrClient):
def complete_qr_login(self, qr_id, code):
self.complete_calls += 1
self.complete_seen_web_sessions.append(self.cookies.get("web_session"))
self.cookies["web_session"] = "real-session"
self.cookies["web_session_sec"] = "real-sec"
return {
"code_status": 2,
"login_info": {
"user_id": "guest-user",
"session": "real-session",
"secure_session": "real-sec",
},
}
def test_qrcode_login_completes_after_confirmation_and_saves_real_session(monkeypatch):
saved = []
monkeypatch.setattr("xhs_cli.qr_login.XhsClient", _FakeQrClient)
monkeypatch.setattr("xhs_cli.qr_login._generate_a1", lambda: "a1-fixed")
monkeypatch.setattr("xhs_cli.qr_login._generate_webid", lambda: "webid-fixed")
monkeypatch.setattr("xhs_cli.qr_login._display_qr_in_terminal", lambda data: True)
monkeypatch.setattr("xhs_cli.qr_login.time.sleep", lambda seconds: None)
monkeypatch.setattr("xhs_cli.qr_login.save_cookies", lambda cookies: saved.append(cookies))
cookies = qrcode_login(timeout_s=1)
client = _FakeQrClient.instances[-1]
assert client.create_seen_web_session == "guest-session"
assert client.status_seen_web_session == "guest-session"
assert client.activate_calls == 1
assert client.complete_calls == 1
assert client.complete_seen_web_sessions == ["guest-session"]
assert cookies == {
"a1": "a1-fixed",
"webId": "webid-fixed",
"web_session": "real-session",
"web_session_sec": "real-sec",
}
assert saved == [cookies]
def test_qrcode_login_accepts_confirmed_user_from_self_info_fallback(monkeypatch):
saved = []
monkeypatch.setattr("xhs_cli.qr_login.XhsClient", _SelfInfoFallbackQrClient)
monkeypatch.setattr("xhs_cli.qr_login._generate_a1", lambda: "a1-fixed")
monkeypatch.setattr("xhs_cli.qr_login._generate_webid", lambda: "webid-fixed")
monkeypatch.setattr("xhs_cli.qr_login._display_qr_in_terminal", lambda data: True)
monkeypatch.setattr("xhs_cli.qr_login.time.sleep", lambda seconds: None)
monkeypatch.setattr("xhs_cli.qr_login.save_cookies", lambda cookies: saved.append(cookies))
cookies = qrcode_login(timeout_s=1)
client = _SelfInfoFallbackQrClient.instances[-1]
assert client.activate_calls == 1
assert client.complete_calls == 1
assert client.self_info_calls >= 1
assert cookies == {
"a1": "a1-fixed",
"webId": "webid-fixed",
"web_session": "real-session",
"web_session_sec": "real-sec",
}
assert saved == [cookies]
def test_qrcode_login_rejects_mismatched_confirmed_user(monkeypatch):
monkeypatch.setattr("xhs_cli.qr_login.XhsClient", _MismatchQrClient)
monkeypatch.setattr("xhs_cli.qr_login._generate_a1", lambda: "a1-fixed")
monkeypatch.setattr("xhs_cli.qr_login._generate_webid", lambda: "webid-fixed")
monkeypatch.setattr("xhs_cli.qr_login._display_qr_in_terminal", lambda data: True)
monkeypatch.setattr("xhs_cli.qr_login.time.sleep", lambda seconds: None)
monkeypatch.setattr("xhs_cli.qr_login.save_cookies", lambda cookies: None)
with pytest.raises(XhsApiError, match="completion never returned"):
qrcode_login(timeout_s=1)
def test_qrcode_login_prefers_browser_assisted_backend(monkeypatch):
saved = []
monkeypatch.setattr(
"xhs_cli.qr_login._browser_assisted_qrcode_login",
lambda **kwargs: {
"a1": "a1-browser",
"webId": "webid-browser",
"web_session": "0400-browser",
"web_session_sec": "secure-browser",
"id_token": "token-browser",
},
)
cookies = qrcode_login(timeout_s=1, prefer_browser_assisted=True)
assert cookies["web_session"] == "0400-browser"
assert cookies["id_token"] == "token-browser"
assert saved == []
def test_qrcode_login_falls_back_when_browser_backend_unavailable(monkeypatch):
monkeypatch.setattr(
"xhs_cli.qr_login._browser_assisted_qrcode_login",
lambda **kwargs: (_ for _ in ()).throw(BrowserQrLoginUnavailable("missing camoufox")),
)
monkeypatch.setattr(
"xhs_cli.qr_login._http_qrcode_login",
lambda **kwargs: {
"a1": "a1-http",
"webId": "webid-http",
"web_session": "http-session",
},
)
cookies = qrcode_login(timeout_s=1, prefer_browser_assisted=True)
assert cookies == {
"a1": "a1-http",
"webId": "webid-http",
"web_session": "http-session",
}
def test_normalize_browser_cookies_uses_allowlist():
cookies = _normalize_browser_cookies([
{"name": "a1", "value": "a1-value", "domain": ".xiaohongshu.com"},
{"name": "web_session", "value": "session-value", "domain": ".xiaohongshu.com"},
{"name": "customer-sso-sid", "value": "skip-me", "domain": ".xiaohongshu.com"},
{"name": "creator_only", "value": "skip-me-too", "domain": "creator.xiaohongshu.com"},
])
assert cookies == {
"a1": "a1-value",
"web_session": "session-value",
}
def test_normalize_xhs_user_payload_reads_basic_info():
user = normalize_xhs_user_payload({
"guest": False,
"basic_info": {
"user_id": "user-1",
"nickname": "Alice",
"red_id": "alice001",
"ip_location": "上海",
"desc": "hello",
},
})
assert user == {
"id": "user-1",
"name": "Alice",
"username": "alice001",
"nickname": "Alice",
"red_id": "alice001",
"ip_location": "上海",
"desc": "hello",
"guest": False,
}