""" 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() # Create more realistic mock issues with proper attributes from datetime import datetime mock_datetime = Mock() mock_datetime.strftime.return_value = "2023-01-01" mock_issue1 = Mock(spec=Issue) mock_issue1.number = 1 mock_issue1.title = "Test Issue 1" mock_issue1.state = "open" mock_issue1.labels = [] mock_issue1.body = "Test body 1" mock_issue1.created_at = mock_datetime mock_issue1.updated_at = mock_datetime mock_issue2 = Mock(spec=Issue) mock_issue2.number = 2 mock_issue2.title = "Test Issue 2" mock_issue2.state = "closed" mock_issue2.labels = [] mock_issue2.body = "Test body 2" mock_issue2.created_at = mock_datetime mock_issue2.updated_at = mock_datetime mock_issues = [mock_issue1, mock_issue2] 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() from datetime import datetime mock_datetime = Mock() mock_datetime.strftime.return_value = "2023-01-01" mock_issue = Mock() mock_issue.number = 59 mock_issue.title = "Test Issue" mock_issue.state = "open" mock_issue.labels = [] mock_issue.body = "Test issue body" mock_issue.created_at = mock_datetime mock_issue.updated_at = mock_datetime 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() from datetime import datetime mock_datetime = Mock() mock_datetime.strftime.return_value = "2023-01-01 00:00" mock_issue = Mock() mock_issue.number = 59 mock_issue.title = "Test Issue" mock_issue._body = "Test issue body" mock_issue.state = Mock() mock_issue.state.value = "open" mock_issue.created_at = mock_datetime mock_issue.updated_at = mock_datetime mock_issue.labels = [] mock_issue.assignee = None mock_issue.milestone = None mock_issue.state_label = "OPEN" mock_issue.priority_label = "Normal" mock_issue.type_labels = [] mock_issue.other_labels = [] mock_issue.html_url = "" 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() from datetime import datetime mock_datetime = Mock() mock_datetime.strftime.return_value = "2023-01-01 00:00" mock_issue = Mock() mock_issue.number = 59 mock_issue.title = "Test Issue" mock_issue._body = "Detailed issue description" mock_issue.state = Mock() mock_issue.state.value = "open" mock_issue.created_at = mock_datetime mock_issue.updated_at = mock_datetime mock_issue.labels = [] mock_issue.assignee = None mock_issue.milestone = None mock_issue.state_label = "OPEN" mock_issue.priority_label = "Normal" mock_issue.type_labels = [] mock_issue.other_labels = [] mock_issue.html_url = "" mock_issue.kanban_column = "To Do" 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() from datetime import datetime mock_datetime = Mock() mock_datetime.strftime.return_value = "2023-01-01 00:00" mock_issue = Mock() mock_issue.number = 59 mock_issue.title = "Test Issue" mock_issue._body = "Test issue body" mock_issue.state = Mock() mock_issue.state.value = "open" mock_issue.created_at = mock_datetime mock_issue.updated_at = mock_datetime mock_issue.labels = [] mock_issue.assignee = None mock_issue.milestone = None mock_issue.state_label = "OPEN" mock_issue.priority_label = "Normal" mock_issue.type_labels = [] mock_issue.other_labels = [] mock_issue.html_url = "" mock_issue.kanban_column = "To Do" 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', '--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