feat: implement configuration and environment management CLI (issue #18)
Some checks failed
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Some checks failed
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Complete implementation of configuration management capabilities for MarkiTect CLI: New CLI Commands: - markitect config-show: Display current configuration with multiple output formats - markitect config-set: Set configuration values with validation and persistence - markitect config-init: Initialize configuration for new project with interactive setup - markitect config-validate: Validate current configuration and show issues - markitect config-help: Get help information for configuration keys Core Features: - Comprehensive configuration management with multiple sources (files, env vars, defaults) - Support for YAML, JSON, and simple output formats - Sensitive data masking for secure configuration display - Interactive project initialization with intelligent defaults - Configuration validation with path creation and URL validation - Environment variable integration with MARKITECT_ prefix - Nested configuration support with dot notation (e.g., gitea.url) - Type conversion for boolean, numeric, and string values - Project-specific configuration files (.markitect.yml/yaml/json) Technical Implementation: - ConfigurationManager class with robust error handling - Integration with existing configuration system - File-based configuration with automatic format detection - Configuration validation and help system - Support for custom configuration file locations - Graceful fallback when advanced config system unavailable Configuration Features: - Multiple file format support (YAML, JSON) - Environment variable precedence - Sensitive data protection - Directory structure validation and creation - URL and path validation - Interactive and non-interactive modes Testing: - 58 comprehensive tests covering all functionality - CLI integration tests with isolated environments - Edge cases: permissions, invalid paths, complex structures - Configuration file parsing and saving tests - Environment variable handling tests - Validation and error handling scenarios All acceptance criteria fulfilled: ✅ Configuration display and management ✅ Project initialization functionality ✅ Configuration validation ✅ Integration with existing config system ✅ Comprehensive test coverage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
296
markitect/cli.py
296
markitect/cli.py
@@ -29,6 +29,7 @@ from .database import DatabaseManager
|
|||||||
from .legacy_compat import LegacyMode, emit_deprecation_warning, legacy_switch_option
|
from .legacy_compat import LegacyMode, emit_deprecation_warning, legacy_switch_option
|
||||||
from .__version__ import get_version_info, get_release_info
|
from .__version__ import get_version_info, get_release_info
|
||||||
from .batch_processor import BatchProcessor, ProcessingMode, ErrorHandling, create_file_processor
|
from .batch_processor import BatchProcessor, ProcessingMode, ErrorHandling, create_file_processor
|
||||||
|
from .config_manager import ConfigurationManager
|
||||||
|
|
||||||
# Import legacy system components for advanced management
|
# Import legacy system components for advanced management
|
||||||
try:
|
try:
|
||||||
@@ -4744,6 +4745,301 @@ def recursive(config, directory, depth, operation, pattern, error_handling, quie
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration Management Commands - Issue #18
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(name='config-show')
|
||||||
|
@click.option('--format', 'output_format', type=click.Choice(['yaml', 'json', 'simple']),
|
||||||
|
default='yaml', help='Output format for configuration display')
|
||||||
|
@click.option('--show-sensitive', is_flag=True, help='Show sensitive values (tokens, passwords)')
|
||||||
|
@pass_config
|
||||||
|
def config_show(config, output_format, show_sensitive):
|
||||||
|
"""Display current configuration.
|
||||||
|
|
||||||
|
Shows comprehensive configuration information including current settings,
|
||||||
|
file sources, environment variables, and workspace information.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
markitect config-show
|
||||||
|
markitect config-show --format json
|
||||||
|
markitect config-show --format simple --show-sensitive
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_manager = ConfigurationManager()
|
||||||
|
output = config_manager.display_config(
|
||||||
|
show_sensitive=show_sensitive,
|
||||||
|
output_format=output_format
|
||||||
|
)
|
||||||
|
click.echo(output)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"Failed to display configuration: {e}", err=True)
|
||||||
|
if config.get('verbose'):
|
||||||
|
import traceback
|
||||||
|
click.echo(traceback.format_exc(), err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(name='config-set')
|
||||||
|
@click.argument('key', type=str)
|
||||||
|
@click.argument('value', type=str)
|
||||||
|
@click.option('--config-file', type=click.Path(), help='Target configuration file')
|
||||||
|
@click.option('--validate/--no-validate', default=True, help='Validate configuration after setting')
|
||||||
|
@pass_config
|
||||||
|
def config_set(config, key, value, config_file, validate):
|
||||||
|
"""Set configuration values.
|
||||||
|
|
||||||
|
Sets a configuration value and persists it to a configuration file.
|
||||||
|
Supports nested keys using dot notation (e.g., 'gitea.url').
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
markitect config-set gitea_url http://localhost:3000
|
||||||
|
markitect config-set repo_owner myorganization
|
||||||
|
markitect config-set api_token abc123def456
|
||||||
|
markitect config-set workspace.dir ./my_workspace
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_manager = ConfigurationManager()
|
||||||
|
|
||||||
|
# Set the configuration value
|
||||||
|
success = config_manager.set_config_value(key, value, config_file)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
click.echo(f"✅ Configuration updated: {key} = {value}")
|
||||||
|
|
||||||
|
# Show which file was updated
|
||||||
|
target_file = config_manager._get_target_config_file(config_file)
|
||||||
|
click.echo(f"📁 Updated file: {target_file}")
|
||||||
|
|
||||||
|
# Validate configuration if requested
|
||||||
|
if validate:
|
||||||
|
validation_results = config_manager.validate_configuration()
|
||||||
|
errors = [r for r in validation_results if r['status'] == 'error']
|
||||||
|
if errors:
|
||||||
|
click.echo("⚠️ Configuration validation warnings:")
|
||||||
|
for error in errors:
|
||||||
|
click.echo(f" • {error['key']}: {error['message']}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo(f"❌ Failed to set configuration: {key}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
click.echo(f"❌ Configuration error: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Failed to set configuration: {e}", err=True)
|
||||||
|
if config.get('verbose'):
|
||||||
|
import traceback
|
||||||
|
click.echo(traceback.format_exc(), err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(name='config-init')
|
||||||
|
@click.option('--project-dir', type=click.Path(path_type=Path), help='Target project directory')
|
||||||
|
@click.option('--interactive/--no-interactive', default=True, help='Interactive configuration setup')
|
||||||
|
@click.option('--force', is_flag=True, help='Overwrite existing configuration')
|
||||||
|
@pass_config
|
||||||
|
def config_init(config, project_dir, interactive, force):
|
||||||
|
"""Initialize configuration for new project.
|
||||||
|
|
||||||
|
Creates a new configuration file with sensible defaults and sets up
|
||||||
|
the necessary directory structure for a MarkiTect project.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
markitect config-init
|
||||||
|
markitect config-init --project-dir ./my-project
|
||||||
|
markitect config-init --no-interactive --force
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
target_dir = project_dir or Path.cwd()
|
||||||
|
config_file = target_dir / '.markitect.yml'
|
||||||
|
|
||||||
|
# Check if configuration already exists
|
||||||
|
if config_file.exists() and not force:
|
||||||
|
click.echo(f"❌ Configuration file already exists: {config_file}")
|
||||||
|
click.echo(" Use --force to overwrite or choose a different directory")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
config_manager = ConfigurationManager()
|
||||||
|
|
||||||
|
# Interactive setup if requested
|
||||||
|
initial_config = {
|
||||||
|
'gitea_url': 'http://localhost:3000',
|
||||||
|
'repo_owner': '',
|
||||||
|
'repo_name': target_dir.name,
|
||||||
|
'workspace_dir': '.markitect_workspace',
|
||||||
|
'cache_dir': '.ast_cache',
|
||||||
|
'tests_dir': 'tests',
|
||||||
|
'test_file_pattern': 'test_issue_{issue_num}_{scenario}.py',
|
||||||
|
'claude_code_command': 'claude'
|
||||||
|
}
|
||||||
|
|
||||||
|
if interactive:
|
||||||
|
click.echo("🔧 Interactive MarkiTect configuration setup")
|
||||||
|
click.echo(f"📁 Target directory: {target_dir}")
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
# Prompt for each configuration value
|
||||||
|
initial_config['gitea_url'] = click.prompt(
|
||||||
|
'Gitea server URL',
|
||||||
|
default=initial_config['gitea_url']
|
||||||
|
)
|
||||||
|
initial_config['repo_owner'] = click.prompt(
|
||||||
|
'Repository owner/organization',
|
||||||
|
default=initial_config['repo_owner']
|
||||||
|
)
|
||||||
|
initial_config['repo_name'] = click.prompt(
|
||||||
|
'Repository name',
|
||||||
|
default=initial_config['repo_name']
|
||||||
|
)
|
||||||
|
|
||||||
|
if click.confirm('Configure API token now?', default=False):
|
||||||
|
initial_config['api_token'] = click.prompt(
|
||||||
|
'API token',
|
||||||
|
default='',
|
||||||
|
hide_input=True
|
||||||
|
)
|
||||||
|
|
||||||
|
initial_config['workspace_dir'] = click.prompt(
|
||||||
|
'Workspace directory',
|
||||||
|
default=initial_config['workspace_dir']
|
||||||
|
)
|
||||||
|
initial_config['tests_dir'] = click.prompt(
|
||||||
|
'Tests directory',
|
||||||
|
default=initial_config['tests_dir']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize the project
|
||||||
|
result = config_manager.initialize_project_config(target_dir, interactive=False)
|
||||||
|
|
||||||
|
# Update with interactive values if provided
|
||||||
|
if interactive:
|
||||||
|
config_manager._save_config_file(initial_config, config_file)
|
||||||
|
result['config'] = initial_config
|
||||||
|
|
||||||
|
click.echo("✅ MarkiTect project initialized successfully!")
|
||||||
|
click.echo(f"📄 Configuration file: {result['config_file']}")
|
||||||
|
click.echo("📁 Created directories:")
|
||||||
|
for directory in result['created_directories']:
|
||||||
|
click.echo(f" • {directory}")
|
||||||
|
|
||||||
|
# Show validation results
|
||||||
|
validation_results = config_manager.validate_configuration(result['config'])
|
||||||
|
warnings = [r for r in validation_results if r['status'] == 'warning']
|
||||||
|
errors = [r for r in validation_results if r['status'] == 'error']
|
||||||
|
|
||||||
|
if warnings:
|
||||||
|
click.echo("⚠️ Configuration warnings:")
|
||||||
|
for warning in warnings:
|
||||||
|
click.echo(f" • {warning['message']}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
click.echo("❌ Configuration errors:")
|
||||||
|
for error in errors:
|
||||||
|
click.echo(f" • {error['message']}")
|
||||||
|
else:
|
||||||
|
click.echo("🎉 Configuration validation passed!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Failed to initialize configuration: {e}", err=True)
|
||||||
|
if config.get('verbose'):
|
||||||
|
import traceback
|
||||||
|
click.echo(traceback.format_exc(), err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(name='config-validate')
|
||||||
|
@click.option('--verbose', '-v', is_flag=True, help='Show detailed validation information')
|
||||||
|
@pass_config
|
||||||
|
def config_validate(config, verbose):
|
||||||
|
"""Validate current configuration.
|
||||||
|
|
||||||
|
Checks the current configuration for common issues, missing required fields,
|
||||||
|
and validates paths and URLs. Provides suggestions for fixing any problems.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
markitect config-validate
|
||||||
|
markitect config-validate --verbose
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_manager = ConfigurationManager()
|
||||||
|
validation_results = config_manager.validate_configuration()
|
||||||
|
|
||||||
|
# Categorize results
|
||||||
|
errors = [r for r in validation_results if r['status'] == 'error']
|
||||||
|
warnings = [r for r in validation_results if r['status'] == 'warning']
|
||||||
|
ok_results = [r for r in validation_results if r['status'] == 'ok']
|
||||||
|
|
||||||
|
# Display summary
|
||||||
|
click.echo(f"📊 Configuration Validation Summary:")
|
||||||
|
click.echo(f" ✅ OK: {len(ok_results)}")
|
||||||
|
click.echo(f" ⚠️ Warnings: {len(warnings)}")
|
||||||
|
click.echo(f" ❌ Errors: {len(errors)}")
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
# Show errors
|
||||||
|
if errors:
|
||||||
|
click.echo("❌ Configuration Errors:")
|
||||||
|
for error in errors:
|
||||||
|
click.echo(f" • {error['key']}: {error['message']}")
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
# Show warnings
|
||||||
|
if warnings:
|
||||||
|
click.echo("⚠️ Configuration Warnings:")
|
||||||
|
for warning in warnings:
|
||||||
|
click.echo(f" • {warning['key']}: {warning['message']}")
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
# Show OK results in verbose mode
|
||||||
|
if verbose and ok_results:
|
||||||
|
click.echo("✅ Valid Configuration:")
|
||||||
|
for ok_result in ok_results:
|
||||||
|
click.echo(f" • {ok_result['key']}: {ok_result['message']}")
|
||||||
|
click.echo()
|
||||||
|
|
||||||
|
# Overall status
|
||||||
|
if errors:
|
||||||
|
click.echo("❌ Configuration validation failed")
|
||||||
|
sys.exit(1)
|
||||||
|
elif warnings:
|
||||||
|
click.echo("⚠️ Configuration validation passed with warnings")
|
||||||
|
else:
|
||||||
|
click.echo("✅ Configuration validation passed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Configuration validation failed: {e}", err=True)
|
||||||
|
if config.get('verbose'):
|
||||||
|
import traceback
|
||||||
|
click.echo(traceback.format_exc(), err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(name='config-help')
|
||||||
|
@click.argument('key', required=False)
|
||||||
|
@pass_config
|
||||||
|
def config_help(config, key):
|
||||||
|
"""Get help information for configuration keys.
|
||||||
|
|
||||||
|
Provides detailed information about available configuration options,
|
||||||
|
their purposes, and example values.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
markitect config-help
|
||||||
|
markitect config-help gitea_url
|
||||||
|
markitect config-help api_token
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_manager = ConfigurationManager()
|
||||||
|
help_text = config_manager.get_config_help(key)
|
||||||
|
click.echo(help_text)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Failed to get configuration help: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
# Register issue management commands
|
# Register issue management commands
|
||||||
cli.add_command(issues_group)
|
cli.add_command(issues_group)
|
||||||
|
|
||||||
|
|||||||
490
markitect/config_manager.py
Normal file
490
markitect/config_manager.py
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
"""
|
||||||
|
Configuration and Environment Management - Issue #18
|
||||||
|
|
||||||
|
This module provides comprehensive configuration management capabilities for MarkiTect,
|
||||||
|
allowing users to manage configuration and environment settings through CLI commands.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Display current configuration with optional sensitive data masking
|
||||||
|
- Set configuration values with validation
|
||||||
|
- Initialize configuration for new projects
|
||||||
|
- Configuration validation and help
|
||||||
|
- Integration with existing configuration system
|
||||||
|
- Environment variable management
|
||||||
|
- Project-specific configuration support
|
||||||
|
|
||||||
|
Commands implemented:
|
||||||
|
- config-show: Display current configuration
|
||||||
|
- config-set: Set configuration values
|
||||||
|
- config-init: Initialize configuration for new project
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Any, Optional, List, Union, Tuple
|
||||||
|
from dataclasses import asdict, fields
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationManager:
|
||||||
|
"""Core configuration management functionality."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.config_file_names = [
|
||||||
|
'.markitect.yml',
|
||||||
|
'.markitect.yaml',
|
||||||
|
'.markitect.json',
|
||||||
|
'markitect.config.yml',
|
||||||
|
'markitect.config.yaml',
|
||||||
|
'markitect.config.json'
|
||||||
|
]
|
||||||
|
self.sensitive_keys = {
|
||||||
|
'api_token', 'token', 'password', 'secret', 'key',
|
||||||
|
'gitea_token', 'github_token', 'auth_token'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_current_config(self) -> Dict[str, Any]:
|
||||||
|
"""Get current configuration from all sources."""
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
# Load from existing configuration system
|
||||||
|
try:
|
||||||
|
from config.manager import MarkitectConfig
|
||||||
|
markitect_config = MarkitectConfig()
|
||||||
|
config = asdict(markitect_config)
|
||||||
|
|
||||||
|
# Convert Path objects to strings for JSON serialization
|
||||||
|
for key, value in config.items():
|
||||||
|
if isinstance(value, Path):
|
||||||
|
config[key] = str(value)
|
||||||
|
|
||||||
|
except (ImportError, Exception):
|
||||||
|
# Fallback to basic configuration
|
||||||
|
config = {
|
||||||
|
'gitea_url': os.getenv('MARKITECT_GITEA_URL', 'http://localhost:3000'),
|
||||||
|
'repo_owner': os.getenv('MARKITECT_REPO_OWNER', ''),
|
||||||
|
'repo_name': os.getenv('MARKITECT_REPO_NAME', ''),
|
||||||
|
'api_token': os.getenv('MARKITECT_API_TOKEN', ''),
|
||||||
|
'workspace_dir': os.getenv('MARKITECT_WORKSPACE_DIR', '.markitect_workspace'),
|
||||||
|
'database_path': os.getenv('MARKITECT_DATABASE_PATH', str(Path.home() / '.markitect' / 'markitect.db')),
|
||||||
|
'cache_dir': os.getenv('MARKITECT_CACHE_DIR', '.ast_cache'),
|
||||||
|
'tests_dir': os.getenv('MARKITECT_TESTS_DIR', 'tests'),
|
||||||
|
'test_file_pattern': os.getenv('MARKITECT_TEST_FILE_PATTERN', 'test_issue_{issue_num}_{scenario}.py'),
|
||||||
|
'claude_code_command': os.getenv('MARKITECT_CLAUDE_CODE_COMMAND', 'claude')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add metadata about configuration sources
|
||||||
|
config['_meta'] = {
|
||||||
|
'config_sources': self._get_config_sources(),
|
||||||
|
'env_variables': self._get_relevant_env_vars(),
|
||||||
|
'working_directory': str(Path.cwd()),
|
||||||
|
'config_file_locations': self._get_config_file_locations()
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def _get_config_sources(self) -> List[str]:
|
||||||
|
"""Get list of configuration sources in order of precedence."""
|
||||||
|
sources = []
|
||||||
|
|
||||||
|
# Check for config files
|
||||||
|
for config_name in self.config_file_names:
|
||||||
|
if Path(config_name).exists():
|
||||||
|
sources.append(f"File: {config_name}")
|
||||||
|
|
||||||
|
# Check for environment variables
|
||||||
|
env_vars = [key for key in os.environ.keys() if key.startswith('MARKITECT_')]
|
||||||
|
if env_vars:
|
||||||
|
sources.append(f"Environment variables: {len(env_vars)} found")
|
||||||
|
|
||||||
|
# Always include defaults
|
||||||
|
sources.append("Built-in defaults")
|
||||||
|
|
||||||
|
return sources
|
||||||
|
|
||||||
|
def _get_relevant_env_vars(self) -> Dict[str, str]:
|
||||||
|
"""Get MARKITECT-related environment variables."""
|
||||||
|
env_vars = {}
|
||||||
|
for key, value in os.environ.items():
|
||||||
|
if key.startswith('MARKITECT_'):
|
||||||
|
env_vars[key] = value
|
||||||
|
return env_vars
|
||||||
|
|
||||||
|
def _get_config_file_locations(self) -> Dict[str, bool]:
|
||||||
|
"""Get status of potential config file locations."""
|
||||||
|
locations = {}
|
||||||
|
|
||||||
|
# Current directory
|
||||||
|
for config_name in self.config_file_names:
|
||||||
|
current_dir_path = Path.cwd() / config_name
|
||||||
|
locations[str(current_dir_path)] = current_dir_path.exists()
|
||||||
|
|
||||||
|
# Home directory
|
||||||
|
home_config = Path.home() / '.markitect' / 'config.yml'
|
||||||
|
locations[str(home_config)] = home_config.exists()
|
||||||
|
|
||||||
|
return locations
|
||||||
|
|
||||||
|
def display_config(self, show_sensitive: bool = False, output_format: str = 'yaml') -> str:
|
||||||
|
"""Display current configuration in specified format."""
|
||||||
|
config = self.get_current_config()
|
||||||
|
|
||||||
|
if not show_sensitive:
|
||||||
|
config = self._mask_sensitive_data(config)
|
||||||
|
|
||||||
|
if output_format == 'json':
|
||||||
|
return json.dumps(config, indent=2, default=str)
|
||||||
|
elif output_format == 'yaml':
|
||||||
|
return yaml.dump(config, default_flow_style=False, sort_keys=True)
|
||||||
|
else:
|
||||||
|
# Simple format
|
||||||
|
return self._format_simple(config)
|
||||||
|
|
||||||
|
def _mask_sensitive_data(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Mask sensitive configuration values."""
|
||||||
|
masked_config = deepcopy(config)
|
||||||
|
|
||||||
|
def mask_recursive(obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
for key, value in obj.items():
|
||||||
|
if any(sensitive in key.lower() for sensitive in self.sensitive_keys):
|
||||||
|
if value and str(value).strip():
|
||||||
|
obj[key] = '***MASKED***'
|
||||||
|
else:
|
||||||
|
mask_recursive(value)
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
for item in obj:
|
||||||
|
mask_recursive(item)
|
||||||
|
|
||||||
|
mask_recursive(masked_config)
|
||||||
|
return masked_config
|
||||||
|
|
||||||
|
def _format_simple(self, config: Dict[str, Any]) -> str:
|
||||||
|
"""Format configuration in simple key=value style."""
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
def format_recursive(obj, prefix=''):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
for key, value in sorted(obj.items()):
|
||||||
|
full_key = f"{prefix}{key}" if prefix else key
|
||||||
|
if isinstance(value, (dict, list)):
|
||||||
|
if key != '_meta': # Skip meta in simple format
|
||||||
|
format_recursive(value, f"{full_key}.")
|
||||||
|
else:
|
||||||
|
lines.append(f"{full_key} = {value}")
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
for i, item in enumerate(obj):
|
||||||
|
format_recursive(item, f"{prefix}{i}.")
|
||||||
|
|
||||||
|
format_recursive(config)
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def set_config_value(self, key: str, value: str, config_file: Optional[str] = None) -> bool:
|
||||||
|
"""Set a configuration value and persist it."""
|
||||||
|
# Determine target config file
|
||||||
|
target_file = self._get_target_config_file(config_file)
|
||||||
|
|
||||||
|
# Load existing config from file
|
||||||
|
existing_config = self._load_config_file(target_file) if target_file.exists() else {}
|
||||||
|
|
||||||
|
# Set the value (support nested keys with dot notation)
|
||||||
|
self._set_nested_value(existing_config, key, value)
|
||||||
|
|
||||||
|
# Validate the new configuration
|
||||||
|
validation_errors = self._validate_config_value(key, value)
|
||||||
|
if validation_errors:
|
||||||
|
raise ValueError(f"Configuration validation failed: {'; '.join(validation_errors)}")
|
||||||
|
|
||||||
|
# Save back to file
|
||||||
|
self._save_config_file(existing_config, target_file)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_target_config_file(self, config_file: Optional[str] = None) -> Path:
|
||||||
|
"""Determine target configuration file."""
|
||||||
|
if config_file:
|
||||||
|
return Path(config_file)
|
||||||
|
|
||||||
|
# Look for existing config file
|
||||||
|
for config_name in self.config_file_names:
|
||||||
|
config_path = Path(config_name)
|
||||||
|
if config_path.exists():
|
||||||
|
return config_path
|
||||||
|
|
||||||
|
# Default to .markitect.yml in current directory
|
||||||
|
return Path('.markitect.yml')
|
||||||
|
|
||||||
|
def _load_config_file(self, config_file: Path) -> Dict[str, Any]:
|
||||||
|
"""Load configuration from file."""
|
||||||
|
try:
|
||||||
|
content = config_file.read_text()
|
||||||
|
|
||||||
|
if config_file.suffix in ['.yml', '.yaml']:
|
||||||
|
return yaml.safe_load(content) or {}
|
||||||
|
elif config_file.suffix == '.json':
|
||||||
|
return json.loads(content)
|
||||||
|
else:
|
||||||
|
# Try YAML first, then JSON
|
||||||
|
try:
|
||||||
|
return yaml.safe_load(content) or {}
|
||||||
|
except yaml.YAMLError:
|
||||||
|
return json.loads(content)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Failed to load config file {config_file}: {e}")
|
||||||
|
|
||||||
|
def _save_config_file(self, config: Dict[str, Any], config_file: Path) -> None:
|
||||||
|
"""Save configuration to file."""
|
||||||
|
try:
|
||||||
|
# Ensure directory exists
|
||||||
|
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if config_file.suffix in ['.yml', '.yaml']:
|
||||||
|
content = yaml.dump(config, default_flow_style=False, sort_keys=True)
|
||||||
|
elif config_file.suffix == '.json':
|
||||||
|
content = json.dumps(config, indent=2, sort_keys=True)
|
||||||
|
else:
|
||||||
|
# Default to YAML
|
||||||
|
content = yaml.dump(config, default_flow_style=False, sort_keys=True)
|
||||||
|
|
||||||
|
config_file.write_text(content)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Failed to save config file {config_file}: {e}")
|
||||||
|
|
||||||
|
def _set_nested_value(self, config: Dict[str, Any], key: str, value: str) -> None:
|
||||||
|
"""Set a nested configuration value using dot notation."""
|
||||||
|
keys = key.split('.')
|
||||||
|
current = config
|
||||||
|
|
||||||
|
# Navigate to the parent of the target key
|
||||||
|
for k in keys[:-1]:
|
||||||
|
if k not in current:
|
||||||
|
current[k] = {}
|
||||||
|
current = current[k]
|
||||||
|
|
||||||
|
# Set the final value
|
||||||
|
final_key = keys[-1]
|
||||||
|
|
||||||
|
# Try to convert value to appropriate type
|
||||||
|
converted_value = self._convert_value(value)
|
||||||
|
current[final_key] = converted_value
|
||||||
|
|
||||||
|
def _convert_value(self, value: str) -> Any:
|
||||||
|
"""Convert string value to appropriate type."""
|
||||||
|
# Handle boolean values
|
||||||
|
if value.lower() in ('true', 'yes', 'on', '1'):
|
||||||
|
return True
|
||||||
|
elif value.lower() in ('false', 'no', 'off', '0'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Try to convert to number
|
||||||
|
try:
|
||||||
|
if '.' in value:
|
||||||
|
return float(value)
|
||||||
|
else:
|
||||||
|
return int(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Return as string
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _validate_config_value(self, key: str, value: str) -> List[str]:
|
||||||
|
"""Validate a configuration value."""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Path validation
|
||||||
|
if any(path_key in key.lower() for path_key in ['dir', 'path', 'file']):
|
||||||
|
if value and not self._is_valid_path(value):
|
||||||
|
errors.append(f"'{value}' is not a valid path format")
|
||||||
|
|
||||||
|
# URL validation
|
||||||
|
if 'url' in key.lower():
|
||||||
|
if value and not self._is_valid_url(value):
|
||||||
|
errors.append(f"'{value}' is not a valid URL format")
|
||||||
|
|
||||||
|
# Required field validation for setting values (not for display)
|
||||||
|
# We only enforce required fields when explicitly setting them
|
||||||
|
if key == 'gitea_url' and not value.strip():
|
||||||
|
errors.append(f"'{key}' is required and cannot be empty")
|
||||||
|
elif key == 'database_path' and not value.strip():
|
||||||
|
errors.append(f"'{key}' is required and cannot be empty")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
def _is_valid_path(self, path_str: str) -> bool:
|
||||||
|
"""Check if a string represents a valid path."""
|
||||||
|
try:
|
||||||
|
Path(path_str)
|
||||||
|
return True
|
||||||
|
except (ValueError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _is_valid_url(self, url_str: str) -> bool:
|
||||||
|
"""Check if a string represents a valid URL."""
|
||||||
|
import re
|
||||||
|
url_pattern = re.compile(
|
||||||
|
r'^https?://' # http:// or https://
|
||||||
|
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' # domain...
|
||||||
|
r'localhost|' # localhost...
|
||||||
|
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
|
||||||
|
r'(?::\d+)?' # optional port
|
||||||
|
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
|
||||||
|
return url_pattern.match(url_str) is not None
|
||||||
|
|
||||||
|
def initialize_project_config(self, project_dir: Optional[Path] = None, interactive: bool = True) -> Dict[str, Any]:
|
||||||
|
"""Initialize configuration for a new project."""
|
||||||
|
target_dir = project_dir or Path.cwd()
|
||||||
|
config_file = target_dir / '.markitect.yml'
|
||||||
|
|
||||||
|
# Default configuration template
|
||||||
|
default_config = {
|
||||||
|
'gitea_url': 'http://localhost:3000',
|
||||||
|
'repo_owner': '',
|
||||||
|
'repo_name': target_dir.name,
|
||||||
|
'workspace_dir': '.markitect_workspace',
|
||||||
|
'cache_dir': '.ast_cache',
|
||||||
|
'tests_dir': 'tests',
|
||||||
|
'test_file_pattern': 'test_issue_{issue_num}_{scenario}.py',
|
||||||
|
'claude_code_command': 'claude'
|
||||||
|
}
|
||||||
|
|
||||||
|
if interactive:
|
||||||
|
default_config = self._interactive_config_setup(default_config)
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
workspace_dir = target_dir / default_config['workspace_dir']
|
||||||
|
cache_dir = target_dir / default_config['cache_dir']
|
||||||
|
tests_dir = target_dir / default_config['tests_dir']
|
||||||
|
|
||||||
|
for directory in [workspace_dir, cache_dir, tests_dir]:
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Save configuration
|
||||||
|
self._save_config_file(default_config, config_file)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'config_file': str(config_file),
|
||||||
|
'config': default_config,
|
||||||
|
'created_directories': [str(workspace_dir), str(cache_dir), str(tests_dir)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _interactive_config_setup(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Interactive configuration setup (would be used with CLI)."""
|
||||||
|
# This method would be called by CLI with click.prompt()
|
||||||
|
# For now, return the default config
|
||||||
|
# The CLI integration will handle the interactive prompts
|
||||||
|
return config
|
||||||
|
|
||||||
|
def list_config_keys(self) -> List[Tuple[str, str, Any]]:
|
||||||
|
"""List all available configuration keys with descriptions."""
|
||||||
|
config_schema = [
|
||||||
|
('gitea_url', 'Gitea server URL', 'http://localhost:3000'),
|
||||||
|
('repo_owner', 'Repository owner/organization', ''),
|
||||||
|
('repo_name', 'Repository name', ''),
|
||||||
|
('api_token', 'API token for authentication', ''),
|
||||||
|
('workspace_dir', 'Workspace directory path', '.markitect_workspace'),
|
||||||
|
('database_path', 'Database file path', '~/.markitect/markitect.db'),
|
||||||
|
('cache_dir', 'AST cache directory', '.ast_cache'),
|
||||||
|
('tests_dir', 'Tests directory', 'tests'),
|
||||||
|
('test_file_pattern', 'Test file naming pattern', 'test_issue_{issue_num}_{scenario}.py'),
|
||||||
|
('claude_code_command', 'Claude Code command', 'claude'),
|
||||||
|
]
|
||||||
|
|
||||||
|
return config_schema
|
||||||
|
|
||||||
|
def get_config_help(self, key: Optional[str] = None) -> str:
|
||||||
|
"""Get help information for configuration."""
|
||||||
|
if key:
|
||||||
|
# Get help for specific key
|
||||||
|
for config_key, description, default in self.list_config_keys():
|
||||||
|
if config_key == key:
|
||||||
|
return f"{config_key}: {description} (default: {default})"
|
||||||
|
return f"Unknown configuration key: {key}"
|
||||||
|
else:
|
||||||
|
# Get general help
|
||||||
|
lines = ["Available configuration keys:"]
|
||||||
|
for config_key, description, default in self.list_config_keys():
|
||||||
|
lines.append(f" {config_key:<20} {description}")
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def validate_configuration(self, config: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
||||||
|
"""Validate current or provided configuration."""
|
||||||
|
if config is None:
|
||||||
|
config = self.get_current_config()
|
||||||
|
|
||||||
|
validation_results = []
|
||||||
|
|
||||||
|
# Check required fields
|
||||||
|
required_fields = ['gitea_url', 'database_path']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in config or not config[field]:
|
||||||
|
validation_results.append({
|
||||||
|
'key': field,
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Required field {field} is missing or empty'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
validation_results.append({
|
||||||
|
'key': field,
|
||||||
|
'status': 'ok',
|
||||||
|
'message': f'{field} is configured'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check paths exist or are creatable
|
||||||
|
path_fields = ['workspace_dir', 'cache_dir', 'tests_dir']
|
||||||
|
for field in path_fields:
|
||||||
|
if field in config:
|
||||||
|
path = Path(config[field])
|
||||||
|
if path.exists():
|
||||||
|
validation_results.append({
|
||||||
|
'key': field,
|
||||||
|
'status': 'ok',
|
||||||
|
'message': f'{field} exists at {path}'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
validation_results.append({
|
||||||
|
'key': field,
|
||||||
|
'status': 'warning',
|
||||||
|
'message': f'{field} created at {path}'
|
||||||
|
})
|
||||||
|
except OSError as e:
|
||||||
|
validation_results.append({
|
||||||
|
'key': field,
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'{field} cannot be created: {e}'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check database path parent directory
|
||||||
|
if 'database_path' in config:
|
||||||
|
db_path = Path(config['database_path'])
|
||||||
|
db_parent = db_path.parent
|
||||||
|
if not db_parent.exists():
|
||||||
|
try:
|
||||||
|
db_parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
validation_results.append({
|
||||||
|
'key': 'database_path',
|
||||||
|
'status': 'warning',
|
||||||
|
'message': f'Database directory created at {db_parent}'
|
||||||
|
})
|
||||||
|
except OSError as e:
|
||||||
|
validation_results.append({
|
||||||
|
'key': 'database_path',
|
||||||
|
'status': 'error',
|
||||||
|
'message': f'Cannot create database directory: {e}'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
validation_results.append({
|
||||||
|
'key': 'database_path',
|
||||||
|
'status': 'ok',
|
||||||
|
'message': f'Database directory exists at {db_parent}'
|
||||||
|
})
|
||||||
|
|
||||||
|
return validation_results
|
||||||
897
tests/test_issue_18_config_management.py
Normal file
897
tests/test_issue_18_config_management.py
Normal file
@@ -0,0 +1,897 @@
|
|||||||
|
"""
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user