Some checks failed
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
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Comprehensive cost tracking system implementation including: - Cost report generator with multiple formats (summary, detailed, audit) - Full CLI integration with cost management commands - Claude session cost tracking and estimation - Professional markdown reports with frontmatter/contentmatter - Automatic cost note generation for issue implementations - Complete test coverage (33 test cases) - Database integration with finance schema initialization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
393 lines
14 KiB
Python
393 lines
14 KiB
Python
"""
|
|
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 |