refactor: unify exception handling, add ISO 8601 time, dedup commands, expand tests

- Replace _error_code_for_message() string matching with error_code attribute on exception classes
- Add error_code to all TwitterError subclasses (AuthenticationError, RateLimitError, etc.)
- Add InvalidInputError exception class
- TwitterAPIError derives error_code from HTTP status code automatically
- auth.py: use AuthenticationError instead of RuntimeError
- cli.py: catch (TwitterError, RuntimeError) for backward compat
- Extract _fetch_and_display_users() to deduplicate followers/following commands
- Add format_iso8601() to timeutil.py
- Add createdAtISO field to tweet and user profile serialization
- New test files: test_output.py, test_cache.py, test_timeutil.py
- Expand test_filter.py (topN, score mode, custom weights, empty input)
- Tests: 152 → 194 unit tests, all passing
This commit is contained in:
jackwener
2026-03-16 18:24:35 +08:00
parent 0b91e66998
commit e496d8f870
11 changed files with 482 additions and 73 deletions

View File

@@ -1,6 +1,8 @@
"""Tests for twitter_cli.filter module."""
from __future__ import annotations
from twitter_cli.filter import filter_tweets
from twitter_cli.filter import filter_tweets, score_tweet
def test_filter_tweets_does_not_mutate_input(tweet_factory) -> None:
@@ -29,3 +31,58 @@ def test_filter_tweets_applies_language_and_retweet_filters(tweet_factory) -> No
)
assert [tweet.id for tweet in output] == ["1"]
def test_filter_topn_mode(tweet_factory) -> None:
tweets = [tweet_factory(str(i)) for i in range(10)]
output = filter_tweets(tweets, {"mode": "topN", "topN": 3})
assert len(output) == 3
def test_filter_topn_default(tweet_factory) -> None:
"""Default topN is 20, so 5 tweets should all be returned."""
tweets = [tweet_factory(str(i)) for i in range(5)]
output = filter_tweets(tweets, {"mode": "topN"})
assert len(output) == 5
def test_filter_score_mode(tweet_factory) -> None:
from twitter_cli.models import Metrics
tweets = [
tweet_factory("high", metrics=Metrics(likes=1000, retweets=500, replies=200, views=100000, bookmarks=50)),
tweet_factory("low", metrics=Metrics(likes=0, retweets=0, replies=0, views=1, bookmarks=0)),
]
output = filter_tweets(tweets, {"mode": "score", "minScore": 100.0})
assert len(output) == 1
assert output[0].id == "high"
def test_filter_empty_input() -> None:
output = filter_tweets([], {"mode": "all"})
assert output == []
def test_score_tweet_basic(tweet_factory) -> None:
tweet = tweet_factory("1")
score = score_tweet(tweet)
assert isinstance(score, float)
assert score > 0
def test_score_tweet_custom_weights(tweet_factory) -> None:
tweet = tweet_factory("1")
weights = {"likes": 0, "retweets": 0, "replies": 0, "bookmarks": 0, "views_log": 0}
assert score_tweet(tweet, weights) == 0.0
def test_filter_all_mode_sorts_by_score(tweet_factory) -> None:
from twitter_cli.models import Metrics
tweets = [
tweet_factory("low", metrics=Metrics(likes=1)),
tweet_factory("high", metrics=Metrics(likes=100)),
]
output = filter_tweets(tweets, {"mode": "all"})
assert output[0].id == "high"
assert output[1].id == "low"