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>
357 lines
12 KiB
Python
357 lines
12 KiB
Python
"""
|
|
Tests for MarkiTect cost report template generator.
|
|
|
|
This module tests the complete cost report generation functionality including:
|
|
- Report generation in different formats (summary, detailed, audit)
|
|
- Markdown output with frontmatter and contentmatter
|
|
- CLI integration and command functionality
|
|
- Template structure validation
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
import os
|
|
import json
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
|
|
from markitect.finance.cost_manager import CostItemManager, CostItem
|
|
from markitect.finance.report_generator import CostReportGenerator, ReportConfig
|
|
from markitect.finance.models import FinanceModels
|
|
|
|
|
|
class TestCostReportGenerator:
|
|
"""Test suite for cost report generation system."""
|
|
|
|
@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='Hosteurope Server',
|
|
description='Monthly server hosting',
|
|
cost_type='monthly',
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
),
|
|
CostItem(
|
|
category_id=software_cat['id'],
|
|
name='Bubble.io Plan',
|
|
description='No-code platform subscription',
|
|
cost_type='monthly',
|
|
amount_eur=Decimal('32.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
),
|
|
CostItem(
|
|
category_id=infra_cat['id'],
|
|
name='SSL Certificate',
|
|
description='Annual SSL certificate',
|
|
cost_type='one_time',
|
|
amount_eur=Decimal('45.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 report_generator(self, setup_test_data):
|
|
"""Create report generator with test data."""
|
|
return CostReportGenerator(setup_test_data)
|
|
|
|
def test_report_config_creation(self):
|
|
"""Test ReportConfig dataclass creation."""
|
|
config = ReportConfig(
|
|
format="summary",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31),
|
|
currency="EUR"
|
|
)
|
|
|
|
assert config.format == "summary"
|
|
assert config.period_start == date(2025, 1, 1)
|
|
assert config.period_end == date(2025, 1, 31)
|
|
assert config.currency == "EUR"
|
|
assert config.include_inactive is False
|
|
assert config.output_path is None
|
|
|
|
def test_generate_summary_report(self, report_generator):
|
|
"""Test generation of summary cost report."""
|
|
config = ReportConfig(
|
|
format="summary",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31)
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
# Check that it's valid markdown with frontmatter
|
|
assert report.startswith("---")
|
|
assert "Cost Summary Report - January 2025" in report
|
|
assert "total_costs: 87.0" in report
|
|
assert "report_type: \"cost_summary\"" in report
|
|
|
|
# Check contentmatter is present
|
|
assert "contentmatter:" in report
|
|
assert "cost_data" in report
|
|
|
|
# Verify total costs are correct (10 + 32 + 45 = 87)
|
|
assert "€87.00" in report
|
|
|
|
def test_generate_detailed_report(self, report_generator):
|
|
"""Test generation of detailed cost report."""
|
|
config = ReportConfig(
|
|
format="detailed",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31)
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
# Check report structure
|
|
assert "Detailed Cost Report - January 2025" in report
|
|
assert "Executive Summary" in report
|
|
assert "report_type: \"cost_detailed\"" in report
|
|
|
|
# Check category sections are present
|
|
assert "Infrastructure" in report
|
|
assert "Software" in report
|
|
|
|
# Check individual items are listed
|
|
assert "Hosteurope Server" in report
|
|
assert "Bubble.io Plan" in report
|
|
assert "SSL Certificate" in report
|
|
|
|
# Check table format
|
|
assert "| Name | Type | Amount | Status | Start Date |" in report
|
|
|
|
def test_generate_audit_report(self, report_generator):
|
|
"""Test generation of audit trail report."""
|
|
config = ReportConfig(
|
|
format="audit",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31)
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
# Check report structure
|
|
assert "Cost Audit Report - January 2025" in report
|
|
assert "Audit Summary" in report
|
|
assert "report_type: \"cost_audit\"" in report
|
|
assert "audit_trail: True" in report
|
|
|
|
# Check audit sections
|
|
assert "Cost Verification" in report
|
|
assert "Active Cost Items" in report
|
|
assert "Transaction History" in report
|
|
assert "Audit Trail" in report
|
|
|
|
# Check contentmatter includes audit data
|
|
assert "audit_data" in report
|
|
|
|
def test_generate_period_report_convenience_method(self, report_generator):
|
|
"""Test convenience method for generating monthly reports."""
|
|
report = report_generator.generate_period_report(2025, 1, "summary")
|
|
|
|
assert "Cost Summary Report - January 2025" in report
|
|
assert "2025-01-01" in report
|
|
assert "2025-01-31" in report
|
|
|
|
def test_invalid_report_format_raises_error(self, report_generator):
|
|
"""Test that invalid report format raises ValueError."""
|
|
config = ReportConfig(
|
|
format="invalid",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31)
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Unknown report format"):
|
|
report_generator.generate_report(config)
|
|
|
|
def test_frontmatter_structure(self, report_generator):
|
|
"""Test frontmatter structure in generated reports."""
|
|
config = ReportConfig(
|
|
format="summary",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31)
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
# Extract frontmatter (between first two ---)
|
|
lines = report.split('\n')
|
|
frontmatter_lines = []
|
|
in_frontmatter = False
|
|
|
|
for line in lines:
|
|
if line.strip() == "---":
|
|
if not in_frontmatter:
|
|
in_frontmatter = True
|
|
continue
|
|
else:
|
|
break
|
|
if in_frontmatter:
|
|
frontmatter_lines.append(line)
|
|
|
|
frontmatter_text = '\n'.join(frontmatter_lines)
|
|
|
|
# Check required frontmatter fields
|
|
assert 'report_type:' in frontmatter_text
|
|
assert 'period_start:' in frontmatter_text
|
|
assert 'period_end:' in frontmatter_text
|
|
assert 'total_costs:' in frontmatter_text
|
|
assert 'currency:' in frontmatter_text
|
|
assert 'generated_at:' in frontmatter_text
|
|
|
|
def test_contentmatter_structure(self, report_generator):
|
|
"""Test contentmatter structure in generated reports."""
|
|
config = ReportConfig(
|
|
format="summary",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31)
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
# Extract contentmatter (JSON in HTML comment)
|
|
assert "<!--" in report
|
|
assert "contentmatter:" in report
|
|
assert "-->" in report
|
|
|
|
# Find and extract JSON
|
|
start = report.find("contentmatter:\n") + len("contentmatter:\n")
|
|
end = report.find("\n-->")
|
|
json_text = report[start:end].strip()
|
|
|
|
# Parse JSON to verify structure
|
|
contentmatter = json.loads(json_text)
|
|
|
|
assert "cost_data" in contentmatter
|
|
assert "total_monthly" in contentmatter["cost_data"]
|
|
assert "total_one_time" in contentmatter["cost_data"]
|
|
assert "categories" in contentmatter["cost_data"]
|
|
assert "active_items" in contentmatter["cost_data"]
|
|
|
|
# Verify totals
|
|
assert contentmatter["cost_data"]["total_monthly"] == 42.0
|
|
assert contentmatter["cost_data"]["total_one_time"] == 45.0
|
|
|
|
def test_save_report_to_file(self, report_generator, temp_db):
|
|
"""Test saving report to file."""
|
|
config = ReportConfig(
|
|
format="summary",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31)
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
# Save to temporary file
|
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.md') as f:
|
|
output_path = f.name
|
|
|
|
try:
|
|
report_generator.save_report(report, output_path)
|
|
|
|
# Verify file was created and contains expected content
|
|
with open(output_path, 'r', encoding='utf-8') as f:
|
|
saved_content = f.read()
|
|
|
|
assert saved_content == report
|
|
assert "Cost Summary Report" in saved_content
|
|
|
|
finally:
|
|
os.unlink(output_path)
|
|
|
|
def test_empty_database_report(self, temp_db):
|
|
"""Test report generation with empty database."""
|
|
# Initialize empty database
|
|
finance_models = FinanceModels(temp_db)
|
|
finance_models.initialize_finance_schema()
|
|
|
|
report_generator = CostReportGenerator(temp_db)
|
|
config = ReportConfig(
|
|
format="summary",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31)
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
# Should still generate valid report with zero costs
|
|
assert "total_costs: 0.0" in report
|
|
assert "€0.00" in report
|
|
|
|
def test_different_currency(self, report_generator):
|
|
"""Test report generation with different currency."""
|
|
config = ReportConfig(
|
|
format="summary",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31),
|
|
currency="USD"
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
assert 'currency: "USD"' in report
|
|
# Note: amounts are still in EUR from database, currency is just metadata
|
|
|
|
def test_report_with_inactive_items(self, setup_test_data):
|
|
"""Test report behavior with inactive cost items."""
|
|
cost_manager = CostItemManager(setup_test_data)
|
|
|
|
# Deactivate one item
|
|
items = cost_manager.list_cost_items()
|
|
if items:
|
|
cost_manager.deactivate_cost_item(items[0]['id'], date(2025, 1, 15))
|
|
|
|
report_generator = CostReportGenerator(setup_test_data)
|
|
config = ReportConfig(
|
|
format="detailed",
|
|
period_start=date(2025, 1, 1),
|
|
period_end=date(2025, 1, 31),
|
|
include_inactive=False
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
# Should still generate valid report, potentially with fewer active items
|
|
assert "Detailed Cost Report" in report
|
|
assert "contentmatter:" in report
|
|
|
|
def test_cross_month_period(self, report_generator):
|
|
"""Test report generation across multiple months."""
|
|
config = ReportConfig(
|
|
format="summary",
|
|
period_start=date(2025, 1, 15),
|
|
period_end=date(2025, 2, 15)
|
|
)
|
|
|
|
report = report_generator.generate_report(config)
|
|
|
|
assert "2025-01-15" in report
|
|
assert "2025-02-15" in report
|
|
# Should include items active during this period |