398 lines
15 KiB
Python
398 lines
15 KiB
Python
|
|
"""
|
||
|
|
Tests for Issue #59 - CLI Interface
|
||
|
|
|
||
|
|
This module contains tests for the unified CLI interface that provides
|
||
|
|
consistent commands for issue management across different backends.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from unittest.mock import Mock, patch, MagicMock
|
||
|
|
from click.testing import CliRunner
|
||
|
|
from typing import List
|
||
|
|
|
||
|
|
# Import CLI commands we'll implement
|
||
|
|
# Note: These imports will fail initially (RED phase)
|
||
|
|
from markitect.cli import cli
|
||
|
|
from markitect.issues.commands import issues_group
|
||
|
|
from markitect.issues.manager import IssuePluginManager
|
||
|
|
from domain.issues.models import Issue
|
||
|
|
|
||
|
|
|
||
|
|
class TestIssuesCLIGroup:
|
||
|
|
"""Test suite for the main issues CLI group."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.runner = CliRunner()
|
||
|
|
|
||
|
|
def test_issues_group_exists_in_main_cli(self):
|
||
|
|
"""Test that issues group is properly registered in main CLI."""
|
||
|
|
result = self.runner.invoke(cli, ['--help'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert 'issues' in result.output
|
||
|
|
|
||
|
|
def test_issues_group_shows_help(self):
|
||
|
|
"""Test that issues group displays help information."""
|
||
|
|
result = self.runner.invoke(cli, ['issues', '--help'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert 'Issue management' in result.output
|
||
|
|
assert 'list' in result.output
|
||
|
|
assert 'show' in result.output
|
||
|
|
assert 'create' in result.output
|
||
|
|
|
||
|
|
def test_issues_group_description(self):
|
||
|
|
"""Test that issues group has appropriate description."""
|
||
|
|
result = self.runner.invoke(cli, ['issues', '--help'])
|
||
|
|
|
||
|
|
assert 'multiple backend support' in result.output.lower()
|
||
|
|
|
||
|
|
|
||
|
|
class TestIssuesListCommand:
|
||
|
|
"""Test suite for the issues list command."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.runner = CliRunner()
|
||
|
|
|
||
|
|
def test_list_all_issues_default(self):
|
||
|
|
"""Test listing all issues with default parameters."""
|
||
|
|
with patch('markitect.issues.commands.IssuePluginManager') as mock_manager_class:
|
||
|
|
# Mock the plugin manager and backend
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_issues = [Mock(spec=Issue), Mock(spec=Issue)]
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.list_issues.return_value = mock_issues
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'list'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_manager.get_backend.assert_called_once_with(None)
|
||
|
|
mock_backend.list_issues.assert_called_once_with(state='all')
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_list_open_issues_only(self, mock_manager_class):
|
||
|
|
"""Test listing only open issues."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.list_issues.return_value = []
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'list', '--state', 'open'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_backend.list_issues.assert_called_once_with(state='open')
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_list_closed_issues_only(self, mock_manager_class):
|
||
|
|
"""Test listing only closed issues."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.list_issues.return_value = []
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'list', '--state', 'closed'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_backend.list_issues.assert_called_once_with(state='closed')
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_list_with_backend_override(self, mock_manager_class):
|
||
|
|
"""Test listing issues with backend override."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.list_issues.return_value = []
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'list', '--backend', 'local'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_manager.get_backend.assert_called_once_with('local')
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_list_displays_issues_in_table_format(self, mock_manager_class):
|
||
|
|
"""Test that list command displays issues in readable table format."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_issue = Mock()
|
||
|
|
mock_issue.number = 59
|
||
|
|
mock_issue.title = "Test Issue"
|
||
|
|
mock_issue.state = "open"
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.list_issues.return_value = [mock_issue]
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'list'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert '59' in result.output
|
||
|
|
assert 'Test Issue' in result.output
|
||
|
|
|
||
|
|
|
||
|
|
class TestIssuesShowCommand:
|
||
|
|
"""Test suite for the issues show command."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.runner = CliRunner()
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_show_specific_issue(self, mock_manager_class):
|
||
|
|
"""Test showing a specific issue by ID."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_issue = Mock()
|
||
|
|
mock_issue.number = 59
|
||
|
|
mock_issue.title = "Test Issue"
|
||
|
|
mock_issue.body = "Test issue body"
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.get_issue.return_value = mock_issue
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'show', '59'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_backend.get_issue.assert_called_once_with('59')
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_show_displays_issue_details(self, mock_manager_class):
|
||
|
|
"""Test that show command displays comprehensive issue details."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_issue = Mock()
|
||
|
|
mock_issue.number = 59
|
||
|
|
mock_issue.title = "Test Issue"
|
||
|
|
mock_issue.body = "Detailed issue description"
|
||
|
|
mock_issue.state = "open"
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.get_issue.return_value = mock_issue
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'show', '59'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert 'Test Issue' in result.output
|
||
|
|
assert 'Detailed issue description' in result.output
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_show_with_backend_override(self, mock_manager_class):
|
||
|
|
"""Test showing issue with specific backend override."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.get_issue.return_value = Mock()
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'show', '59', '--backend', 'gitea'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_manager.get_backend.assert_called_once_with('gitea')
|
||
|
|
|
||
|
|
|
||
|
|
class TestIssuesCreateCommand:
|
||
|
|
"""Test suite for the issues create command."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.runner = CliRunner()
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_create_issue_with_title_and_body(self, mock_manager_class):
|
||
|
|
"""Test creating an issue with title and body."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_created_issue = Mock()
|
||
|
|
mock_created_issue.number = 60
|
||
|
|
mock_created_issue.title = "New Issue"
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.create_issue.return_value = mock_created_issue
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'create', 'New Issue', 'Issue body content'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_backend.create_issue.assert_called_once_with('New Issue', 'Issue body content')
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_create_displays_success_message(self, mock_manager_class):
|
||
|
|
"""Test that create command displays success message with issue number."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_created_issue = Mock()
|
||
|
|
mock_created_issue.number = 60
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.create_issue.return_value = mock_created_issue
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'create', 'Test', 'Body'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert '60' in result.output
|
||
|
|
assert 'created' in result.output.lower()
|
||
|
|
|
||
|
|
|
||
|
|
class TestIssuesCommentCommand:
|
||
|
|
"""Test suite for the issues comment command."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.runner = CliRunner()
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_add_comment_to_issue(self, mock_manager_class):
|
||
|
|
"""Test adding a comment to an existing issue."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.add_comment.return_value = {}
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'comment', '59', 'This is a comment'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_backend.add_comment.assert_called_once_with('59', 'This is a comment')
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_comment_displays_success_message(self, mock_manager_class):
|
||
|
|
"""Test that comment command displays success message."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.add_comment.return_value = {}
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'comment', '59', 'Test comment'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert 'comment added' in result.output.lower()
|
||
|
|
|
||
|
|
|
||
|
|
class TestIssuesCloseCommand:
|
||
|
|
"""Test suite for the issues close command."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.runner = CliRunner()
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_close_issue(self, mock_manager_class):
|
||
|
|
"""Test closing an issue."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_closed_issue = Mock()
|
||
|
|
mock_closed_issue.state = "closed"
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.close_issue.return_value = mock_closed_issue
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'close', '59'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_backend.close_issue.assert_called_once_with('59')
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_close_displays_success_message(self, mock_manager_class):
|
||
|
|
"""Test that close command displays success message."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.close_issue.return_value = Mock()
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'close', '59'])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
assert 'closed' in result.output.lower()
|
||
|
|
|
||
|
|
|
||
|
|
class TestErrorHandling:
|
||
|
|
"""Test suite for CLI error handling."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.runner = CliRunner()
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_backend_error_displays_user_friendly_message(self, mock_manager_class):
|
||
|
|
"""Test that backend errors are displayed in user-friendly format."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.side_effect = Exception("Backend connection failed")
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'list'])
|
||
|
|
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert 'error' in result.output.lower()
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_invalid_issue_id_displays_helpful_error(self, mock_manager_class):
|
||
|
|
"""Test that invalid issue IDs display helpful error messages."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_backend = Mock()
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
mock_manager.get_backend.return_value = mock_backend
|
||
|
|
mock_backend.get_issue.side_effect = Exception("Issue not found")
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'show', '999999'])
|
||
|
|
|
||
|
|
assert result.exit_code != 0
|
||
|
|
assert 'not found' in result.output.lower()
|
||
|
|
|
||
|
|
|
||
|
|
class TestBackendIntegration:
|
||
|
|
"""Test suite for backend integration in CLI commands."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.runner = CliRunner()
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_cli_respects_backend_configuration(self, mock_manager_class):
|
||
|
|
"""Test that CLI commands respect backend configuration."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
|
||
|
|
# Test with different backends
|
||
|
|
for backend in ['gitea', 'local']:
|
||
|
|
mock_manager.get_backend.return_value = Mock()
|
||
|
|
mock_manager.get_backend.return_value.list_issues.return_value = []
|
||
|
|
|
||
|
|
result = self.runner.invoke(cli, ['issues', 'list', '--backend', backend])
|
||
|
|
|
||
|
|
assert result.exit_code == 0
|
||
|
|
mock_manager.get_backend.assert_called_with(backend)
|
||
|
|
|
||
|
|
@patch('markitect.issues.commands.IssuePluginManager')
|
||
|
|
def test_cli_handles_plugin_switching_gracefully(self, mock_manager_class):
|
||
|
|
"""Test that CLI handles switching between plugins gracefully."""
|
||
|
|
mock_manager = Mock()
|
||
|
|
mock_manager_class.return_value = mock_manager
|
||
|
|
|
||
|
|
# First call with gitea
|
||
|
|
mock_manager.get_backend.return_value = Mock()
|
||
|
|
mock_manager.get_backend.return_value.list_issues.return_value = []
|
||
|
|
result1 = self.runner.invoke(cli, ['issues', 'list', '--backend', 'gitea'])
|
||
|
|
|
||
|
|
# Second call with local
|
||
|
|
mock_manager.get_backend.return_value = Mock()
|
||
|
|
mock_manager.get_backend.return_value.list_issues.return_value = []
|
||
|
|
result2 = self.runner.invoke(cli, ['issues', 'list', '--backend', 'local'])
|
||
|
|
|
||
|
|
assert result1.exit_code == 0
|
||
|
|
assert result2.exit_code == 0
|