- 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
89 lines
2.7 KiB
Python
89 lines
2.7 KiB
Python
"""Tests for twitter_cli.filter module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from twitter_cli.filter import filter_tweets, score_tweet
|
|
|
|
|
|
def test_filter_tweets_does_not_mutate_input(tweet_factory) -> None:
|
|
tweet = tweet_factory("1", score=0.0)
|
|
output = filter_tweets([tweet], {"mode": "all", "weights": {}})
|
|
|
|
assert tweet.score == 0.0
|
|
assert output[0].score > 0.0
|
|
assert output[0] is not tweet
|
|
|
|
|
|
def test_filter_tweets_applies_language_and_retweet_filters(tweet_factory) -> None:
|
|
tweets = [
|
|
tweet_factory("1", lang="en", is_retweet=False),
|
|
tweet_factory("2", lang="zh", is_retweet=False),
|
|
tweet_factory("3", lang="en", is_retweet=True),
|
|
]
|
|
output = filter_tweets(
|
|
tweets,
|
|
{
|
|
"mode": "all",
|
|
"lang": ["en"],
|
|
"excludeRetweets": True,
|
|
"weights": {},
|
|
},
|
|
)
|
|
|
|
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"
|