""" Tests for Issue #18: Configuration and Environment Management CLI This test suite verifies the configuration management functionality including: - Configuration display and management - Project initialization functionality - Configuration validation - Integration with existing config system - Environment variable handling """ import pytest import tempfile import shutil import os import json import yaml from pathlib import Path from unittest.mock import Mock, patch, MagicMock from click.testing import CliRunner from markitect.config_manager import ConfigurationManager from markitect.cli import cli class TestConfigurationManager: """Test the core ConfigurationManager functionality.""" def setup_method(self): """Set up test environment.""" self.temp_dir = tempfile.mkdtemp() self.test_dir = Path(self.temp_dir) self.original_cwd = os.getcwd() os.chdir(self.temp_dir) def teardown_method(self): """Clean up test environment.""" os.chdir(self.original_cwd) shutil.rmtree(self.temp_dir) def test_get_current_config(self): """Test getting current configuration.""" config_manager = ConfigurationManager() config = config_manager.get_current_config() # Should have basic configuration keys expected_keys = [ 'gitea_url', 'repo_owner', 'repo_name', 'api_token', 'workspace_dir', 'database_path', 'cache_dir', 'tests_dir' ] for key in expected_keys: assert key in config # Should have metadata assert '_meta' in config assert 'config_sources' in config['_meta'] assert 'env_variables' in config['_meta'] assert 'working_directory' in config['_meta'] def test_display_config_yaml_format(self): """Test configuration display in YAML format.""" config_manager = ConfigurationManager() output = config_manager.display_config(output_format='yaml') # Should be valid YAML parsed = yaml.safe_load(output) assert isinstance(parsed, dict) assert 'gitea_url' in parsed def test_display_config_json_format(self): """Test configuration display in JSON format.""" config_manager = ConfigurationManager() output = config_manager.display_config(output_format='json') # Should be valid JSON parsed = json.loads(output) assert isinstance(parsed, dict) assert 'gitea_url' in parsed def test_display_config_simple_format(self): """Test configuration display in simple format.""" config_manager = ConfigurationManager() output = config_manager.display_config(output_format='simple') # Should contain key=value pairs lines = output.split('\n') assert any('gitea_url =' in line for line in lines) assert any('repo_owner =' in line for line in lines) def test_mask_sensitive_data(self): """Test masking of sensitive configuration data.""" config_manager = ConfigurationManager() # Create config with sensitive data config = { 'api_token': 'secret123', 'password': 'mypassword', 'gitea_url': 'http://localhost:3000', 'repo_name': 'test-repo' } masked = config_manager._mask_sensitive_data(config) # Sensitive fields should be masked assert masked['api_token'] == '***MASKED***' assert masked['password'] == '***MASKED***' # Non-sensitive fields should remain assert masked['gitea_url'] == 'http://localhost:3000' assert masked['repo_name'] == 'test-repo' def test_mask_sensitive_data_preserves_empty_values(self): """Test that empty sensitive values are not masked.""" config_manager = ConfigurationManager() config = { 'api_token': '', 'secret': None, 'repo_name': 'test-repo' } masked = config_manager._mask_sensitive_data(config) # Empty/None values should not be masked assert masked['api_token'] == '' assert masked['secret'] is None assert masked['repo_name'] == 'test-repo' def test_set_config_value_simple(self): """Test setting a simple configuration value.""" config_manager = ConfigurationManager() success = config_manager.set_config_value('repo_name', 'test-repository') assert success # Verify file was created config_file = self.test_dir / '.markitect.yml' assert config_file.exists() # Verify content content = yaml.safe_load(config_file.read_text()) assert content['repo_name'] == 'test-repository' def test_set_config_value_nested(self): """Test setting nested configuration value with dot notation.""" config_manager = ConfigurationManager() success = config_manager.set_config_value('gitea.url', 'http://example.com') assert success # Verify nested structure config_file = self.test_dir / '.markitect.yml' content = yaml.safe_load(config_file.read_text()) assert content['gitea']['url'] == 'http://example.com' def test_set_config_value_type_conversion(self): """Test automatic type conversion for configuration values.""" config_manager = ConfigurationManager() # Test boolean conversion config_manager.set_config_value('debug', 'true') config_manager.set_config_value('verbose', 'false') # Test number conversion config_manager.set_config_value('port', '3000') config_manager.set_config_value('timeout', '30.5') config_file = self.test_dir / '.markitect.yml' content = yaml.safe_load(config_file.read_text()) assert content['debug'] is True assert content['verbose'] is False assert content['port'] == 3000 assert content['timeout'] == 30.5 def test_set_config_value_validation_error(self): """Test configuration validation during value setting.""" config_manager = ConfigurationManager() # Test invalid URL with pytest.raises(ValueError, match="not a valid URL format"): config_manager.set_config_value('gitea_url', 'invalid-url') def test_set_config_value_existing_file(self): """Test setting values in existing configuration file.""" # Create existing config file existing_config = { 'repo_name': 'old-name', 'gitea_url': 'http://localhost:3000' } config_file = self.test_dir / '.markitect.yml' config_file.write_text(yaml.dump(existing_config)) config_manager = ConfigurationManager() success = config_manager.set_config_value('repo_name', 'new-name') assert success # Verify update content = yaml.safe_load(config_file.read_text()) assert content['repo_name'] == 'new-name' assert content['gitea_url'] == 'http://localhost:3000' # Should be preserved def test_set_config_value_custom_file(self): """Test setting values in custom configuration file.""" custom_file = self.test_dir / 'custom.yml' config_manager = ConfigurationManager() success = config_manager.set_config_value('repo_name', 'test', str(custom_file)) assert success assert custom_file.exists() content = yaml.safe_load(custom_file.read_text()) assert content['repo_name'] == 'test' def test_initialize_project_config(self): """Test project configuration initialization.""" config_manager = ConfigurationManager() result = config_manager.initialize_project_config(self.test_dir, interactive=False) # Verify config file created config_file = Path(result['config_file']) assert config_file.exists() assert config_file.name == '.markitect.yml' # Verify directories created assert len(result['created_directories']) > 0 for directory in result['created_directories']: assert Path(directory).exists() # Verify config structure config = result['config'] assert 'gitea_url' in config assert 'repo_name' in config assert config['repo_name'] == self.test_dir.name def test_initialize_project_config_custom_dir(self): """Test project initialization in custom directory.""" project_dir = self.test_dir / 'my-project' config_manager = ConfigurationManager() result = config_manager.initialize_project_config(project_dir, interactive=False) # Verify correct location config_file = Path(result['config_file']) assert config_file.parent == project_dir assert project_dir.exists() def test_validate_configuration_success(self): """Test configuration validation with valid config.""" config = { 'gitea_url': 'http://localhost:3000', 'database_path': str(self.test_dir / 'db.sqlite'), 'workspace_dir': str(self.test_dir / 'workspace'), 'cache_dir': str(self.test_dir / 'cache'), 'tests_dir': str(self.test_dir / 'tests') } config_manager = ConfigurationManager() results = config_manager.validate_configuration(config) # Should have validation results assert len(results) > 0 # Check for required field validations required_checks = [r for r in results if r['key'] in ['gitea_url', 'database_path']] assert len(required_checks) > 0 def test_validate_configuration_missing_required(self): """Test configuration validation with missing required fields.""" config = { 'repo_name': 'test' # Missing gitea_url and database_path } config_manager = ConfigurationManager() results = config_manager.validate_configuration(config) # Should have errors for missing required fields errors = [r for r in results if r['status'] == 'error'] assert len(errors) > 0 error_keys = [r['key'] for r in errors] assert 'gitea_url' in error_keys or 'database_path' in error_keys def test_validate_configuration_path_creation(self): """Test configuration validation creates missing directories.""" config = { 'gitea_url': 'http://localhost:3000', 'database_path': str(self.test_dir / 'data' / 'db.sqlite'), 'workspace_dir': str(self.test_dir / 'new_workspace'), 'cache_dir': str(self.test_dir / 'new_cache'), 'tests_dir': str(self.test_dir / 'new_tests') } config_manager = ConfigurationManager() results = config_manager.validate_configuration(config) # Directories should be created assert (self.test_dir / 'new_workspace').exists() assert (self.test_dir / 'new_cache').exists() assert (self.test_dir / 'new_tests').exists() assert (self.test_dir / 'data').exists() # Database parent directory # Should have warnings for created directories warnings = [r for r in results if r['status'] == 'warning'] assert len(warnings) > 0 def test_list_config_keys(self): """Test listing available configuration keys.""" config_manager = ConfigurationManager() keys = config_manager.list_config_keys() # Should return list of tuples assert isinstance(keys, list) assert len(keys) > 0 # Each item should be (key, description, default) for item in keys: assert len(item) == 3 assert isinstance(item[0], str) # key assert isinstance(item[1], str) # description # Should include expected keys key_names = [item[0] for item in keys] assert 'gitea_url' in key_names assert 'repo_name' in key_names assert 'api_token' in key_names def test_get_config_help_specific_key(self): """Test getting help for specific configuration key.""" config_manager = ConfigurationManager() help_text = config_manager.get_config_help('gitea_url') assert 'gitea_url' in help_text assert 'description' in help_text.lower() or ':' in help_text def test_get_config_help_unknown_key(self): """Test getting help for unknown configuration key.""" config_manager = ConfigurationManager() help_text = config_manager.get_config_help('unknown_key') assert 'unknown' in help_text.lower() def test_get_config_help_general(self): """Test getting general configuration help.""" config_manager = ConfigurationManager() help_text = config_manager.get_config_help() assert 'available' in help_text.lower() or 'configuration' in help_text.lower() assert 'gitea_url' in help_text assert 'repo_name' in help_text def test_convert_value_booleans(self): """Test value conversion for boolean types.""" config_manager = ConfigurationManager() # True values for true_val in ['true', 'True', 'TRUE', 'yes', 'YES', 'on', 'ON', '1']: assert config_manager._convert_value(true_val) is True # False values for false_val in ['false', 'False', 'FALSE', 'no', 'NO', 'off', 'OFF', '0']: assert config_manager._convert_value(false_val) is False def test_convert_value_numbers(self): """Test value conversion for numeric types.""" config_manager = ConfigurationManager() # Integers assert config_manager._convert_value('42') == 42 assert config_manager._convert_value('-10') == -10 # Floats assert config_manager._convert_value('3.14') == 3.14 assert config_manager._convert_value('-2.5') == -2.5 # Strings that look like numbers but aren't assert config_manager._convert_value('not-a-number') == 'not-a-number' def test_is_valid_url(self): """Test URL validation.""" config_manager = ConfigurationManager() # Valid URLs valid_urls = [ 'http://localhost:3000', 'https://example.com', 'http://192.168.1.1:8080', 'https://api.github.com/repos' ] for url in valid_urls: assert config_manager._is_valid_url(url), f"Should be valid: {url}" # Invalid URLs invalid_urls = [ 'not-a-url', 'ftp://example.com', 'localhost:3000', 'http://', '' ] for url in invalid_urls: assert not config_manager._is_valid_url(url), f"Should be invalid: {url}" def test_is_valid_path(self): """Test path validation.""" config_manager = ConfigurationManager() # Valid paths valid_paths = [ '/absolute/path', './relative/path', '~/home/path', 'simple-path', str(self.test_dir / 'subdir') ] for path in valid_paths: assert config_manager._is_valid_path(path), f"Should be valid: {path}" # Edge case: empty string (should be considered valid as it can represent current directory) assert config_manager._is_valid_path('') class TestConfigFileParsing: """Test configuration file parsing and saving.""" def setup_method(self): """Set up test environment.""" self.temp_dir = tempfile.mkdtemp() self.test_dir = Path(self.temp_dir) self.original_cwd = os.getcwd() os.chdir(self.temp_dir) def teardown_method(self): """Clean up test environment.""" os.chdir(self.original_cwd) shutil.rmtree(self.temp_dir) def test_load_yaml_config_file(self): """Test loading YAML configuration file.""" config_content = { 'gitea_url': 'http://localhost:3000', 'repo_name': 'test-repo', 'nested': { 'key': 'value' } } config_file = self.test_dir / 'test.yml' config_file.write_text(yaml.dump(config_content)) config_manager = ConfigurationManager() loaded = config_manager._load_config_file(config_file) assert loaded == config_content def test_load_json_config_file(self): """Test loading JSON configuration file.""" config_content = { 'gitea_url': 'http://localhost:3000', 'repo_name': 'test-repo', 'debug': True } config_file = self.test_dir / 'test.json' config_file.write_text(json.dumps(config_content)) config_manager = ConfigurationManager() loaded = config_manager._load_config_file(config_file) assert loaded == config_content def test_save_yaml_config_file(self): """Test saving YAML configuration file.""" config_content = { 'gitea_url': 'http://localhost:3000', 'repo_name': 'test-repo' } config_file = self.test_dir / 'output.yml' config_manager = ConfigurationManager() config_manager._save_config_file(config_content, config_file) assert config_file.exists() loaded = yaml.safe_load(config_file.read_text()) assert loaded == config_content def test_save_json_config_file(self): """Test saving JSON configuration file.""" config_content = { 'gitea_url': 'http://localhost:3000', 'repo_name': 'test-repo' } config_file = self.test_dir / 'output.json' config_manager = ConfigurationManager() config_manager._save_config_file(config_content, config_file) assert config_file.exists() loaded = json.loads(config_file.read_text()) assert loaded == config_content def test_load_invalid_config_file(self): """Test loading invalid configuration file.""" config_file = self.test_dir / 'invalid.yml' config_file.write_text('invalid: yaml: content: [') config_manager = ConfigurationManager() with pytest.raises(ValueError, match="Failed to load config file"): config_manager._load_config_file(config_file) def test_get_target_config_file_existing(self): """Test getting target config file when one exists.""" # Create an existing config file existing_file = self.test_dir / '.markitect.yml' existing_file.write_text('test: value') config_manager = ConfigurationManager() target = config_manager._get_target_config_file() # Should be relative path that matches the existing file assert target.name == existing_file.name assert target.exists() def test_get_target_config_file_default(self): """Test getting target config file when none exists.""" config_manager = ConfigurationManager() target = config_manager._get_target_config_file() # Should be the default config file name assert target.name == '.markitect.yml' def test_get_target_config_file_custom(self): """Test getting custom target config file.""" custom_file = 'custom-config.yml' config_manager = ConfigurationManager() target = config_manager._get_target_config_file(custom_file) assert target == Path(custom_file) class TestCLIIntegration: """Test CLI command integration.""" def setup_method(self): """Set up test environment.""" self.temp_dir = tempfile.mkdtemp() self.test_dir = Path(self.temp_dir) self.original_cwd = os.getcwd() os.chdir(self.temp_dir) self.runner = CliRunner() def teardown_method(self): """Clean up test environment.""" os.chdir(self.original_cwd) shutil.rmtree(self.temp_dir) def test_config_show_command(self): """Test config-show CLI command.""" result = self.runner.invoke(cli, ['config-show']) assert result.exit_code == 0 assert 'gitea_url' in result.output def test_config_show_command_json_format(self): """Test config-show with JSON format.""" result = self.runner.invoke(cli, ['config-show', '--format', 'json']) assert result.exit_code == 0 # Output should be valid JSON try: json.loads(result.output) except json.JSONDecodeError: pytest.fail("Output is not valid JSON") def test_config_show_command_simple_format(self): """Test config-show with simple format.""" result = self.runner.invoke(cli, ['config-show', '--format', 'simple']) assert result.exit_code == 0 assert '=' in result.output # Should contain key=value pairs def test_config_set_command(self): """Test config-set CLI command.""" result = self.runner.invoke(cli, [ 'config-set', 'repo_name', 'test-repository' ]) assert result.exit_code == 0 assert 'Configuration updated' in result.output # Verify file was created config_file = self.test_dir / '.markitect.yml' assert config_file.exists() content = yaml.safe_load(config_file.read_text()) assert content['repo_name'] == 'test-repository' def test_config_set_command_nested_key(self): """Test config-set with nested key.""" result = self.runner.invoke(cli, [ 'config-set', 'gitea.url', 'http://example.com' ]) assert result.exit_code == 0 config_file = self.test_dir / '.markitect.yml' content = yaml.safe_load(config_file.read_text()) assert content['gitea']['url'] == 'http://example.com' def test_config_set_command_custom_file(self): """Test config-set with custom config file.""" custom_file = 'custom.yml' result = self.runner.invoke(cli, [ 'config-set', 'repo_name', 'test', '--config-file', custom_file ]) assert result.exit_code == 0 assert Path(custom_file).exists() def test_config_set_command_no_validate(self): """Test config-set with validation disabled.""" result = self.runner.invoke(cli, [ 'config-set', 'repo_name', 'test', '--no-validate' ]) assert result.exit_code == 0 def test_config_set_command_validation_error(self): """Test config-set with validation error.""" result = self.runner.invoke(cli, [ 'config-set', 'gitea_url', 'invalid-url' ]) assert result.exit_code == 1 assert 'Configuration error' in result.output def test_config_init_command_non_interactive(self): """Test config-init CLI command in non-interactive mode.""" result = self.runner.invoke(cli, [ 'config-init', '--no-interactive' ]) assert result.exit_code == 0 assert 'initialized successfully' in result.output # Verify config file created config_file = self.test_dir / '.markitect.yml' assert config_file.exists() # Verify directories created assert (self.test_dir / '.markitect_workspace').exists() assert (self.test_dir / '.ast_cache').exists() assert (self.test_dir / 'tests').exists() def test_config_init_command_custom_directory(self): """Test config-init with custom project directory.""" project_dir = self.test_dir / 'my-project' result = self.runner.invoke(cli, [ 'config-init', '--project-dir', str(project_dir), '--no-interactive' ]) assert result.exit_code == 0 # Verify in correct location config_file = project_dir / '.markitect.yml' assert config_file.exists() def test_config_init_command_existing_file_no_force(self): """Test config-init with existing file without force.""" # Create existing config file config_file = self.test_dir / '.markitect.yml' config_file.write_text('existing: config') result = self.runner.invoke(cli, [ 'config-init', '--no-interactive' ]) assert result.exit_code == 1 assert 'already exists' in result.output def test_config_init_command_existing_file_with_force(self): """Test config-init with existing file with force.""" # Create existing config file config_file = self.test_dir / '.markitect.yml' config_file.write_text('existing: config') result = self.runner.invoke(cli, [ 'config-init', '--no-interactive', '--force' ]) assert result.exit_code == 0 assert 'initialized successfully' in result.output def test_config_validate_command(self): """Test config-validate CLI command.""" result = self.runner.invoke(cli, ['config-validate']) assert result.exit_code in [0, 1] # May have warnings/errors assert 'Configuration Validation Summary' in result.output def test_config_validate_command_verbose(self): """Test config-validate with verbose output.""" result = self.runner.invoke(cli, ['config-validate', '--verbose']) assert result.exit_code in [0, 1] assert 'Configuration Validation Summary' in result.output def test_config_help_command_general(self): """Test config-help CLI command.""" result = self.runner.invoke(cli, ['config-help']) assert result.exit_code == 0 assert 'available' in result.output.lower() or 'configuration' in result.output.lower() assert 'gitea_url' in result.output def test_config_help_command_specific_key(self): """Test config-help for specific key.""" result = self.runner.invoke(cli, ['config-help', 'gitea_url']) assert result.exit_code == 0 assert 'gitea_url' in result.output def test_config_help_command_unknown_key(self): """Test config-help for unknown key.""" result = self.runner.invoke(cli, ['config-help', 'unknown_key']) assert result.exit_code == 0 assert 'unknown' in result.output.lower() class TestEnvironmentVariables: """Test environment variable handling.""" def setup_method(self): """Set up test environment.""" self.temp_dir = tempfile.mkdtemp() self.test_dir = Path(self.temp_dir) self.original_cwd = os.getcwd() os.chdir(self.temp_dir) # Store original environment self.original_env = dict(os.environ) def teardown_method(self): """Clean up test environment.""" os.chdir(self.original_cwd) shutil.rmtree(self.temp_dir) # Restore original environment os.environ.clear() os.environ.update(self.original_env) def test_get_relevant_env_vars(self): """Test getting MARKITECT-related environment variables.""" # Set some test environment variables os.environ['MARKITECT_GITEA_URL'] = 'http://test.com' os.environ['MARKITECT_REPO_NAME'] = 'test-repo' os.environ['OTHER_VAR'] = 'should-be-ignored' config_manager = ConfigurationManager() env_vars = config_manager._get_relevant_env_vars() assert 'MARKITECT_GITEA_URL' in env_vars assert 'MARKITECT_REPO_NAME' in env_vars assert 'OTHER_VAR' not in env_vars assert env_vars['MARKITECT_GITEA_URL'] == 'http://test.com' def test_config_with_env_vars(self): """Test configuration loading with environment variables.""" # Set environment variables os.environ['MARKITECT_GITEA_URL'] = 'http://env-test.com' os.environ['MARKITECT_REPO_NAME'] = 'env-repo' config_manager = ConfigurationManager() config = config_manager.get_current_config() # Environment variables should be reflected in config assert config['gitea_url'] == 'http://env-test.com' assert config['repo_name'] == 'env-repo' # Should have env vars in metadata assert 'MARKITECT_GITEA_URL' in config['_meta']['env_variables'] assert 'MARKITECT_REPO_NAME' in config['_meta']['env_variables'] class TestEdgeCases: """Test edge cases and error conditions.""" def setup_method(self): """Set up test environment.""" self.temp_dir = tempfile.mkdtemp() self.test_dir = Path(self.temp_dir) self.original_cwd = os.getcwd() os.chdir(self.temp_dir) def teardown_method(self): """Clean up test environment.""" os.chdir(self.original_cwd) shutil.rmtree(self.temp_dir) def test_config_manager_with_permission_error(self): """Test handling permission errors.""" # Create a directory we can't write to restricted_dir = self.test_dir / 'restricted' restricted_dir.mkdir(mode=0o444) # Read-only config_manager = ConfigurationManager() # This should not crash, but may warn about permission issues try: config = config_manager.get_current_config() assert isinstance(config, dict) except PermissionError: # Acceptable to fail with permission error pass def test_set_config_value_invalid_file_path(self): """Test setting config value with invalid file path.""" config_manager = ConfigurationManager() # Try to write to a path that doesn't exist and can't be created # Use a more reliable invalid path that doesn't depend on system permissions invalid_path = '/nonexistent_directory_12345/config.yml' with pytest.raises(ValueError): config_manager.set_config_value('test', 'value', invalid_path) def test_validate_configuration_with_none(self): """Test configuration validation with None input.""" config_manager = ConfigurationManager() # Should not crash with None input results = config_manager.validate_configuration(None) assert isinstance(results, list) def test_mask_sensitive_data_with_complex_structure(self): """Test masking sensitive data in complex nested structure.""" config_manager = ConfigurationManager() complex_config = { 'database': { 'password': 'secret123', 'host': 'localhost' }, 'apis': [ {'name': 'github', 'token': 'ghp_secret'}, {'name': 'gitea', 'url': 'http://localhost:3000'} ], 'secrets': { 'nested': { 'api_key': 'very_secret' } } } masked = config_manager._mask_sensitive_data(complex_config) # Sensitive fields should be masked at any level assert masked['database']['password'] == '***MASKED***' assert masked['apis'][0]['token'] == '***MASKED***' # The 'secrets' key itself gets masked because it's a sensitive keyword # This is actually correct behavior assert masked['secrets'] == '***MASKED***' # Non-sensitive fields should remain assert masked['database']['host'] == 'localhost' assert masked['apis'][1]['url'] == 'http://localhost:3000' def test_get_config_sources_empty_directory(self): """Test getting config sources in empty directory.""" config_manager = ConfigurationManager() sources = config_manager._get_config_sources() # Should always include defaults assert len(sources) > 0 assert any('defaults' in source.lower() for source in sources) def test_initialize_project_config_existing_directories(self): """Test project initialization with existing directories.""" # Pre-create some directories (self.test_dir / '.markitect_workspace').mkdir() (self.test_dir / 'tests').mkdir() config_manager = ConfigurationManager() result = config_manager.initialize_project_config(self.test_dir, interactive=False) # Should succeed even with existing directories assert 'config_file' in result assert Path(result['config_file']).exists()