""" Tests for configuration CLI commands. Tests the new configuration management CLI commands: - config-show - config-validate - config-troubleshoot - config-files """ import os import sys import pytest from pathlib import Path from unittest.mock import patch, MagicMock, mock_open from io import StringIO # Add the project root to the path sys.path.insert(0, str(Path(__file__).parent.parent)) from cli.commands.config import ConfigCommands from cli.presenters.config import ConfigPresenter from config import MarkitectConfig, ConfigurationError class TestConfigCommands: """Test suite for configuration CLI commands.""" def setup_method(self): """Set up test fixtures.""" self.config_commands = ConfigCommands() def _get_mock_config(self): """Get a mock configuration for testing.""" return MarkitectConfig( gitea_url="https://github.com", repo_owner="test_owner", repo_name="test_repo", workspace_dir=Path(".test_workspace"), database_path=Path("/tmp/test.db") ) def _get_mock_status(self): """Get mock configuration status.""" return { 'sources': { 'environment': {'loaded': True, 'path': 'Environment'}, 'env_file': {'loaded': True, 'path': '.env.tddai'}, 'defaults': {'loaded': True, 'path': 'System'} } } @patch('cli.commands.config.get_unified_config') @patch('cli.commands.config.get_config_status') @patch('sys.stdout', new_callable=StringIO) def test_config_show_command_displays_current_configuration_status(self, mock_stdout, mock_status, mock_config): """Test config-show command displays current configuration status.""" mock_config.return_value = self._get_mock_config() mock_status.return_value = self._get_mock_status() self.config_commands.show_config() output = mock_stdout.getvalue() assert "🔧 Configuration Status" in output assert "Core Configuration" in output # The output shows real config is being used, verify mock was called mock_config.assert_called_once() mock_status.assert_called_once() @patch('cli.commands.config.get_unified_config') @patch('cli.commands.config.get_config_status') @patch('os.getenv') @patch('sys.stdout', new_callable=StringIO) def test_show_config_with_sensitive(self, mock_stdout, mock_getenv, mock_status, mock_config): """Test config-show with sensitive information.""" mock_config.return_value = self._get_mock_config() mock_status.return_value = self._get_mock_status() mock_getenv.side_effect = lambda key, default=None: "test_token_12345678" if "TOKEN" in key else default self.config_commands.show_config(show_sensitive=True) output = mock_stdout.getvalue() assert "🔧 Configuration Status" in output # Should show some masked token (pattern varies) assert "..." in output and "tok" in output @patch('cli.commands.config.get_unified_config') @patch('sys.stderr', new_callable=StringIO) def test_show_config_error(self, mock_stderr, mock_config): """Test config-show with configuration error.""" mock_config.side_effect = ConfigurationError("Test configuration error") with pytest.raises(SystemExit) as exc_info: self.config_commands.show_config() assert exc_info.value.code == 1 @patch('cli.commands.config.get_unified_config') @patch('sys.stdout', new_callable=StringIO) def test_config_validate_command_reports_valid_configuration_status(self, mock_stdout, mock_config): """Test config-validate command reports valid configuration status.""" mock_config.return_value = self._get_mock_config() with patch.object(self.config_commands, '_perform_validation_checks') as mock_validate: mock_validate.return_value = [ {'check': 'Test check', 'status': 'success', 'message': 'All good'}, {'check': 'Another check', 'status': 'success', 'message': 'Perfect'} ] self.config_commands.validate_config() output = mock_stdout.getvalue() assert "✅ Configuration Validation" in output assert "2/2 checks passed" in output assert "✅ Test check" in output @patch('cli.commands.config.get_unified_config') @patch('sys.stdout', new_callable=StringIO) def test_validate_config_with_errors(self, mock_stdout, mock_config): """Test config validation with errors.""" mock_config.return_value = self._get_mock_config() with patch.object(self.config_commands, '_perform_validation_checks') as mock_validate: mock_validate.return_value = [ {'check': 'Good check', 'status': 'success', 'message': 'All good'}, {'check': 'Bad check', 'status': 'error', 'message': 'Error found', 'suggestion': 'Fix it'} ] with pytest.raises(SystemExit) as exc_info: self.config_commands.validate_config() assert exc_info.value.code == 1 output = mock_stdout.getvalue() assert "❌ 1 errors" in output @patch('cli.commands.config.get_unified_config') @patch('cli.commands.config.get_config_status') @patch('sys.stdout', new_callable=StringIO) def test_config_troubleshoot_command_provides_diagnostic_information(self, mock_stdout, mock_status, mock_config): """Test config-troubleshoot command provides diagnostic information.""" mock_config.return_value = self._get_mock_config() mock_status.return_value = self._get_mock_status() with patch.object(self.config_commands, '_run_diagnostics') as mock_diagnostics: mock_diagnostics.return_value = { 'environment': {'python_version': '3.8.0', 'environment_variables': {}}, 'filesystem': {}, 'config_files': {}, 'git_repository': {}, 'network': {} } self.config_commands.troubleshoot_config() output = mock_stdout.getvalue() assert "🔍 Configuration Troubleshooting" in output assert "✅ Configuration loaded successfully" in output @patch('cli.commands.config.get_unified_config') @patch('sys.stdout', new_callable=StringIO) def test_troubleshoot_config_failure(self, mock_stdout, mock_config): """Test config troubleshooting when config loading fails.""" mock_config.side_effect = ConfigurationError("Failed to load config") with patch.object(self.config_commands, '_run_basic_diagnostics') as mock_diagnostics: mock_diagnostics.return_value = { 'environment': { 'python_version': '3.8.0', 'python_executable': '/usr/bin/python3', 'current_directory': '/test', 'environment_variables': {} }, 'filesystem': {}, 'config_files': {}, 'git_repository': {'is_git_repository': False} } self.config_commands.troubleshoot_config() output = mock_stdout.getvalue() assert "🔍 Configuration Troubleshooting" in output # Should not show "Configuration loaded successfully" assert "✅ Configuration loaded successfully" not in output @patch('sys.stdout', new_callable=StringIO) def test_check_config_files(self, mock_stdout): """Test config files checking.""" with patch.object(self.config_commands, '_check_configuration_files') as mock_check: mock_check.return_value = { '.env.tddai': { 'path': '.env.tddai', 'exists': True, 'readable': True, 'size': 100, 'modified': 1234567890, 'parsed_variables': 3, 'parse_error': None }, '.env': { 'path': '.env', 'exists': False, 'readable': False, 'size': 0, 'modified': None } } self.config_commands.check_config_files() output = mock_stdout.getvalue() assert "📁 Configuration Files Status" in output assert "✅ .env.tddai" in output assert "❌ .env" in output def test_perform_validation_checks_all_valid(self): """Test validation checks with all valid configuration.""" config = self._get_mock_config() with patch.dict('os.environ', {'GITEA_API_TOKEN': 'test_token'}): results = self.config_commands._perform_validation_checks(config) # Should have checks for required fields, URL format, workspace, and auth token assert len(results) == 6 # All should be successful success_results = [r for r in results if r['status'] == 'success'] assert len(success_results) == 6 def test_perform_validation_checks_missing_fields(self): """Test validation checks with missing required fields.""" # Create a config that bypasses normal validation config = MarkitectConfig.__new__(MarkitectConfig) config.gitea_url = "" config.repo_owner = "" config.repo_name = "test_repo" config.workspace_dir = Path(".test_workspace") results = self.config_commands._perform_validation_checks(config) # Should have error results for missing fields error_results = [r for r in results if r['status'] == 'error'] assert len(error_results) >= 2 # At least gitea_url and repo_owner def test_perform_validation_checks_invalid_gitea_url(self): """Test validation checks with invalid Gitea URL format.""" # Create a config that bypasses normal validation config = MarkitectConfig.__new__(MarkitectConfig) config.gitea_url = "invalid-url" config.repo_owner = "test_owner" config.repo_name = "test_repo" config.workspace_dir = Path(".test_workspace") results = self.config_commands._perform_validation_checks(config) # Should have error for invalid URL format url_errors = [r for r in results if 'URL format' in r['check'] and r['status'] == 'error'] assert len(url_errors) == 1 @patch('os.access') def test_check_filesystem_permissions(self, mock_access): """Test filesystem diagnostics.""" mock_access.return_value = True result = self.config_commands._check_filesystem() assert 'current_directory' in result assert 'home_directory' in result assert result['current_directory']['readable'] is True assert result['current_directory']['writable'] is True @patch('subprocess.run') def test_check_git_repository_with_git(self, mock_run): """Test git repository checking with git available.""" # Mock git commands def side_effect(cmd, **kwargs): if 'remote' in cmd: return MagicMock(returncode=0, stdout="https://github.com/test/repo.git") elif 'branch' in cmd: return MagicMock(returncode=0, stdout="main") return MagicMock(returncode=0, stdout="") mock_run.side_effect = side_effect with patch('pathlib.Path.exists', return_value=True): result = self.config_commands._check_git_repository() assert result['is_git_repository'] is True assert 'remote_origin' in result assert 'current_branch' in result def test_check_git_repository_without_git(self): """Test git repository checking without git directory.""" with patch('pathlib.Path.exists', return_value=False): result = self.config_commands._check_git_repository() assert result['is_git_repository'] is False @patch('urllib.request.urlopen') def test_check_network_connectivity_success(self, mock_urlopen): """Test successful network connectivity check.""" mock_response = MagicMock() mock_response.getcode.return_value = 200 mock_urlopen.return_value.__enter__.return_value = mock_response config = self._get_mock_config() result = self.config_commands._check_network_connectivity(config) assert 'gitea_connectivity' in result assert result['gitea_connectivity']['reachable'] is True assert result['gitea_connectivity']['status_code'] == 200 @patch('urllib.request.urlopen') def test_check_network_connectivity_failure(self, mock_urlopen): """Test failed network connectivity check.""" mock_urlopen.side_effect = Exception("Connection failed") config = self._get_mock_config() result = self.config_commands._check_network_connectivity(config) assert 'gitea_connectivity' in result assert result['gitea_connectivity']['reachable'] is False assert 'error' in result['gitea_connectivity'] @patch('pathlib.Path.exists') @patch('os.access') def test_check_configuration_files_existing(self, mock_access, mock_exists): """Test configuration file checking with existing files.""" mock_exists.return_value = True mock_access.return_value = True with patch('pathlib.Path.stat') as mock_stat: mock_stat.return_value.st_size = 100 mock_stat.return_value.st_mtime = 1234567890 with patch('config.load_env_file', return_value={'TEST': 'value'}): result = self.config_commands._check_configuration_files() assert '.env.tddai' in result assert result['.env.tddai']['exists'] is True assert result['.env.tddai']['readable'] is True assert result['.env.tddai']['size'] == 100 def test_run_diagnostics_complete(self): """Test running complete diagnostics.""" config = self._get_mock_config() with patch.object(self.config_commands, '_check_environment') as mock_env, \ patch.object(self.config_commands, '_check_filesystem') as mock_fs, \ patch.object(self.config_commands, '_check_configuration_files') as mock_files, \ patch.object(self.config_commands, '_check_git_repository') as mock_git, \ patch.object(self.config_commands, '_check_network_connectivity') as mock_network: mock_env.return_value = {} mock_fs.return_value = {} mock_files.return_value = {} mock_git.return_value = {} mock_network.return_value = {} result = self.config_commands._run_diagnostics(config) assert 'environment' in result assert 'filesystem' in result assert 'config_files' in result assert 'git_repository' in result assert 'network' in result def test_run_basic_diagnostics(self): """Test running basic diagnostics when config fails.""" with patch.object(self.config_commands, '_check_environment') as mock_env, \ patch.object(self.config_commands, '_check_filesystem') as mock_fs, \ patch.object(self.config_commands, '_check_configuration_files') as mock_files, \ patch.object(self.config_commands, '_check_git_repository') as mock_git: mock_env.return_value = {} mock_fs.return_value = {} mock_files.return_value = {} mock_git.return_value = {} result = self.config_commands._run_basic_diagnostics() assert 'environment' in result assert 'filesystem' in result assert 'config_files' in result assert 'git_repository' in result assert 'network' not in result # Should not include network check class TestConfigPresenter: """Test suite for configuration presenter.""" def setup_method(self): """Set up test fixtures.""" self.presenter = ConfigPresenter() @patch('sys.stdout', new_callable=StringIO) def test_show_error(self, mock_stdout): """Test error display.""" self.presenter.show_error("Test error message") output = mock_stdout.getvalue() assert "❌ Test error message" in output @patch('sys.stdout', new_callable=StringIO) @patch('pathlib.Path.exists') @patch('pathlib.Path.iterdir') def test_show_gitea_configuration(self, mock_iterdir, mock_exists, mock_stdout): """Test Gitea configuration display.""" mock_exists.return_value = False # Don't check real filesystem mock_iterdir.return_value = [] config = MarkitectConfig( gitea_url="https://github.com", repo_owner="test_owner", repo_name="test_repo", workspace_dir=Path(".test_workspace") ) status = {'sources': {}} self.presenter.show_configuration(config, status, show_sensitive=False) output = mock_stdout.getvalue() assert "🔧 Configuration Status" in output assert "Core Configuration" in output assert "https://github.com" in output @patch('sys.stdout', new_callable=StringIO) def test_show_validation_results_success(self, mock_stdout): """Test validation results display with all success.""" results = [ {'check': 'Test 1', 'status': 'success', 'message': 'Good'}, {'check': 'Test 2', 'status': 'success', 'message': 'Also good'} ] self.presenter.show_validation_results(results) output = mock_stdout.getvalue() assert "✅ Configuration Validation" in output assert "2/2 checks passed" in output assert "✅ Test 1" in output assert "✅ Test 2" in output @patch('sys.stdout', new_callable=StringIO) def test_show_validation_results_with_errors(self, mock_stdout): """Test validation results display with errors.""" results = [ {'check': 'Good test', 'status': 'success', 'message': 'Good'}, {'check': 'Bad test', 'status': 'error', 'message': 'Bad', 'suggestion': 'Fix it'}, {'check': 'Warning test', 'status': 'warning', 'message': 'Warning', 'suggestion': 'Consider this'} ] self.presenter.show_validation_results(results) output = mock_stdout.getvalue() assert "1/3 checks passed" in output assert "⚠️ 1 warnings" in output assert "❌ 1 errors" in output assert "💡 Fix it" in output assert "💡 Consider this" in output @patch('sys.stdout', new_callable=StringIO) def test_show_config_file_status(self, mock_stdout): """Test configuration file status display.""" file_checks = { '.env.tddai': { 'path': '.env.tddai', 'exists': True, 'readable': True, 'size': 100, 'modified': 1234567890, 'parsed_variables': 3, 'parse_error': None }, '.env': { 'path': '.env', 'exists': False, 'readable': False, 'size': 0, 'modified': None } } self.presenter.show_config_file_status(file_checks) output = mock_stdout.getvalue() assert "📁 Configuration Files Status" in output assert "✅ .env.tddai" in output assert "❌ .env" in output assert "🔧 Variables: 3" in output