""" Tests for MarkiTect cost tracking CLI commands. This module tests the command-line interface for cost management including: - Cost report generation commands - Cost item management commands - Category management commands - Period cost calculations """ import pytest import tempfile import os import json from datetime import date from decimal import Decimal from click.testing import CliRunner from markitect.finance.cli import cost_commands from markitect.finance.cost_manager import CostItemManager, CostItem from markitect.finance.models import FinanceModels class TestCostCLICommands: """Test suite for cost tracking CLI commands.""" @pytest.fixture def temp_db(self): """Create temporary database for testing.""" fd, path = tempfile.mkstemp(suffix='.db') os.close(fd) yield path os.unlink(path) @pytest.fixture def setup_test_data(self, temp_db): """Setup test database with sample cost data.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() cost_manager = CostItemManager(temp_db) # Get categories infra_cat = cost_manager.get_category_by_name('Infrastructure') software_cat = cost_manager.get_category_by_name('Software') # Create sample cost items cost_items = [ CostItem( category_id=infra_cat['id'], name='Test Server', cost_type='monthly', amount_eur=Decimal('25.00'), starting_from_date=date(2025, 1, 1) ), CostItem( category_id=software_cat['id'], name='Test Software', cost_type='one_time', amount_eur=Decimal('50.00'), starting_from_date=date(2025, 1, 15) ) ] for item in cost_items: cost_manager.create_cost_item(item) return temp_db @pytest.fixture def runner(self): """Create Click test runner.""" return CliRunner() def test_cost_report_generate_summary(self, runner, setup_test_data): """Test cost report generate command with summary format.""" result = runner.invoke(cost_commands, [ 'report', 'generate', '--period', '2025-01', '--format', 'summary', '--database', setup_test_data ]) assert result.exit_code == 0 assert "Cost Summary Report - January 2025" in result.output assert "€75.00" in result.output # 25 + 50 assert "frontmatter" not in result.output.lower() # Should be properly formatted def test_cost_report_generate_detailed(self, runner, setup_test_data): """Test cost report generate command with detailed format.""" result = runner.invoke(cost_commands, [ 'report', 'generate', '--period', '2025-01', '--format', 'detailed', '--database', setup_test_data ]) assert result.exit_code == 0 assert "Detailed Cost Report - January 2025" in result.output assert "Infrastructure" in result.output assert "Software" in result.output assert "Test Server" in result.output assert "Test Software" in result.output def test_cost_report_generate_audit(self, runner, setup_test_data): """Test cost report generate command with audit format.""" result = runner.invoke(cost_commands, [ 'report', 'generate', '--period', '2025-01', '--format', 'audit', '--database', setup_test_data ]) assert result.exit_code == 0 assert "Cost Audit Report - January 2025" in result.output assert "Audit Summary" in result.output assert "Transaction History" in result.output def test_cost_report_generate_with_output_file(self, runner, setup_test_data): """Test saving report to output file.""" with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.md') as f: output_path = f.name try: result = runner.invoke(cost_commands, [ 'report', 'generate', '--period', '2025-01', '--output', output_path, '--database', setup_test_data ]) assert result.exit_code == 0 assert f"Report saved to: {output_path}" in result.output # Verify file was created assert os.path.exists(output_path) with open(output_path, 'r') as f: content = f.read() assert "Cost Summary Report" in content finally: if os.path.exists(output_path): os.unlink(output_path) def test_cost_report_generate_invalid_period(self, runner, setup_test_data): """Test report generation with invalid period format.""" result = runner.invoke(cost_commands, [ 'report', 'generate', '--period', 'invalid-period', '--database', setup_test_data ]) assert result.exit_code == 1 assert "Period must be in YYYY-MM format" in result.output def test_cost_report_generate_default_database(self, runner): """Test report generation with default database path from config.""" result = runner.invoke(cost_commands, [ 'report', 'generate', '--period', '2025-01' ]) # Should succeed with default config and empty database assert result.exit_code == 0 assert "Cost Summary Report - January 2025" in result.output assert "€0.00" in result.output # Empty database shows zero costs def test_cost_report_template_show(self, runner): """Test cost report template show command.""" result = runner.invoke(cost_commands, [ 'report', 'template', '--show' ]) assert result.exit_code == 0 assert "Summary Report Template" in result.output assert "Description" in result.output assert "Frontmatter Fields" in result.output def test_cost_report_template_different_formats(self, runner): """Test template show for different formats.""" formats = ['summary', 'detailed', 'audit'] for format_type in formats: result = runner.invoke(cost_commands, [ 'report', 'template', '--show', '--format', format_type ]) assert result.exit_code == 0 assert f"{format_type.title()} Report Template" in result.output def test_cost_item_add(self, runner, temp_db): """Test adding new cost item via CLI.""" # Initialize database finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'item', 'add', 'Test Item', '--category', 'Infrastructure', '--amount', '15.50', '--type', 'monthly', '--database', temp_db ]) assert result.exit_code == 0 assert "✅ Created cost item 'Test Item'" in result.output # Verify item was created cost_manager = CostItemManager(temp_db) items = cost_manager.list_cost_items() assert len(items) == 1 assert items[0]['name'] == 'Test Item' assert float(items[0]['amount_eur']) == 15.50 def test_cost_item_add_with_description_and_date(self, runner, temp_db): """Test adding cost item with description and start date.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'item', 'add', 'Test Item', '--category', 'Software', '--amount', '99.99', '--type', 'one_time', '--description', 'Test description', '--start-date', '2025-01-15', '--database', temp_db ]) assert result.exit_code == 0 assert "✅ Created cost item 'Test Item'" in result.output def test_cost_item_add_invalid_category(self, runner, temp_db): """Test adding item with non-existent category.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'item', 'add', 'Test Item', '--category', 'NonExistent', '--amount', '10.00', '--type', 'monthly', '--database', temp_db ]) assert result.exit_code == 1 assert "Category 'NonExistent' not found" in result.output assert "Available categories:" in result.output def test_cost_item_list(self, runner, setup_test_data): """Test listing cost items.""" result = runner.invoke(cost_commands, [ 'item', 'list', '--database', setup_test_data ]) assert result.exit_code == 0 assert "Test Server" in result.output assert "Test Software" in result.output assert "€25.00" in result.output assert "€50.00" in result.output def test_cost_item_list_with_filters(self, runner, setup_test_data): """Test listing cost items with filters.""" # Filter by category result = runner.invoke(cost_commands, [ 'item', 'list', '--category', 'Infrastructure', '--database', setup_test_data ]) assert result.exit_code == 0 assert "Test Server" in result.output assert "Test Software" not in result.output # Filter by type result = runner.invoke(cost_commands, [ 'item', 'list', '--type', 'monthly', '--database', setup_test_data ]) assert result.exit_code == 0 assert "Test Server" in result.output assert "Test Software" not in result.output def test_cost_category_list(self, runner, setup_test_data): """Test listing cost categories.""" result = runner.invoke(cost_commands, [ 'category', 'list', '--database', setup_test_data ]) assert result.exit_code == 0 assert "Infrastructure" in result.output assert "Software" in result.output assert "Total: 8 categories" in result.output # Default categories def test_cost_category_add(self, runner, temp_db): """Test adding new cost category.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'category', 'add', 'Custom Category', '--description', 'Custom test category', '--database', temp_db ]) assert result.exit_code == 0 assert "✅ Created category 'Custom Category'" in result.output # Verify category was created cost_manager = CostItemManager(temp_db) categories = cost_manager.list_categories() category_names = [cat['name'] for cat in categories] assert 'Custom Category' in category_names def test_cost_calculate(self, runner, setup_test_data): """Test cost calculation command.""" result = runner.invoke(cost_commands, [ 'calculate', '--period', '2025-01', '--database', setup_test_data ]) assert result.exit_code == 0 assert "Cost Calculation - January 2025" in result.output assert "Monthly Recurring: €25.00" in result.output assert "One-time Expenses: €50.00" in result.output assert "Total Period Cost: €75.00" in result.output assert "Active Cost Items: 2" in result.output def test_cost_calculate_current_month(self, runner, setup_test_data): """Test cost calculation for current month (default).""" result = runner.invoke(cost_commands, [ 'calculate', '--database', setup_test_data ]) assert result.exit_code == 0 assert "Cost Calculation" in result.output # Should default to current month def test_cost_calculate_invalid_period(self, runner, setup_test_data): """Test cost calculation with invalid period.""" result = runner.invoke(cost_commands, [ 'calculate', '--period', 'invalid', '--database', setup_test_data ]) assert result.exit_code == 1 assert "Period must be in YYYY-MM format" in result.output def test_cost_item_add_invalid_date_format(self, runner, temp_db): """Test adding item with invalid date format.""" finance_models = FinanceModels(temp_db) finance_models.initialize_finance_schema() result = runner.invoke(cost_commands, [ 'item', 'add', 'Test Item', '--category', 'Infrastructure', '--amount', '10.00', '--type', 'monthly', '--start-date', 'invalid-date', '--database', temp_db ]) assert result.exit_code == 1 assert "Start date must be in YYYY-MM-DD format" in result.output def test_help_commands(self, runner): """Test help output for cost commands.""" # Test main cost help result = runner.invoke(cost_commands, ['--help']) assert result.exit_code == 0 assert "Cost tracking and financial reporting commands" in result.output # Test report help result = runner.invoke(cost_commands, ['report', '--help']) assert result.exit_code == 0 assert "Generate cost reports" in result.output # Test item help result = runner.invoke(cost_commands, ['item', '--help']) assert result.exit_code == 0 assert "Manage cost items" in result.output # Test category help result = runner.invoke(cost_commands, ['category', '--help']) assert result.exit_code == 0 assert "Manage cost categories" in result.output