feat: add advanced search options (--from, --to, --lang, --since, --until, --has, --exclude, --min-likes, --min-retweets)
Closes #17 - New search.py query builder module - QUERY argument now optional when using advanced filters - 21 unit tests + 3 CLI integration tests for search - Bumped version to 0.7.0
This commit is contained in:
@@ -425,6 +425,70 @@ def test_cli_unfollow_command(monkeypatch) -> None:
|
||||
assert actions == [("unfollow", "42")]
|
||||
|
||||
|
||||
def test_cli_search_advanced_options(monkeypatch) -> None:
|
||||
captured = {}
|
||||
|
||||
class FakeClient:
|
||||
def fetch_search(self, query: str, count: int, product: str):
|
||||
captured["query"] = query
|
||||
captured["product"] = product
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None: FakeClient())
|
||||
monkeypatch.setattr(
|
||||
"twitter_cli.cli.load_config",
|
||||
lambda: {"fetch": {"count": 50}, "filter": {}, "rateLimit": {}},
|
||||
)
|
||||
runner = CliRunner()
|
||||
|
||||
result = runner.invoke(cli, [
|
||||
"search", "python",
|
||||
"--from", "elonmusk",
|
||||
"--lang", "en",
|
||||
"--since", "2026-01-01",
|
||||
"--has", "links",
|
||||
"--exclude", "retweets",
|
||||
"--min-likes", "100",
|
||||
"-t", "Latest",
|
||||
"--json",
|
||||
])
|
||||
|
||||
assert result.exit_code == 0, f"search failed: {result.output}"
|
||||
assert captured["query"] == (
|
||||
"python from:elonmusk lang:en since:2026-01-01 "
|
||||
"filter:links -filter:retweets min_faves:100"
|
||||
)
|
||||
assert captured["product"] == "Latest"
|
||||
|
||||
|
||||
def test_cli_search_operators_only_no_query(monkeypatch) -> None:
|
||||
captured = {}
|
||||
|
||||
class FakeClient:
|
||||
def fetch_search(self, query: str, count: int, product: str):
|
||||
captured["query"] = query
|
||||
return []
|
||||
|
||||
monkeypatch.setattr("twitter_cli.cli._get_client", lambda config=None: FakeClient())
|
||||
monkeypatch.setattr(
|
||||
"twitter_cli.cli.load_config",
|
||||
lambda: {"fetch": {"count": 50}, "filter": {}, "rateLimit": {}},
|
||||
)
|
||||
runner = CliRunner()
|
||||
|
||||
result = runner.invoke(cli, ["search", "--from", "bbc", "--json"])
|
||||
assert result.exit_code == 0, f"search failed: {result.output}"
|
||||
assert captured["query"] == "from:bbc"
|
||||
|
||||
|
||||
def test_cli_search_empty_query_no_options() -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
result = runner.invoke(cli, ["search"])
|
||||
assert result.exit_code != 0
|
||||
assert "Provide a QUERY" in result.output
|
||||
|
||||
|
||||
def test_cli_compact_mode(tmp_path, tweet_factory) -> None:
|
||||
json_path = tmp_path / "tweets.json"
|
||||
json_path.write_text(tweets_to_json([tweet_factory("1")]), encoding="utf-8")
|
||||
|
||||
86
tests/test_search.py
Normal file
86
tests/test_search.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Unit tests for the advanced search query builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from twitter_cli.search import build_search_query
|
||||
|
||||
|
||||
class TestBuildSearchQuery:
|
||||
def test_plain_query(self) -> None:
|
||||
assert build_search_query("python") == "python"
|
||||
|
||||
def test_empty_query(self) -> None:
|
||||
assert build_search_query("") == ""
|
||||
|
||||
def test_from_user(self) -> None:
|
||||
assert build_search_query("AI", from_user="elonmusk") == "AI from:elonmusk"
|
||||
|
||||
def test_from_user_strips_at(self) -> None:
|
||||
assert build_search_query("AI", from_user="@elonmusk") == "AI from:elonmusk"
|
||||
|
||||
def test_to_user(self) -> None:
|
||||
assert build_search_query("hello", to_user="jack") == "hello to:jack"
|
||||
|
||||
def test_lang(self) -> None:
|
||||
assert build_search_query("news", lang="fr") == "news lang:fr"
|
||||
|
||||
def test_since(self) -> None:
|
||||
assert build_search_query("python", since="2026-01-01") == "python since:2026-01-01"
|
||||
|
||||
def test_until(self) -> None:
|
||||
assert build_search_query("python", until="2026-03-01") == "python until:2026-03-01"
|
||||
|
||||
def test_date_range(self) -> None:
|
||||
result = build_search_query("rust", since="2026-01-01", until="2026-03-01")
|
||||
assert result == "rust since:2026-01-01 until:2026-03-01"
|
||||
|
||||
def test_has_links(self) -> None:
|
||||
assert build_search_query("python", has=["links"]) == "python filter:links"
|
||||
|
||||
def test_has_multiple(self) -> None:
|
||||
result = build_search_query("art", has=["images", "videos"])
|
||||
assert result == "art filter:images filter:videos"
|
||||
|
||||
def test_exclude_retweets(self) -> None:
|
||||
assert build_search_query("news", exclude=["retweets"]) == "news -filter:retweets"
|
||||
|
||||
def test_exclude_replies(self) -> None:
|
||||
assert build_search_query("news", exclude=["replies"]) == "news -filter:replies"
|
||||
|
||||
def test_exclude_multiple(self) -> None:
|
||||
result = build_search_query("news", exclude=["retweets", "replies"])
|
||||
assert result == "news -filter:retweets -filter:replies"
|
||||
|
||||
def test_min_likes(self) -> None:
|
||||
assert build_search_query("python", min_likes=100) == "python min_faves:100"
|
||||
|
||||
def test_min_retweets(self) -> None:
|
||||
assert build_search_query("python", min_retweets=50) == "python min_retweets:50"
|
||||
|
||||
def test_combined_operators(self) -> None:
|
||||
result = build_search_query(
|
||||
"machine learning",
|
||||
from_user="openai",
|
||||
lang="en",
|
||||
since="2026-01-01",
|
||||
has=["links"],
|
||||
min_likes=50,
|
||||
exclude=["retweets"],
|
||||
)
|
||||
assert result == (
|
||||
"machine learning from:openai lang:en since:2026-01-01 "
|
||||
"filter:links -filter:retweets min_faves:50"
|
||||
)
|
||||
|
||||
def test_operators_only_no_query(self) -> None:
|
||||
result = build_search_query("", from_user="elonmusk", since="2026-03-01")
|
||||
assert result == "from:elonmusk since:2026-03-01"
|
||||
|
||||
def test_whitespace_query_trimmed(self) -> None:
|
||||
assert build_search_query(" python ", lang="en") == "python lang:en"
|
||||
|
||||
def test_empty_has_list(self) -> None:
|
||||
assert build_search_query("test", has=[]) == "test"
|
||||
|
||||
def test_empty_exclude_list(self) -> None:
|
||||
assert build_search_query("test", exclude=[]) == "test"
|
||||
Reference in New Issue
Block a user