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
25 changes: 25 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,31 @@ def test_cli_user_error_yaml(monkeypatch) -> None:
assert payload["error"]["code"] == "not_found"


def test_cli_tweet_reports_missing_focal_tweet_as_structured_error(monkeypatch) -> None:
from twitter_cli.exceptions import NotFoundError

class FakeClient:
def fetch_tweet_detail(self, tweet_id: str, max_count: int):
raise NotFoundError(
"Tweet %s was not returned by the TweetDetail response" % tweet_id
)

monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None, quiet=False: FakeClient())
monkeypatch.setattr(
"twitter_cli.cli.load_config",
lambda: {"fetch": {"count": 50}, "filter": {}, "rateLimit": {}},
)
runner = CliRunner()

result = runner.invoke(cli, ["tweet", "12345", "--json"])

assert result.exit_code == 1
payload = json.loads(result.output)
assert payload["ok"] is False
assert payload["error"]["code"] == "not_found"
assert "TweetDetail response" in payload["error"]["message"]


def test_cli_tweet_accepts_shared_url_with_query(monkeypatch) -> None:
class FakeClient:
def fetch_tweet_detail(self, tweet_id: str, max_count: int):
Expand Down
24 changes: 23 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
_best_chrome_target,
TwitterClient,
)
from twitter_cli.exceptions import TwitterAPIError
from twitter_cli.exceptions import NotFoundError, TwitterAPIError
from twitter_cli.graphql import (
FEATURES,
FALLBACK_QUERY_IDS,
Expand Down Expand Up @@ -332,6 +332,28 @@ def test_cookie_string_used_when_available(
assert headers["sec-ch-ua-platform-version"] == '""'


class TestTweetDetailFetch:
def test_raises_when_focal_tweet_is_missing(self):
client = TwitterClient.__new__(TwitterClient)
client._fetch_timeline = MagicMock(return_value=[MagicMock(id="reply-1")])

with pytest.raises(
NotFoundError,
match="Tweet 123 was not returned by the TweetDetail response",
):
client.fetch_tweet_detail("123", 20)

def test_places_focal_tweet_before_replies(self):
client = TwitterClient.__new__(TwitterClient)
reply = MagicMock(id="reply-1")
focal_tweet = MagicMock(id="123")
client._fetch_timeline = MagicMock(return_value=[reply, focal_tweet])

tweets = client.fetch_tweet_detail("123", 20)

assert tweets == [focal_tweet, reply]


class TestPaginationBehavior:
def test_fetch_timeline_can_include_promoted_content(self):
client = TwitterClient.__new__(TwitterClient)
Expand Down
8 changes: 7 additions & 1 deletion twitter_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def fetch_search(self, query, count=20, product="Top"):
def fetch_tweet_detail(self, tweet_id, count=20):
# type: (str, int) -> List[Tweet]
"""Fetch a tweet and its conversation thread (replies)."""
return self._fetch_timeline(
tweets = self._fetch_timeline(
"TweetDetail",
count,
lambda data: _deep_get(data, "data", "tweetResult", "result", "timeline", "instructions")
Expand All @@ -397,6 +397,12 @@ def fetch_tweet_detail(self, tweet_id, count=20):
"withDisallowedReplyControls": False,
},
)
focal_tweet = next((tweet for tweet in tweets if tweet.id == tweet_id), None)
if focal_tweet is None:
raise NotFoundError(
"Tweet %s was not returned by the TweetDetail response" % tweet_id
)
return [focal_tweet] + [tweet for tweet in tweets if tweet.id != tweet_id]

def fetch_article(self, tweet_id):
# type: (str) -> Tweet
Expand Down
Loading