fix: harden pagination auth and runtime headers

This commit is contained in:
jackwener
2026-03-10 12:33:04 +08:00
parent 4f144d1591
commit d71ad45a0a
8 changed files with 256 additions and 70 deletions

View File

@@ -125,7 +125,7 @@ def test_extract_via_subprocess_script_includes_arc(monkeypatch) -> None:
seen = {}
def _run(cmd, capture_output=True, text=True, timeout=15):
script = cmd[2]
script = cmd[-1]
seen["script"] = script
return Completed(json.dumps({"error": "No Twitter cookies found", "attempts": []}))
@@ -137,6 +137,29 @@ def test_extract_via_subprocess_script_includes_arc(monkeypatch) -> None:
assert '("arc", browser_cookie3.arc)' in seen["script"]
def test_extract_via_subprocess_retries_uv_when_current_env_has_no_output(monkeypatch) -> None:
class Completed:
def __init__(self, stdout: str, stderr: str = "") -> None:
self.stdout = stdout
self.stderr = stderr
calls = []
def _run(cmd, capture_output=True, text=True, timeout=15):
calls.append(cmd)
if cmd[0] == sys.executable:
return Completed("", "")
return Completed(json.dumps({"auth_token": "token", "ct0": "csrf", "browser": "arc"}))
monkeypatch.setattr(auth.subprocess, "run", _run)
cookies = auth._extract_via_subprocess()
assert cookies == {"auth_token": "token", "ct0": "csrf"}
assert len(calls) == 2
assert calls[1][:5] == ["uv", "run", "--with", "browser-cookie3", "python"]
def test_verify_cookies_logs_attempt_summary_on_non_auth_failures(monkeypatch, caplog) -> None:
class Response:
def __init__(self, status_code: int, payload=None) -> None:

View File

@@ -275,9 +275,19 @@ class TestBuildHeaders:
assert "User-Agent" in headers
assert "sec-ch-ua" in headers
@patch("twitter_cli.client.get_sec_ch_ua_platform", return_value='"Linux"')
@patch("twitter_cli.client.get_accept_language", return_value="zh-CN,zh;q=0.9,en;q=0.8")
@patch("twitter_cli.client.get_twitter_client_language", return_value="zh")
@patch("twitter_cli.client._get_cffi_session")
@patch("twitter_cli.client._gen_ct_headers", return_value={})
def test_cookie_string_used_when_available(self, mock_ct_headers, mock_session):
def test_cookie_string_used_when_available(
self,
mock_ct_headers,
mock_session,
mock_client_language,
mock_accept_language,
mock_platform,
):
mock_session.return_value = MagicMock()
mock_session.return_value.get = MagicMock(side_effect=Exception("skip"))
@@ -294,6 +304,57 @@ class TestBuildHeaders:
headers = client._build_headers()
assert headers["Cookie"] == "auth_token=x; ct0=y; other=z"
assert headers["X-Twitter-Client-Language"] == "zh"
assert headers["Accept-Language"] == "zh-CN,zh;q=0.9,en;q=0.8"
assert headers["sec-ch-ua-platform"] == '"Linux"'
class TestPaginationBehavior:
def test_continues_when_cursor_advances_without_new_tweets(self):
client = TwitterClient.__new__(TwitterClient)
client._request_delay = 0.0
client._max_count = 200
responses = iter(
[
{"page": 1},
{"page": 2},
]
)
def _graphql_get(operation_name, variables, features, field_toggles=None):
return next(responses)
def _parse_timeline_response(data, get_instructions):
if data["page"] == 1:
return [], "cursor-2"
return [MagicMock(id="tweet-1")], None
client._graphql_get = _graphql_get
client._parse_timeline_response = _parse_timeline_response
tweets = client._fetch_timeline("HomeTimeline", 1, lambda data: data)
assert [tweet.id for tweet in tweets] == ["tweet-1"]
def test_stops_when_cursor_does_not_advance(self):
client = TwitterClient.__new__(TwitterClient)
client._request_delay = 0.0
client._max_count = 200
calls = []
def _graphql_get(operation_name, variables, features, field_toggles=None):
calls.append(variables.get("cursor"))
return {"page": len(calls)}
client._graphql_get = _graphql_get
client._parse_timeline_response = lambda data, get_instructions: ([], "cursor-same")
tweets = client._fetch_timeline("HomeTimeline", 1, lambda data: data)
assert tweets == []
assert calls == [None, "cursor-same"]
# ── TwitterClient._parse_tweet_result ─────────────────────────────────────
@@ -407,3 +468,26 @@ class TestTwitterAPIError:
def test_is_runtime_error(self):
err = TwitterAPIError(500, "Server error")
assert isinstance(err, RuntimeError)
class TestParseUserResult:
def test_coerces_count_fields_to_int(self):
user = TwitterClient._parse_user_result(
{
"rest_id": "user-1",
"legacy": {
"name": "Alice",
"screen_name": "alice",
"followers_count": "1,234",
"friends_count": "56",
"statuses_count": "78.9",
"favourites_count": None,
},
}
)
assert user is not None
assert user.followers_count == 1234
assert user.following_count == 56
assert user.tweets_count == 78
assert user.likes_count == 0