- Tests mock dependencies and verify command existence, flag processing, and output format - Tests use --group-id=-1001 format (equals sign) to avoid argparse treating -1001 as a flag - All 17 tests passing
377 lines
14 KiB
Python
377 lines
14 KiB
Python
"""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"])
|