""" 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()