- Storage: Change from per-user to per-group JSON files
- Data location: ~/.jigaido/ instead of apps/telegram-bot/data/
- Group bounties: data/{group_id}/group.json
- User tracking: data/{group_id}/{user_id}.json
- Personal bounties: data/{user_id}/user.json
- Update commands.py for new storage model
- Update bot.py to remove admin handlers
- Update tests to reflect created_by_user_id field
- Update SPEC.md with new design
Addresses user feedback from issue #2
171 lines
5.4 KiB
Python
171 lines
5.4 KiB
Python
"""Tests for commands.py — parsing and formatting functions only."""
|
|
|
|
import time
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from commands import extract_args, parse_args, format_bounty
|
|
|
|
|
|
class TestExtractArgs:
|
|
def test_basic_split(self):
|
|
assert extract_args("/add hello world") == ["hello", "world"]
|
|
assert extract_args("/track 42") == ["42"]
|
|
|
|
def test_no_args(self):
|
|
assert extract_args("/bounty") == []
|
|
assert extract_args("/add") == []
|
|
|
|
def test_preserves_urls(self):
|
|
result = extract_args("/add check https://example.com stuff")
|
|
assert result == ["check", "https://example.com", "stuff"]
|
|
|
|
def test_none_input(self):
|
|
assert extract_args(None) == []
|
|
|
|
def test_strips_whitespace(self):
|
|
assert extract_args(" /add hello world ") == ["hello", "world"]
|
|
|
|
|
|
class TestParseArgs:
|
|
def test_text_only(self):
|
|
text, link, due = parse_args(["hello", "world"])
|
|
assert text == "hello world"
|
|
assert link is None
|
|
assert due is None
|
|
|
|
def test_link_extracted(self):
|
|
text, link, due = parse_args(["hello", "https://example.com"])
|
|
# "hello" is non-link non-date → becomes text; only the URL becomes link
|
|
assert text == "hello"
|
|
assert link == "https://example.com"
|
|
assert due is None
|
|
|
|
def test_text_and_link(self):
|
|
text, link, due = parse_args(["hello", "world", "https://example.com"])
|
|
assert text == "hello world"
|
|
assert link == "https://example.com"
|
|
|
|
def test_due_date_parsed(self):
|
|
text, link, due = parse_args(["hello", "tomorrow"])
|
|
assert text == "hello"
|
|
assert due is not None
|
|
# Should be some time in the future
|
|
assert due > int(time.time())
|
|
|
|
def test_all_three(self):
|
|
text, link, due = parse_args(["hello", "https://example.com", "tomorrow"])
|
|
assert text == "hello"
|
|
assert link == "https://example.com"
|
|
assert due is not None
|
|
|
|
def test_http_and_https_both_detected(self):
|
|
_, link1, _ = parse_args(["http://example.com"])
|
|
_, link2, _ = parse_args(["https://example.com"])
|
|
assert link1 == "http://example.com"
|
|
assert link2 == "https://example.com"
|
|
|
|
def test_non_url_non_date_becomes_text(self):
|
|
text, link, due = parse_args(["fix", "the", "bug"])
|
|
assert text == "fix the bug"
|
|
assert link is None
|
|
assert due is None
|
|
|
|
def test_multiple_links_first_only(self):
|
|
_, link, _ = parse_args(["text", "https://first.com", "https://second.com"])
|
|
assert link == "https://first.com"
|
|
|
|
def test_due_date_after_link(self):
|
|
text, link, due = parse_args(["task", "https://example.com", "in 5 days"])
|
|
assert text == "task"
|
|
assert link == "https://example.com"
|
|
assert due is not None
|
|
|
|
def test_empty_args(self):
|
|
text, link, due = parse_args([])
|
|
assert text is None
|
|
assert link is None
|
|
assert due is None
|
|
|
|
def test_date_parser_failure_returns_none(self):
|
|
# "asdfjkl" is not parseable → goes to text
|
|
text, link, due = parse_args(["hello", "asdfjkl"])
|
|
assert text == "hello asdfjkl"
|
|
assert due is None
|
|
|
|
def test_link_takes_first_match(self):
|
|
# Even if it's not a valid URL, starts with https://
|
|
_, link, _ = parse_args(["skip", "https://not-real.but-still-a-link"])
|
|
assert link == "https://not-real.but-still-a-link"
|
|
|
|
|
|
class TestFormatBounty:
|
|
def _row(
|
|
self,
|
|
id=1,
|
|
text="Test bounty",
|
|
link="https://example.com",
|
|
due_date_ts=None,
|
|
created_by_user_id=123456,
|
|
):
|
|
row = MagicMock()
|
|
row.__getitem__ = lambda s, k: {
|
|
"id": id,
|
|
"text": text,
|
|
"link": link,
|
|
"due_date_ts": due_date_ts,
|
|
"created_by_user_id": created_by_user_id,
|
|
}[k]
|
|
return row
|
|
|
|
def test_shows_id(self):
|
|
b = self._row(id=42)
|
|
out = format_bounty(b, show_id=True)
|
|
assert "[#42]" in out
|
|
|
|
def test_hides_id_when_requested(self):
|
|
b = self._row(id=42)
|
|
out = format_bounty(b, show_id=False)
|
|
assert "[#42]" not in out
|
|
|
|
def test_text_included(self):
|
|
b = self._row(text="Fix the login bug")
|
|
out = format_bounty(b)
|
|
assert "Fix the login bug" in out
|
|
|
|
def test_link_shown(self):
|
|
b = self._row(link="https://github.com/bob/repo")
|
|
out = format_bounty(b)
|
|
assert "https://github.com/bob/repo" in out
|
|
assert "🔗" in out
|
|
|
|
def test_no_link(self):
|
|
b = self._row(link=None)
|
|
out = format_bounty(b)
|
|
assert "🔗" not in out
|
|
|
|
def test_due_date_future(self):
|
|
future = int(time.time()) + 3 * 86400
|
|
b = self._row(due_date_ts=future)
|
|
out = format_bounty(b)
|
|
assert "⏰" in out
|
|
assert "3d" in out
|
|
|
|
def test_due_date_today(self):
|
|
today = int(time.time()) + 3600 # within today
|
|
b = self._row(due_date_ts=today)
|
|
out = format_bounty(b)
|
|
assert "TODAY" in out
|
|
|
|
def test_due_date_overdue(self):
|
|
past = int(time.time()) - 86400 # yesterday
|
|
b = self._row(due_date_ts=past)
|
|
out = format_bounty(b)
|
|
assert "OVERDUE" in out
|
|
|
|
def test_created_by_shown(self):
|
|
b = self._row(created_by_user_id=999)
|
|
out = format_bounty(b)
|
|
assert "999" in out
|