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>
430 lines
15 KiB
Python
430 lines
15 KiB
Python
"""
|
|
Tests for MarkiTect finance models and database schema.
|
|
|
|
This module tests the complete finance schema including:
|
|
- Database table creation and relationships
|
|
- Data integrity constraints
|
|
- Index performance
|
|
- Schema validation
|
|
- Migration functionality
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
import os
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
|
|
from markitect.finance.models import FinanceModels
|
|
|
|
|
|
class TestFinanceModels:
|
|
"""Test suite for finance database models."""
|
|
|
|
@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 finance_models(self, temp_db):
|
|
"""Create FinanceModels instance with temporary database."""
|
|
return FinanceModels(temp_db)
|
|
|
|
def test_initialize_finance_schema(self, finance_models):
|
|
"""Test complete finance schema initialization."""
|
|
# Initialize schema
|
|
finance_models.initialize_finance_schema()
|
|
|
|
# Validate schema was created
|
|
assert finance_models.validate_schema()
|
|
|
|
# Check all required tables exist
|
|
schema_info = finance_models.get_schema_info()
|
|
expected_tables = [
|
|
'cost_categories',
|
|
'cost_items',
|
|
'cost_periods',
|
|
'cost_transactions',
|
|
'issue_cost_allocations',
|
|
'issue_activity_log'
|
|
]
|
|
|
|
for table in expected_tables:
|
|
assert table in schema_info['tables']
|
|
assert len(schema_info['tables'][table]['columns']) > 0
|
|
|
|
def test_cost_categories_table(self, finance_models):
|
|
"""Test cost categories table structure and data."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Test default categories were inserted
|
|
cursor.execute('SELECT COUNT(*) FROM cost_categories')
|
|
count = cursor.fetchone()[0]
|
|
assert count >= 8 # At least 8 default categories
|
|
|
|
# Test unique constraint
|
|
with pytest.raises(Exception): # Should violate unique constraint
|
|
cursor.execute('''
|
|
INSERT INTO cost_categories (name, description)
|
|
VALUES ('Infrastructure', 'Duplicate category')
|
|
''')
|
|
|
|
conn.close()
|
|
|
|
def test_cost_items_table(self, finance_models):
|
|
"""Test cost items table constraints and relationships."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Insert test category
|
|
cursor.execute('''
|
|
INSERT INTO cost_categories (name, description)
|
|
VALUES ('Test Category', 'For testing')
|
|
''')
|
|
category_id = cursor.lastrowid
|
|
|
|
# Test valid cost item insertion
|
|
cursor.execute('''
|
|
INSERT INTO cost_items
|
|
(category_id, name, cost_type, amount_eur, starting_from_date)
|
|
VALUES (?, 'Test Server', 'monthly', 10.50, '2025-01-01')
|
|
''', (category_id,))
|
|
|
|
# Test cost_type constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO cost_items
|
|
(category_id, name, cost_type, amount_eur, starting_from_date)
|
|
VALUES (?, 'Invalid Type', 'invalid', 10.00, '2025-01-01')
|
|
''', (category_id,))
|
|
|
|
# Test negative amount constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO cost_items
|
|
(category_id, name, cost_type, amount_eur, starting_from_date)
|
|
VALUES (?, 'Negative Cost', 'monthly', -10.00, '2025-01-01')
|
|
''', (category_id,))
|
|
|
|
# Test date range constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO cost_items
|
|
(category_id, name, cost_type, amount_eur, starting_from_date, ending_date)
|
|
VALUES (?, 'Invalid Dates', 'monthly', 10.00, '2025-01-01', '2024-12-31')
|
|
''', (category_id,))
|
|
|
|
conn.close()
|
|
|
|
def test_cost_periods_table(self, finance_models):
|
|
"""Test cost periods table constraints."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Test valid period insertion
|
|
cursor.execute('''
|
|
INSERT INTO cost_periods (period_start, period_end)
|
|
VALUES ('2025-01-01', '2025-01-31')
|
|
''')
|
|
|
|
# Test period date constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO cost_periods (period_start, period_end)
|
|
VALUES ('2025-01-31', '2025-01-01')
|
|
''')
|
|
|
|
# Test status constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO cost_periods (period_start, period_end, status)
|
|
VALUES ('2025-02-01', '2025-02-28', 'invalid_status')
|
|
''')
|
|
|
|
# Test unique period constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO cost_periods (period_start, period_end)
|
|
VALUES ('2025-01-01', '2025-01-31')
|
|
''')
|
|
|
|
conn.close()
|
|
|
|
def test_cost_transactions_table(self, finance_models):
|
|
"""Test cost transactions table and audit trail."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Create test data
|
|
cursor.execute('''
|
|
INSERT INTO cost_categories (name) VALUES ('Test Category')
|
|
''')
|
|
category_id = cursor.lastrowid
|
|
|
|
cursor.execute('''
|
|
INSERT INTO cost_items
|
|
(category_id, name, cost_type, amount_eur, starting_from_date)
|
|
VALUES (?, 'Test Item', 'monthly', 10.00, '2025-01-01')
|
|
''', (category_id,))
|
|
cost_item_id = cursor.lastrowid
|
|
|
|
cursor.execute('''
|
|
INSERT INTO cost_periods (period_start, period_end)
|
|
VALUES ('2025-01-01', '2025-01-31')
|
|
''')
|
|
period_id = cursor.lastrowid
|
|
|
|
# Test valid transaction
|
|
cursor.execute('''
|
|
INSERT INTO cost_transactions
|
|
(period_id, cost_item_id, transaction_type, amount_eur, transaction_date)
|
|
VALUES (?, ?, 'cost_incurred', 10.00, '2025-01-15')
|
|
''', (period_id, cost_item_id))
|
|
|
|
# Test transaction type constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO cost_transactions
|
|
(period_id, cost_item_id, transaction_type, amount_eur, transaction_date)
|
|
VALUES (?, ?, 'invalid_type', 10.00, '2025-01-15')
|
|
''', (period_id, cost_item_id))
|
|
|
|
conn.close()
|
|
|
|
def test_issue_cost_allocations_table(self, finance_models):
|
|
"""Test issue cost allocations table."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Create test period
|
|
cursor.execute('''
|
|
INSERT INTO cost_periods (period_start, period_end)
|
|
VALUES ('2025-01-01', '2025-01-31')
|
|
''')
|
|
period_id = cursor.lastrowid
|
|
|
|
# Test valid allocation
|
|
cursor.execute('''
|
|
INSERT INTO issue_cost_allocations
|
|
(issue_id, period_id, allocated_amount, allocation_date)
|
|
VALUES (123, ?, 5.50, '2025-01-31')
|
|
''', (period_id,))
|
|
|
|
# Test positive amount constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO issue_cost_allocations
|
|
(issue_id, period_id, allocated_amount, allocation_date)
|
|
VALUES (124, ?, -1.00, '2025-01-31')
|
|
''', (period_id,))
|
|
|
|
# Test unique issue-period constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO issue_cost_allocations
|
|
(issue_id, period_id, allocated_amount, allocation_date)
|
|
VALUES (123, ?, 3.00, '2025-01-31')
|
|
''', (period_id,))
|
|
|
|
conn.close()
|
|
|
|
def test_issue_activity_log_table(self, finance_models):
|
|
"""Test issue activity log table."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Test valid activity log entry
|
|
cursor.execute('''
|
|
INSERT INTO issue_activity_log
|
|
(issue_id, activity_type, activity_date)
|
|
VALUES (123, 'created', '2025-01-15')
|
|
''')
|
|
|
|
# Test activity type constraint
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO issue_activity_log
|
|
(issue_id, activity_type, activity_date)
|
|
VALUES (124, 'invalid_activity', '2025-01-15')
|
|
''')
|
|
|
|
conn.close()
|
|
|
|
def test_foreign_key_constraints(self, finance_models):
|
|
"""Test foreign key relationships are enforced."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Test cost_items references cost_categories
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO cost_items
|
|
(category_id, name, cost_type, amount_eur, starting_from_date)
|
|
VALUES (999, 'Invalid Category', 'monthly', 10.00, '2025-01-01')
|
|
''')
|
|
|
|
# Test cost_transactions references cost_periods
|
|
with pytest.raises(Exception):
|
|
cursor.execute('''
|
|
INSERT INTO cost_transactions
|
|
(period_id, transaction_type, amount_eur, transaction_date)
|
|
VALUES (999, 'cost_incurred', 10.00, '2025-01-15')
|
|
''')
|
|
|
|
conn.close()
|
|
|
|
def test_indexes_created(self, finance_models):
|
|
"""Test that performance indexes are created."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
schema_info = finance_models.get_schema_info()
|
|
index_names = [idx['name'] for idx in schema_info['indexes']]
|
|
|
|
# Check critical indexes exist
|
|
expected_indexes = [
|
|
'idx_cost_items_active',
|
|
'idx_cost_items_type',
|
|
'idx_cost_periods_status',
|
|
'idx_cost_transactions_period',
|
|
'idx_issue_allocations_issue'
|
|
]
|
|
|
|
for index in expected_indexes:
|
|
assert index in index_names
|
|
|
|
def test_schema_validation(self, finance_models):
|
|
"""Test schema validation functionality."""
|
|
# Before initialization
|
|
assert not finance_models.validate_schema()
|
|
|
|
# After initialization
|
|
finance_models.initialize_finance_schema()
|
|
assert finance_models.validate_schema()
|
|
|
|
def test_drop_finance_schema(self, finance_models):
|
|
"""Test schema cleanup functionality."""
|
|
# Initialize schema
|
|
finance_models.initialize_finance_schema()
|
|
assert finance_models.validate_schema()
|
|
|
|
# Drop schema
|
|
finance_models.drop_finance_schema()
|
|
assert not finance_models.validate_schema()
|
|
|
|
def test_database_integration(self, temp_db):
|
|
"""Test integration with existing DatabaseManager."""
|
|
from markitect.database import DatabaseManager
|
|
|
|
# Initialize standard database
|
|
db_manager = DatabaseManager(temp_db)
|
|
db_manager.initialize_database()
|
|
|
|
# Verify finance tables were also created
|
|
finance_models = FinanceModels(temp_db)
|
|
assert finance_models.validate_schema()
|
|
|
|
# Verify existing tables still exist
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
SELECT name FROM sqlite_master
|
|
WHERE type='table' AND name IN ('markdown_files', 'schemas')
|
|
''')
|
|
existing_tables = [row[0] for row in cursor.fetchall()]
|
|
|
|
assert 'markdown_files' in existing_tables
|
|
assert 'schemas' in existing_tables
|
|
|
|
conn.close()
|
|
|
|
def test_decimal_precision(self, finance_models):
|
|
"""Test decimal precision for financial calculations."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Insert test category
|
|
cursor.execute('''
|
|
INSERT INTO cost_categories (name) VALUES ('Test Category')
|
|
''')
|
|
category_id = cursor.lastrowid
|
|
|
|
# Test precise decimal amounts
|
|
test_amounts = [10.50, 99.99, 0.01, 1234.56]
|
|
|
|
for amount in test_amounts:
|
|
cursor.execute('''
|
|
INSERT INTO cost_items
|
|
(category_id, name, cost_type, amount_eur, starting_from_date)
|
|
VALUES (?, ?, 'monthly', ?, '2025-01-01')
|
|
''', (category_id, f'Test Item {amount}', amount))
|
|
|
|
# Verify precision is maintained
|
|
cursor.execute('SELECT amount_eur FROM cost_items ORDER BY id')
|
|
stored_amounts = [float(row[0]) for row in cursor.fetchall()]
|
|
|
|
assert stored_amounts == test_amounts
|
|
|
|
conn.close()
|
|
|
|
def test_example_cost_data(self, finance_models):
|
|
"""Test insertion of example cost data from issue description."""
|
|
finance_models.initialize_finance_schema()
|
|
|
|
conn = finance_models.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
# Get category IDs
|
|
cursor.execute('SELECT id, name FROM cost_categories')
|
|
categories = {name: id for id, name in cursor.fetchall()}
|
|
|
|
# Insert example costs from issue #88
|
|
example_costs = [
|
|
('Infrastructure', 'Hosteurope Server', 'Monthly server hosting', 10.00),
|
|
('Software', 'Bubble.io Plan', 'No-code platform subscription', 32.00),
|
|
('Domain & DNS', 'Coulomb.social Domain', 'Domain registration', 5.00),
|
|
('Development Tools', 'Claude Code Plan', 'AI coding assistant', 20.00),
|
|
('AI & ML Services', 'Gemini Plan', 'LLM API for specifications', 20.00)
|
|
]
|
|
|
|
for category_name, name, description, amount in example_costs:
|
|
category_id = categories.get(category_name)
|
|
assert category_id is not None
|
|
|
|
cursor.execute('''
|
|
INSERT INTO cost_items
|
|
(category_id, name, description, cost_type, amount_eur, starting_from_date)
|
|
VALUES (?, ?, ?, 'monthly', ?, '2025-01-01')
|
|
''', (category_id, name, description, amount))
|
|
|
|
# Verify total monthly costs
|
|
cursor.execute('''
|
|
SELECT SUM(amount_eur) FROM cost_items
|
|
WHERE cost_type = 'monthly' AND is_active = TRUE
|
|
''')
|
|
total_monthly = float(cursor.fetchone()[0])
|
|
|
|
assert total_monthly == 87.00 # €87/month as described in issue
|
|
|
|
conn.close() |