feat(cli): implement CLI for jigaido bounty tracker #32
0
cli/__init__.py
Normal file
0
cli/__init__.py
Normal file
251
cli/main.py
Normal file
251
cli/main.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""JIGAIDO CLI - Command line interface for bounty tracking."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import dateparser
|
||||
|
||||
from adapters.storage.json_file import JsonFileRoomStorage, JsonFileTrackingStorage
|
||||
from config import Config
|
||||
from core.services import BountyService, TrackingService
|
||||
|
||||
|
||||
def parse_due_date(due_str: str | None) -> int | None:
|
||||
"""Parse due date string to Unix timestamp."""
|
||||
if not due_str:
|
||||
return None
|
||||
parsed = dateparser.parse(due_str)
|
||||
if parsed:
|
||||
return int(parsed.timestamp())
|
||||
return None
|
||||
|
||||
|
||||
def create_services():
|
||||
"""Create service instances with storage."""
|
||||
config = Config()
|
||||
config.ensure_data_dir()
|
||||
room_storage = JsonFileRoomStorage(config.data_dir / "room")
|
||||
tracking_storage = JsonFileTrackingStorage(config.data_dir / "tracking")
|
||||
bounty_service = BountyService(room_storage)
|
||||
tracking_service = TrackingService(tracking_storage, room_storage)
|
||||
return bounty_service, tracking_service
|
||||
|
||||
|
||||
def cmd_add(args):
|
||||
"""Add a new bounty."""
|
||||
bounty_service, _ = create_services()
|
||||
due_ts = parse_due_date(args.due)
|
||||
bounty = bounty_service.add_bounty(
|
||||
room_id=args.group_id or args.user_id,
|
||||
user_id=args.user_id or 0,
|
||||
text=args.text,
|
||||
link=args.link,
|
||||
due_date_ts=due_ts,
|
||||
)
|
||||
print(f"Added bounty #{bounty.id}")
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
"""List all bounties in a room."""
|
||||
bounty_service, _ = create_services()
|
||||
bounties = bounty_service.list_bounties(room_id=args.group_id or args.user_id)
|
||||
if not bounties:
|
||||
print("No bounties")
|
||||
return
|
||||
for b in bounties:
|
||||
due_str = (
|
||||
f", due: {dateparser.parse(str(b.due_date_ts)).strftime('%Y-%m-%d')}"
|
||||
if b.due_date_ts
|
||||
else ""
|
||||
)
|
||||
link_str = f", link: {b.link}" if b.link else ""
|
||||
print(f"#{b.id}: {b.text or '(no text)'}{link_str}{due_str}")
|
||||
|
||||
|
||||
def cmd_my(args):
|
||||
"""List tracked bounties for a user."""
|
||||
_, tracking_service = create_services()
|
||||
room_id = args.group_id or args.user_id
|
||||
tracked = tracking_service.get_tracked_bounties(
|
||||
room_id=room_id, user_id=args.user_id
|
||||
)
|
||||
if not tracked:
|
||||
print("Not tracking any bounties")
|
||||
return
|
||||
for b in tracked:
|
||||
due_str = (
|
||||
f", due: {dateparser.parse(str(b.due_date_ts)).strftime('%Y-%m-%d')}"
|
||||
if b.due_date_ts
|
||||
else ""
|
||||
)
|
||||
link_str = f", link: {b.link}" if b.link else ""
|
||||
print(f"#{b.id}: {b.text or '(no text)'}{link_str}{due_str}")
|
||||
|
||||
|
||||
def cmd_update(args):
|
||||
"""Update a bounty."""
|
||||
bounty_service, _ = create_services()
|
||||
due_ts = parse_due_date(args.due)
|
||||
try:
|
||||
success = bounty_service.update_bounty(
|
||||
room_id=args.group_id or args.user_id,
|
||||
bounty_id=args.bounty_id,
|
||||
user_id=args.user_id or 0,
|
||||
text=args.text,
|
||||
link=args.link,
|
||||
due_date_ts=due_ts,
|
||||
clear_link=args.clear_link,
|
||||
clear_due=args.clear_due,
|
||||
)
|
||||
if success:
|
||||
print(f"Updated bounty #{args.bounty_id}")
|
||||
else:
|
||||
print(f"Bounty #{args.bounty_id} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except PermissionError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_delete(args):
|
||||
"""Delete a bounty."""
|
||||
bounty_service, _ = create_services()
|
||||
try:
|
||||
success = bounty_service.delete_bounty(
|
||||
room_id=args.group_id or args.user_id,
|
||||
bounty_id=args.bounty_id,
|
||||
user_id=args.user_id or 0,
|
||||
)
|
||||
if success:
|
||||
print(f"Deleted bounty #{args.bounty_id}")
|
||||
else:
|
||||
print(f"Bounty #{args.bounty_id} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except PermissionError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_track(args):
|
||||
"""Track a bounty."""
|
||||
_, tracking_service = create_services()
|
||||
try:
|
||||
success = tracking_service.track_bounty(
|
||||
room_id=args.group_id,
|
||||
user_id=args.user_id,
|
||||
bounty_id=args.bounty_id,
|
||||
)
|
||||
if success:
|
||||
print(f"Tracking bounty #{args.bounty_id}")
|
||||
else:
|
||||
print(f"Already tracking bounty #{args.bounty_id}")
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_untrack(args):
|
||||
"""Untrack a bounty."""
|
||||
_, tracking_service = create_services()
|
||||
success = tracking_service.untrack_bounty(
|
||||
room_id=args.group_id,
|
||||
user_id=args.user_id,
|
||||
bounty_id=args.bounty_id,
|
||||
)
|
||||
if success:
|
||||
print(f"Untracked bounty #{args.bounty_id}")
|
||||
else:
|
||||
print(f"Not tracking bounty #{args.bounty_id}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="jigaido-cli", description="JIGAIDO bounty tracker CLI"
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
||||
parser_add = subparsers.add_parser("add", help="Add a new bounty")
|
||||
parser_add.add_argument("text", help="Bounty description")
|
||||
parser_add.add_argument("--link", help="Optional link")
|
||||
parser_add.add_argument(
|
||||
"--due", help="Optional due date (e.g., 'tomorrow', 'in 3 days')"
|
||||
)
|
||||
|
||||
parser_list = subparsers.add_parser("list", help="List all bounties")
|
||||
|
||||
parser_my = subparsers.add_parser("my", help="List tracked bounties")
|
||||
|
||||
parser_update = subparsers.add_parser("update", help="Update a bounty")
|
||||
parser_update.add_argument("bounty_id", type=int, help="Bounty ID to update")
|
||||
parser_update.add_argument("text", nargs="?", help="New description")
|
||||
parser_update.add_argument("--link", help="New link")
|
||||
parser_update.add_argument("--due", help="New due date")
|
||||
parser_update.add_argument("--clear-link", action="store_true", help="Clear link")
|
||||
parser_update.add_argument(
|
||||
"--clear-due", action="store_true", help="Clear due date"
|
||||
)
|
||||
|
||||
parser_delete = subparsers.add_parser("delete", help="Delete a bounty")
|
||||
parser_delete.add_argument("bounty_id", type=int, help="Bounty ID to delete")
|
||||
|
||||
parser_track = subparsers.add_parser("track", help="Track a bounty")
|
||||
parser_track.add_argument("bounty_id", type=int, help="Bounty ID to track")
|
||||
|
||||
parser_untrack = subparsers.add_parser("untrack", help="Untrack a bounty")
|
||||
parser_untrack.add_argument("bounty_id", type=int, help="Bounty ID to untrack")
|
||||
|
||||
for sp in [
|
||||
parser_add,
|
||||
parser_list,
|
||||
parser_my,
|
||||
parser_update,
|
||||
parser_delete,
|
||||
parser_track,
|
||||
parser_untrack,
|
||||
]:
|
||||
sp.add_argument(
|
||||
"--group-id", type=int, help="Group context (use group room ID)"
|
||||
)
|
||||
sp.add_argument(
|
||||
"--user-id", type=int, help="User context (use Telegram user ID)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if not args.group_id and not args.user_id:
|
||||
print("Error: either --group-id or --user-id is required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.command in ("add", "list", "my", "update", "delete"):
|
||||
if not (args.group_id or args.user_id):
|
||||
print("Error: --group-id or --user-id required", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if args.command == "add" and not args.text:
|
||||
print("Error: text is required for add", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
command_map = {
|
||||
"add": cmd_add,
|
||||
"list": cmd_list,
|
||||
"my": cmd_my,
|
||||
"update": cmd_update,
|
||||
"delete": cmd_delete,
|
||||
"track": cmd_track,
|
||||
"untrack": cmd_untrack,
|
||||
}
|
||||
|
||||
if args.command in command_map:
|
||||
command_map[args.command](args)
|
||||
else:
|
||||
print(f"Unknown command: {args.command}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
376
tests/test_cli.py
Normal file
376
tests/test_cli.py
Normal file
@@ -0,0 +1,376 @@
|
||||
"""Tests for cli/main.py — CLI commands with mocked dependencies."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from io import StringIO
|
||||
import sys
|
||||
|
||||
from core.models import Bounty
|
||||
from core.ports import RoomStorage, TrackingStorage
|
||||
|
||||
|
||||
class MockRoomStorage(RoomStorage):
|
||||
"""Mock RoomStorage for testing."""
|
||||
|
||||
def __init__(self):
|
||||
self._rooms: dict[int, dict] = {}
|
||||
|
||||
def load(self, room_id: int) -> dict | None:
|
||||
return self._rooms.get(room_id)
|
||||
|
||||
def save(self, room_data: dict) -> None:
|
||||
self._rooms[room_data["room_id"]] = room_data
|
||||
|
||||
def add_bounty(self, room_id: int, bounty: Bounty) -> None:
|
||||
if room_id not in self._rooms:
|
||||
self._rooms[room_id] = {"room_id": room_id, "bounties": [], "next_id": 1}
|
||||
self._rooms[room_id]["bounties"].append(bounty)
|
||||
|
||||
def update_bounty(self, room_id: int, bounty: Bounty) -> None:
|
||||
if room_id in self._rooms:
|
||||
for i, b in enumerate(self._rooms[room_id]["bounties"]):
|
||||
if b.id == bounty.id:
|
||||
self._rooms[room_id]["bounties"][i] = bounty
|
||||
break
|
||||
|
||||
def delete_bounty(self, room_id: int, bounty_id: int) -> None:
|
||||
if room_id in self._rooms:
|
||||
self._rooms[room_id]["bounties"] = [
|
||||
b for b in self._rooms[room_id]["bounties"] if b.id != bounty_id
|
||||
]
|
||||
|
||||
def get_bounty(self, room_id: int, bounty_id: int) -> Bounty | None:
|
||||
if room_id in self._rooms:
|
||||
for b in self._rooms[room_id]["bounties"]:
|
||||
if b.id == bounty_id:
|
||||
return b
|
||||
return None
|
||||
|
||||
|
||||
class MockTrackingStorage(TrackingStorage):
|
||||
"""Mock TrackingStorage for testing."""
|
||||
|
||||
def __init__(self):
|
||||
self._tracking: dict[int, dict] = {}
|
||||
|
||||
def load(self, user_id: int) -> dict | None:
|
||||
return self._tracking.get(user_id)
|
||||
|
||||
def save(self, tracking_data: dict) -> None:
|
||||
self._tracking[tracking_data["user_id"]] = tracking_data
|
||||
|
||||
def track_bounty(self, user_id: int, bounty_id: int, room_id: int) -> bool:
|
||||
if user_id not in self._tracking:
|
||||
self._tracking[user_id] = {"user_id": user_id, "bounty_ids": []}
|
||||
if bounty_id not in self._tracking[user_id]["bounty_ids"]:
|
||||
self._tracking[user_id]["bounty_ids"].append(bounty_id)
|
||||
return True
|
||||
return False
|
||||
|
||||
def untrack_bounty(self, user_id: int, bounty_id: int) -> bool:
|
||||
if user_id in self._tracking:
|
||||
if bounty_id in self._tracking[user_id]["bounty_ids"]:
|
||||
self._tracking[user_id]["bounty_ids"].remove(bounty_id)
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_tracked_bounty_ids(self, user_id: int) -> list[int]:
|
||||
if user_id in self._tracking:
|
||||
return self._tracking[user_id]["bounty_ids"]
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_services():
|
||||
"""Create mock services for CLI testing."""
|
||||
room_storage = MockRoomStorage()
|
||||
tracking_storage = MockTrackingStorage()
|
||||
return room_storage, tracking_storage
|
||||
|
||||
|
||||
class TestCLIParsing:
|
||||
"""Test CLI argument parsing through main()."""
|
||||
|
||||
def test_add_command_requires_text(self):
|
||||
"""Test that 'add' command requires text argument."""
|
||||
from cli.main import main
|
||||
|
||||
with patch("sys.argv", ["jigaido-cli", "add", "--group-id=-1001"]):
|
||||
with patch("cli.main.create_services"):
|
||||
with patch("sys.stderr", new=StringIO()) as stderr:
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
assert "arguments are required: text" in stderr.getvalue()
|
||||
|
||||
def test_list_command(self):
|
||||
"""Test 'list' command parsing."""
|
||||
from cli.main import main
|
||||
|
||||
with patch("sys.argv", ["jigaido-cli", "list", "--group-id=-1001"]):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_create.return_value = (MagicMock(), MagicMock())
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.list_bounties.return_value = []
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
assert "No bounties" in stdout.getvalue()
|
||||
|
||||
def test_my_command(self):
|
||||
"""Test 'my' command parsing."""
|
||||
from cli.main import main
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["jigaido-cli", "my", "--group-id=-1001", "--user-id=123"]
|
||||
):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_tracking_service = MagicMock()
|
||||
mock_tracking_service.get_tracked_bounties.return_value = []
|
||||
mock_create.return_value = (MagicMock(), mock_tracking_service)
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
assert "Not tracking any bounties" in stdout.getvalue()
|
||||
|
||||
def test_update_command(self):
|
||||
"""Test 'update' command parsing."""
|
||||
from cli.main import main
|
||||
|
||||
with patch(
|
||||
"sys.argv", ["jigaido-cli", "update", "1", "--group-id=-1001", "new text"]
|
||||
):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.update_bounty.return_value = True
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
assert "Updated bounty #1" in stdout.getvalue()
|
||||
|
||||
def test_delete_command(self):
|
||||
"""Test 'delete' command parsing."""
|
||||
from cli.main import main
|
||||
|
||||
with patch("sys.argv", ["jigaido-cli", "delete", "1", "--group-id=-1001"]):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.delete_bounty.return_value = True
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
assert "Deleted bounty #1" in stdout.getvalue()
|
||||
|
||||
def test_track_command(self):
|
||||
"""Test 'track' command parsing."""
|
||||
from cli.main import main
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["jigaido-cli", "track", "1", "--group-id=-1001", "--user-id=123"],
|
||||
):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_tracking_service = MagicMock()
|
||||
mock_tracking_service.track_bounty.return_value = True
|
||||
mock_create.return_value = (MagicMock(), mock_tracking_service)
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
assert "Tracking bounty #1" in stdout.getvalue()
|
||||
|
||||
def test_untrack_command(self):
|
||||
"""Test 'untrack' command parsing."""
|
||||
from cli.main import main
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["jigaido-cli", "untrack", "1", "--group-id=-1001", "--user-id=123"],
|
||||
):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_tracking_service = MagicMock()
|
||||
mock_tracking_service.untrack_bounty.return_value = True
|
||||
mock_create.return_value = (MagicMock(), mock_tracking_service)
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
assert "Untracked bounty #1" in stdout.getvalue()
|
||||
|
||||
|
||||
class TestCLIValidation:
|
||||
"""Test CLI input validation."""
|
||||
|
||||
def test_requires_group_id_or_user_id(self):
|
||||
"""Test that commands require either --group-id or --user-id."""
|
||||
from cli.main import main
|
||||
|
||||
with patch("sys.argv", ["jigaido-cli", "add", "some text"]):
|
||||
with patch("sys.stderr", new=StringIO()) as stderr:
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
assert "group-id or --user-id is required" in stderr.getvalue()
|
||||
|
||||
def test_unknown_command(self):
|
||||
"""Test handling of unknown commands."""
|
||||
from cli.main import main
|
||||
|
||||
with patch("sys.argv", ["jigaido-cli", "unknown_cmd", "--group-id=-1001"]):
|
||||
with patch("sys.stderr", new=StringIO()) as stderr:
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
assert "invalid choice" in stderr.getvalue()
|
||||
|
||||
def test_update_clear_link_flag(self):
|
||||
"""Test update with --clear-link flag."""
|
||||
from cli.main import main
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["jigaido-cli", "update", "1", "--clear-link", "--group-id=-1001"],
|
||||
):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.update_bounty.return_value = True
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
main()
|
||||
mock_bounty_service.update_bounty.assert_called_once()
|
||||
call_kwargs = mock_bounty_service.update_bounty.call_args
|
||||
assert call_kwargs.kwargs.get("clear_link") == True
|
||||
|
||||
def test_update_clear_due_flag(self):
|
||||
"""Test update with --clear-due flag."""
|
||||
from cli.main import main
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["jigaido-cli", "update", "1", "--clear-due", "--group-id=-1001"],
|
||||
):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.update_bounty.return_value = True
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
main()
|
||||
mock_bounty_service.update_bounty.assert_called_once()
|
||||
call_kwargs = mock_bounty_service.update_bounty.call_args
|
||||
assert call_kwargs.kwargs.get("clear_due") == True
|
||||
|
||||
|
||||
class TestCLIOutput:
|
||||
"""Test CLI output formatting."""
|
||||
|
||||
def test_list_shows_bounties(self):
|
||||
"""Test that 'list' shows bounty details correctly."""
|
||||
from cli.main import main
|
||||
|
||||
mock_bounty = MagicMock()
|
||||
mock_bounty.id = 1
|
||||
mock_bounty.text = "Fix bug"
|
||||
mock_bounty.link = "https://github.com/issue/1"
|
||||
mock_bounty.due_date_ts = None
|
||||
|
||||
with patch("sys.argv", ["jigaido-cli", "list", "--group-id=-1001"]):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.list_bounties.return_value = [mock_bounty]
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
output = stdout.getvalue()
|
||||
assert "#1:" in output
|
||||
assert "Fix bug" in output
|
||||
assert "https://github.com/issue/1" in output
|
||||
|
||||
def test_list_empty(self):
|
||||
"""Test that 'list' shows 'No bounties' when empty."""
|
||||
from cli.main import main
|
||||
|
||||
with patch("sys.argv", ["jigaido-cli", "list", "--group-id=-1001"]):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.list_bounties.return_value = []
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
assert "No bounties" in stdout.getvalue()
|
||||
|
||||
def test_add_output(self):
|
||||
"""Test that 'add' outputs the new bounty ID."""
|
||||
from cli.main import main
|
||||
|
||||
mock_bounty = MagicMock()
|
||||
mock_bounty.id = 42
|
||||
|
||||
with patch("sys.argv", ["jigaido-cli", "add", "New task", "--group-id=-1001"]):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.add_bounty.return_value = mock_bounty
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
assert "Added bounty #42" in stdout.getvalue()
|
||||
|
||||
|
||||
class TestCLIErrorHandling:
|
||||
"""Test CLI error handling."""
|
||||
|
||||
def test_delete_nonexistent_bounty(self):
|
||||
"""Test deleting a non-existent bounty."""
|
||||
from cli.main import main
|
||||
|
||||
with patch("sys.argv", ["jigaido-cli", "delete", "999", "--group-id=-1001"]):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.delete_bounty.return_value = False
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
with patch("sys.stderr", new=StringIO()) as stderr:
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
assert "not found" in stderr.getvalue()
|
||||
|
||||
def test_update_permission_error(self):
|
||||
"""Test update with permission error."""
|
||||
from cli.main import main
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["jigaido-cli", "update", "1", "new text", "--group-id=-1001"],
|
||||
):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_bounty_service = MagicMock()
|
||||
mock_bounty_service.update_bounty.side_effect = PermissionError(
|
||||
"Not owner"
|
||||
)
|
||||
mock_create.return_value = (mock_bounty_service, MagicMock())
|
||||
|
||||
with patch("sys.stderr", new=StringIO()) as stderr:
|
||||
with pytest.raises(SystemExit):
|
||||
main()
|
||||
assert "Not owner" in stderr.getvalue()
|
||||
|
||||
def test_track_already_tracking(self):
|
||||
"""Test tracking a bounty that's already tracked."""
|
||||
from cli.main import main
|
||||
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["jigaido-cli", "track", "1", "--group-id=-1001", "--user-id=123"],
|
||||
):
|
||||
with patch("cli.main.create_services") as mock_create:
|
||||
mock_tracking_service = MagicMock()
|
||||
mock_tracking_service.track_bounty.return_value = False
|
||||
mock_create.return_value = (MagicMock(), mock_tracking_service)
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as stdout:
|
||||
main()
|
||||
assert "Already tracking" in stdout.getvalue()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user