Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1545,3 +1545,54 @@ def mock_post(operation_name, variables, features=None):

assert captured.get("product") == "Latest"
assert captured.get("querySource") == "typed_query"


# ── TwitterClient._ensure_client_transaction ─────────────────────────────

class TestEnsureClientTransaction:
"""The x.com fetch must be authenticated.

Anonymous requests receive the logged-out landing page, which carries no
ondemand.s marker, so get_ondemand_file_url() fails and every subsequent
API call goes out without an x-client-transaction-id header.
"""

def _make_client(self):
client = TwitterClient.__new__(TwitterClient)
client._auth_token = "test_token"
client._ct0 = "test_ct0"
client._cookie_string = None
client._client_transaction = None
client._ct_init_attempted = False
return client

@patch("twitter_cli.client._gen_ct_headers", return_value={})
@patch("twitter_cli.client._get_cffi_session")
def test_homepage_fetch_sends_cookies(self, mock_session, mock_ct_headers):
session = MagicMock()
session.get = MagicMock(side_effect=Exception("stop after first fetch"))
mock_session.return_value = session

client = self._make_client()
client._load_ct_cache = lambda: False
client._ensure_client_transaction()

_, kwargs = session.get.call_args
cookie = kwargs["headers"]["Cookie"]
assert "auth_token=test_token" in cookie
assert "ct0=test_ct0" in cookie

@patch("twitter_cli.client._gen_ct_headers", return_value={})
@patch("twitter_cli.client._get_cffi_session")
def test_full_cookie_string_preferred(self, mock_session, mock_ct_headers):
session = MagicMock()
session.get = MagicMock(side_effect=Exception("stop after first fetch"))
mock_session.return_value = session

client = self._make_client()
client._cookie_string = "auth_token=a; ct0=b; guest_id=c"
client._load_ct_cache = lambda: False
client._ensure_client_transaction()

_, kwargs = session.get.call_args
assert kwargs["headers"]["Cookie"] == "auth_token=a; ct0=b; guest_id=c"
7 changes: 7 additions & 0 deletions twitter_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,13 @@ def _ensure_client_transaction(self):
# a different TLS fingerprint on the same IP — a detection vector.
cffi_session = _get_cffi_session()
ct_headers = _gen_ct_headers()
# x.com serves the logged-out landing page to anonymous requests,
# and that page contains no ondemand.s marker for
# get_ondemand_file_url() to match. Send the session cookies so we
# receive the authenticated app shell instead.
ct_headers["Cookie"] = self._cookie_string or (
"auth_token=%s; ct0=%s" % (self._auth_token, self._ct0)
)
home_page = cffi_session.get(
"https://x.com", headers=ct_headers, timeout=10,
)
Expand Down