""" Tests for Issue #59 - Issue Management Plugin Manager This module contains tests for the plugin manager that handles backend discovery, loading, and configuration for the unified issue management CLI. """ import pytest from unittest.mock import Mock, patch from pathlib import Path from typing import Dict, Any # Import the classes we'll implement # Note: These imports will fail initially (RED phase) from markitect.issues.manager import IssuePluginManager from markitect.issues.base import IssueBackend from markitect.issues.plugins.gitea import GiteaPlugin from markitect.issues.plugins.local import LocalPlugin from markitect.issues.exceptions import PluginNotFoundError, ConfigurationError class TestIssuePluginManager: """Test suite for the issue plugin manager.""" def test_manager_initialization_with_default_config(self): """Test plugin manager initializes with default configuration.""" manager = IssuePluginManager() assert manager is not None assert hasattr(manager, 'config') assert hasattr(manager, 'plugins') def test_manager_initialization_with_custom_config_path(self): """Test plugin manager accepts custom config path.""" config_path = "/custom/path/config.yml" with patch.object(IssuePluginManager, '_load_config') as mock_load: mock_load.return_value = {'default_backend': 'gitea'} manager = IssuePluginManager(config_path) mock_load.assert_called_once_with(config_path) def test_plugin_discovery_finds_available_backends(self): """Test plugin discovery locates all available backend plugins.""" manager = IssuePluginManager() # Should discover at least gitea and local plugins assert 'gitea' in manager.plugins assert 'local' in manager.plugins assert len(manager.plugins) >= 2 def test_get_default_backend_when_none_specified(self): """Test getting backend instance uses default from config.""" with patch.object(IssuePluginManager, '_load_config') as mock_load: mock_load.return_value = { 'default_backend': 'gitea', 'backends': {'gitea': {'url': 'http://test.com'}} } manager = IssuePluginManager() backend = manager.get_backend() assert isinstance(backend, IssueBackend) def test_get_specific_backend_override(self): """Test getting specific backend overrides default config.""" with patch.object(IssuePluginManager, '_load_config') as mock_load: mock_load.return_value = { 'default_backend': 'gitea', 'backends': { 'gitea': {'url': 'http://test.com'}, 'local': {'directory': '.issues'} } } manager = IssuePluginManager() backend = manager.get_backend('local') assert isinstance(backend, IssueBackend) def test_get_unknown_backend_raises_error(self): """Test requesting unknown backend raises appropriate error.""" manager = IssuePluginManager() with pytest.raises(PluginNotFoundError): manager.get_backend('nonexistent') def test_config_loading_with_missing_file(self): """Test configuration loading handles missing config file gracefully.""" manager = IssuePluginManager() # Should have default configuration assert manager.config is not None assert 'default_backend' in manager.config def test_config_loading_with_invalid_yaml(self): """Test configuration loading handles invalid YAML gracefully.""" with patch('builtins.open', side_effect=Exception("Invalid YAML")): manager = IssuePluginManager() # Should fall back to default configuration assert manager.config is not None class TestPluginInterface: """Test suite for the abstract plugin interface.""" def test_abstract_backend_cannot_be_instantiated(self): """Test abstract IssueBackend cannot be instantiated directly.""" with pytest.raises(TypeError): IssueBackend() def test_plugin_must_implement_all_abstract_methods(self): """Test concrete plugins must implement all abstract methods.""" class IncompletePlugin(IssueBackend): def list_issues(self, state=None): return [] # Missing other required methods with pytest.raises(TypeError): IncompletePlugin() def test_complete_plugin_implementation_works(self): """Test properly implemented plugin can be instantiated.""" class CompletePlugin(IssueBackend): def list_issues(self, state=None): return [] def get_issue(self, issue_id): return Mock() def create_issue(self, title, body, **kwargs): return Mock() def add_comment(self, issue_id, comment): return {} def close_issue(self, issue_id): return Mock() def update_issue(self, issue_id, **kwargs): return Mock() # Should not raise any errors plugin = CompletePlugin({}) assert isinstance(plugin, IssueBackend) class TestPluginConfiguration: """Test suite for plugin configuration management.""" def test_backend_receives_configuration_on_initialization(self): """Test backend plugins receive their configuration during init.""" config = { 'default_backend': 'gitea', 'backends': { 'gitea': {'url': 'http://test.com', 'repo': 'test/repo'} } } with patch.object(IssuePluginManager, '_load_config', return_value=config): with patch.object(IssuePluginManager, '_discover_plugins') as mock_discover: # Mock the plugin class to verify config is passed mock_plugin_class = Mock() mock_discover.return_value = {'gitea': mock_plugin_class} manager = IssuePluginManager() manager.get_backend('gitea') # Verify plugin was initialized with backend config mock_plugin_class.assert_called_once_with({'url': 'http://test.com', 'repo': 'test/repo'}) def test_missing_backend_config_uses_empty_dict(self): """Test backend initialization with missing config uses empty dict.""" config = { 'default_backend': 'local', 'backends': {} # No local backend config } with patch.object(IssuePluginManager, '_load_config', return_value=config): with patch.object(IssuePluginManager, '_discover_plugins') as mock_discover: mock_plugin_class = Mock() mock_discover.return_value = {'local': mock_plugin_class} manager = IssuePluginManager() manager.get_backend('local') # Should initialize with empty config mock_plugin_class.assert_called_once_with({}) def test_config_validation_rejects_invalid_backend_names(self): """Test configuration validation rejects invalid backend names.""" config = { 'default_backend': 'invalid-backend-name', 'backends': {} } with patch.object(IssuePluginManager, '_load_config', return_value=config): manager = IssuePluginManager() with pytest.raises(PluginNotFoundError): manager.get_backend() class TestErrorHandling: """Test suite for error handling scenarios.""" def test_plugin_loading_failure_provides_helpful_error(self): """Test plugin loading failures provide helpful error messages.""" manager = IssuePluginManager() with pytest.raises(PluginNotFoundError) as exc_info: manager.get_backend('nonexistent') assert 'nonexistent' in str(exc_info.value) assert 'backend' in str(exc_info.value).lower() def test_configuration_error_for_malformed_config(self): """Test configuration errors for malformed configuration.""" # This will be implemented when we add config validation pass def test_graceful_degradation_on_plugin_import_failure(self): """Test system handles plugin import failures gracefully.""" # Mock import failure for one plugin with patch('importlib.import_module', side_effect=ImportError("Mock import failure")): manager = IssuePluginManager() # Should still work with available plugins assert manager.plugins is not None