Files
twitter-cli-cookiefile/tests/test_auth.py
2026-03-10 12:33:04 +08:00

190 lines
6.2 KiB
Python

from __future__ import annotations
import json
import sys
from types import SimpleNamespace
import pytest
from twitter_cli import auth
def test_get_cookies_prefers_env(monkeypatch) -> None:
monkeypatch.setattr(auth, "load_from_env", lambda: {"auth_token": "env-token", "ct0": "env-csrf"})
monkeypatch.setattr(auth, "extract_from_browser", lambda: pytest.fail("should not extract from browser"))
seen = []
monkeypatch.setattr(
auth,
"verify_cookies",
lambda auth_token, ct0, cookie_string=None: seen.append((auth_token, ct0, cookie_string)) or {},
)
cookies = auth.get_cookies()
assert cookies == {"auth_token": "env-token", "ct0": "env-csrf"}
assert seen == [("env-token", "env-csrf", None)]
def test_get_cookies_reextracts_after_verify_failure(monkeypatch) -> None:
monkeypatch.setattr(auth, "load_from_env", lambda: None)
extracted = iter(
[
{"auth_token": "stale-token", "ct0": "stale-csrf", "cookie_string": "stale=1"},
{"auth_token": "fresh-token", "ct0": "fresh-csrf", "cookie_string": "fresh=1"},
]
)
monkeypatch.setattr(auth, "extract_from_browser", lambda: next(extracted))
calls = []
def _verify(auth_token, ct0, cookie_string=None):
calls.append((auth_token, ct0, cookie_string))
if auth_token == "stale-token":
raise RuntimeError("expired")
return {}
monkeypatch.setattr(auth, "verify_cookies", _verify)
cookies = auth.get_cookies()
assert cookies["auth_token"] == "fresh-token"
assert calls == [
("stale-token", "stale-csrf", "stale=1"),
("fresh-token", "fresh-csrf", "fresh=1"),
]
def test_load_from_env_logs_incomplete_env(monkeypatch, caplog) -> None:
monkeypatch.setenv("TWITTER_AUTH_TOKEN", "token")
monkeypatch.delenv("TWITTER_CT0", raising=False)
with caplog.at_level("DEBUG"):
cookies = auth.load_from_env()
assert cookies is None
assert "Environment cookies incomplete" in caplog.text
def test_extract_cookies_from_jar_logs_missing_required_cookies(caplog) -> None:
class Cookie:
def __init__(self, domain: str, name: str, value: str) -> None:
self.domain = domain
self.name = name
self.value = value
jar = [Cookie(".x.com", "auth_token", "token")]
with caplog.at_level("DEBUG"):
cookies = auth._extract_cookies_from_jar(jar, source="test-jar")
assert cookies is None
assert "test-jar" in caplog.text
assert "ct0=False" in caplog.text
def test_extract_from_browser_logs_warning_when_all_methods_fail(monkeypatch, caplog) -> None:
monkeypatch.setattr(auth, "_extract_in_process", lambda: None)
monkeypatch.setattr(auth, "_extract_via_subprocess", lambda: None)
with caplog.at_level("WARNING"):
cookies = auth.extract_from_browser()
assert cookies is None
assert "Twitter cookie extraction failed in both in-process and subprocess modes" in caplog.text
def test_extract_in_process_supports_arc(monkeypatch) -> None:
class Cookie:
def __init__(self, domain: str, name: str, value: str) -> None:
self.domain = domain
self.name = name
self.value = value
fake_module = SimpleNamespace(
arc=lambda: [Cookie(".x.com", "auth_token", "token"), Cookie(".x.com", "ct0", "csrf")],
chrome=lambda: pytest.fail("chrome should not be used when arc succeeds"),
edge=lambda: pytest.fail("edge should not be used when arc succeeds"),
firefox=lambda: pytest.fail("firefox should not be used when arc succeeds"),
brave=lambda: pytest.fail("brave should not be used when arc succeeds"),
)
monkeypatch.setitem(sys.modules, "browser_cookie3", fake_module)
cookies = auth._extract_in_process()
assert cookies is not None
assert cookies["auth_token"] == "token"
assert cookies["ct0"] == "csrf"
def test_extract_via_subprocess_script_includes_arc(monkeypatch) -> None:
class Completed:
def __init__(self, stdout: str, stderr: str = "") -> None:
self.stdout = stdout
self.stderr = stderr
seen = {}
def _run(cmd, capture_output=True, text=True, timeout=15):
script = cmd[-1]
seen["script"] = script
return Completed(json.dumps({"error": "No Twitter cookies found", "attempts": []}))
monkeypatch.setattr(auth.subprocess, "run", _run)
cookies = auth._extract_via_subprocess()
assert cookies is 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:
self.status_code = status_code
self._payload = payload or {}
def json(self):
return self._payload
class Session:
def __init__(self) -> None:
self.calls = 0
def get(self, url, headers=None, timeout=5):
self.calls += 1
if self.calls == 1:
return Response(404)
raise Exception("network")
monkeypatch.setattr("twitter_cli.client._get_cffi_session", lambda: Session())
with caplog.at_level("INFO"):
result = auth.verify_cookies("token", "csrf")
assert result == {}
assert "verify_credentials.json=404" in caplog.text
assert "settings.json=Exception" in caplog.text