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

73
tests/test_timeutil.py Normal file
View File

@@ -0,0 +1,73 @@
"""Tests for twitter_cli.timeutil module."""
from __future__ import annotations
from twitter_cli.timeutil import format_iso8601, format_local_time, format_relative_time
SAMPLE_TIMESTAMP = "Sat Mar 08 12:00:00 +0000 2026"
# ── format_local_time ────────────────────────────────────────────────────
def test_format_local_time_valid() -> None:
result = format_local_time(SAMPLE_TIMESTAMP)
# Should be in YYYY-MM-DD HH:MM format (local timezone)
assert result.startswith("2026-03-")
assert ":" in result
def test_format_local_time_empty() -> None:
assert format_local_time("") == ""
def test_format_local_time_invalid() -> None:
assert format_local_time("not a date") == "not a date"
# ── format_relative_time ─────────────────────────────────────────────────
def test_format_relative_time_old() -> None:
# A timestamp from 2020 should show years ago
old_ts = "Sat Jan 01 00:00:00 +0000 2020"
result = format_relative_time(old_ts)
assert result.endswith("ago")
assert "y" in result or "mo" in result or "d" in result
def test_format_relative_time_empty() -> None:
assert format_relative_time("") == ""
def test_format_relative_time_invalid() -> None:
assert format_relative_time("garbage") == "garbage"
# ── format_iso8601 ───────────────────────────────────────────────────────
def test_format_iso8601_valid() -> None:
result = format_iso8601(SAMPLE_TIMESTAMP)
assert result.startswith("2026-03-08T12:00:00")
assert "+00:00" in result or "Z" in result
def test_format_iso8601_empty() -> None:
assert format_iso8601("") == ""
def test_format_iso8601_invalid() -> None:
assert format_iso8601("not a date") == "not a date"
def test_format_iso8601_roundtrip() -> None:
"""ISO 8601 output should be parseable by datetime.fromisoformat."""
from datetime import datetime
result = format_iso8601(SAMPLE_TIMESTAMP)
dt = datetime.fromisoformat(result)
assert dt.year == 2026
assert dt.month == 3
assert dt.day == 8