"""Tests for database service.""" from datetime import datetime from unittest.mock import patch import pytest from leggen.services.database_service import DatabaseService @pytest.fixture def database_service(): """Create a database service instance for testing.""" return DatabaseService() @pytest.fixture def sample_transactions_db_format(): """Sample transactions in database format.""" return [ { "accountId": "test-account-123", "transactionId": "txn-001", "internalTransactionId": "txn-001", "institutionId": "REVOLUT_REVOLT21", "iban": "LT313250081177977789", "transactionDate": datetime(2025, 9, 1, 9, 30), "description": "Coffee Shop Payment", "transactionValue": -10.50, "transactionCurrency": "EUR", "transactionStatus": "booked", "rawTransaction": {"transactionId": "txn-001", "some": "data"}, }, { "accountId": "test-account-123", "transactionId": "txn-002", "internalTransactionId": "txn-002", "institutionId": "REVOLUT_REVOLT21", "iban": "LT313250081177977789", "transactionDate": datetime(2025, 9, 2, 14, 15), "description": "Grocery Store", "transactionValue": -45.30, "transactionCurrency": "EUR", "transactionStatus": "booked", "rawTransaction": {"transactionId": "txn-002", "other": "data"}, }, ] @pytest.fixture def sample_balances_db_format(): """Sample balances in database format.""" return [ { "id": 1, "account_id": "test-account-123", "bank": "REVOLUT_REVOLT21", "status": "active", "iban": "LT313250081177977789", "amount": 1000.00, "currency": "EUR", "type": "interimAvailable", "timestamp": datetime(2025, 9, 1, 10, 0), }, { "id": 2, "account_id": "test-account-123", "bank": "REVOLUT_REVOLT21", "status": "active", "iban": "LT313250081177977789", "amount": 950.00, "currency": "EUR", "type": "expected", "timestamp": datetime(2025, 9, 1, 10, 0), }, ] @pytest.mark.asyncio class TestDatabaseService: """Test database service operations.""" async def test_get_transactions_from_db_success( self, database_service, sample_transactions_db_format ): """Test successful retrieval of transactions from database.""" with patch.object( database_service.transactions, "get_transactions" ) as mock_get_transactions: mock_get_transactions.return_value = sample_transactions_db_format result = await database_service.get_transactions_from_db( account_id="test-account-123", limit=10 ) assert len(result) == 2 assert result[0]["internalTransactionId"] == "txn-001" mock_get_transactions.assert_called_once_with( account_id="test-account-123", limit=10, offset=0, date_from=None, date_to=None, min_amount=None, max_amount=None, search=None, ) async def test_get_transactions_from_db_with_filters( self, database_service, sample_transactions_db_format ): """Test retrieving transactions with filters.""" with patch.object( database_service.transactions, "get_transactions" ) as mock_get_transactions: mock_get_transactions.return_value = sample_transactions_db_format result = await database_service.get_transactions_from_db( account_id="test-account-123", limit=5, offset=10, date_from="2025-09-01", date_to="2025-09-02", min_amount=-50.0, max_amount=0.0, search="Coffee", ) assert len(result) == 2 mock_get_transactions.assert_called_once_with( account_id="test-account-123", limit=5, offset=10, date_from="2025-09-01", date_to="2025-09-02", min_amount=-50.0, max_amount=0.0, search="Coffee", ) async def test_get_transactions_from_db_sqlite_disabled(self, database_service): """Test getting transactions when SQLite is disabled.""" database_service.sqlite_enabled = False result = await database_service.get_transactions_from_db() assert result == [] async def test_get_transactions_from_db_error(self, database_service): """Test handling error when getting transactions.""" with patch.object( database_service.transactions, "get_transactions" ) as mock_get_transactions: mock_get_transactions.side_effect = Exception("Database error") result = await database_service.get_transactions_from_db() assert result == [] async def test_get_transaction_count_from_db_success(self, database_service): """Test successful retrieval of transaction count.""" with patch.object(database_service.transactions, "get_count") as mock_get_count: mock_get_count.return_value = 42 result = await database_service.get_transaction_count_from_db( account_id="test-account-123" ) assert result == 42 mock_get_count.assert_called_once_with( account_id="test-account-123", date_from=None, date_to=None, min_amount=None, max_amount=None, search=None, ) async def test_get_transaction_count_from_db_with_filters(self, database_service): """Test getting transaction count with filters.""" with patch.object(database_service.transactions, "get_count") as mock_get_count: mock_get_count.return_value = 15 result = await database_service.get_transaction_count_from_db( account_id="test-account-123", date_from="2025-09-01", min_amount=-100.0, search="Coffee", ) assert result == 15 mock_get_count.assert_called_once_with( account_id="test-account-123", date_from="2025-09-01", date_to=None, min_amount=-100.0, max_amount=None, search="Coffee", ) async def test_get_transaction_count_from_db_sqlite_disabled( self, database_service ): """Test getting count when SQLite is disabled.""" database_service.sqlite_enabled = False result = await database_service.get_transaction_count_from_db() assert result == 0 async def test_get_transaction_count_from_db_error(self, database_service): """Test handling error when getting count.""" with patch.object(database_service.transactions, "get_count") as mock_get_count: mock_get_count.side_effect = Exception("Database error") result = await database_service.get_transaction_count_from_db() assert result == 0 async def test_get_balances_from_db_success( self, database_service, sample_balances_db_format ): """Test successful retrieval of balances from database.""" with patch.object( database_service.balances, "get_balances" ) as mock_get_balances: mock_get_balances.return_value = sample_balances_db_format result = await database_service.get_balances_from_db( account_id="test-account-123" ) assert len(result) == 2 assert result[0]["account_id"] == "test-account-123" assert result[0]["amount"] == 1000.00 mock_get_balances.assert_called_once_with(account_id="test-account-123") async def test_get_balances_from_db_sqlite_disabled(self, database_service): """Test getting balances when SQLite is disabled.""" database_service.sqlite_enabled = False result = await database_service.get_balances_from_db() assert result == [] async def test_get_balances_from_db_error(self, database_service): """Test handling error when getting balances.""" with patch.object( database_service.balances, "get_balances" ) as mock_get_balances: mock_get_balances.side_effect = Exception("Database error") result = await database_service.get_balances_from_db() assert result == [] async def test_get_account_summary_from_db_success(self, database_service): """Test successful retrieval of account summary.""" mock_summary = { "accountId": "test-account-123", "institutionId": "REVOLUT_REVOLT21", "iban": "LT313250081177977789", } with patch.object( database_service.transactions, "get_account_summary" ) as mock_get_summary: mock_get_summary.return_value = mock_summary result = await database_service.get_account_summary_from_db( "test-account-123" ) assert result == mock_summary mock_get_summary.assert_called_once_with("test-account-123") async def test_get_account_summary_from_db_sqlite_disabled(self, database_service): """Test getting summary when SQLite is disabled.""" database_service.sqlite_enabled = False result = await database_service.get_account_summary_from_db("test-account-123") assert result is None async def test_get_account_summary_from_db_error(self, database_service): """Test handling error when getting summary.""" with patch.object( database_service.transactions, "get_account_summary" ) as mock_get_summary: mock_get_summary.side_effect = Exception("Database error") result = await database_service.get_account_summary_from_db( "test-account-123" ) assert result is None async def test_persist_balance_sqlite_success(self, database_service): """Test successful balance persistence.""" balance_data = { "institution_id": "REVOLUT_REVOLT21", "iban": "LT313250081177977789", "balances": [ { "balanceAmount": {"amount": "1000.00", "currency": "EUR"}, "balanceType": "interimAvailable", } ], } with ( patch.object(database_service.balances, "persist") as mock_persist, patch.object( database_service.balance_transformer, "transform_to_database_format" ) as mock_transform, ): mock_transform.return_value = [ ( "test-account-123", "REVOLUT_REVOLT21", "active", "LT313250081177977789", 1000.0, "EUR", "interimAvailable", "2025-09-01T10:00:00", ) ] await database_service.persist_balance("test-account-123", balance_data) # Verify transformation and persistence were called mock_transform.assert_called_once_with("test-account-123", balance_data) mock_persist.assert_called_once() async def test_persist_balance_sqlite_error(self, database_service): """Test handling error during balance persistence.""" balance_data = {"balances": []} with ( patch.object(database_service.balances, "persist") as mock_persist, patch.object( database_service.balance_transformer, "transform_to_database_format" ) as mock_transform, ): mock_persist.side_effect = Exception("Database error") mock_transform.return_value = [] with pytest.raises(Exception, match="Database error"): await database_service.persist_balance("test-account-123", balance_data) async def test_persist_transactions_sqlite_success( self, database_service, sample_transactions_db_format ): """Test successful transaction persistence.""" with patch.object(database_service.transactions, "persist") as mock_persist: mock_persist.return_value = sample_transactions_db_format result = await database_service.persist_transactions( "test-account-123", sample_transactions_db_format ) # Should return the new transactions assert len(result) == 2 mock_persist.assert_called_once_with( "test-account-123", sample_transactions_db_format ) async def test_persist_transactions_sqlite_duplicate_detection( self, database_service, sample_transactions_db_format ): """Test that existing transactions are not returned as new.""" with patch.object(database_service.transactions, "persist") as mock_persist: # Return empty list indicating all were duplicates mock_persist.return_value = [] result = await database_service.persist_transactions( "test-account-123", sample_transactions_db_format ) # Should return empty list since all transactions already exist assert len(result) == 0 mock_persist.assert_called_once() async def test_persist_transactions_sqlite_error(self, database_service): """Test handling error during transaction persistence.""" with patch.object(database_service.transactions, "persist") as mock_persist: mock_persist.side_effect = Exception("Database error") with pytest.raises(Exception, match="Database error"): await database_service.persist_transactions("test-account-123", []) async def test_process_transactions_booked_and_pending(self, database_service): """Test processing transactions with both booked and pending.""" account_info = { "institution_id": "REVOLUT_REVOLT21", "iban": "LT313250081177977789", } transaction_data = { "transactions": { "booked": [ { "internalTransactionId": "txn-001", "transactionId": "txn-001", "bookingDate": "2025-09-01", "transactionAmount": {"amount": "-10.50", "currency": "EUR"}, "remittanceInformationUnstructured": "Coffee Shop", } ], "pending": [ { "internalTransactionId": "txn-002", "transactionId": "txn-002", "bookingDate": "2025-09-02", "transactionAmount": {"amount": "-25.00", "currency": "EUR"}, "remittanceInformationUnstructured": "Gas Station", } ], } } result = database_service.process_transactions( "test-account-123", account_info, transaction_data ) assert len(result) == 2 # Check booked transaction booked_txn = next(t for t in result if t["transactionStatus"] == "booked") assert booked_txn["transactionId"] == "txn-001" assert booked_txn["internalTransactionId"] == "txn-001" assert booked_txn["transactionValue"] == -10.50 assert booked_txn["description"] == "Coffee Shop" # Check pending transaction pending_txn = next(t for t in result if t["transactionStatus"] == "pending") assert pending_txn["transactionId"] == "txn-002" assert pending_txn["internalTransactionId"] == "txn-002" assert pending_txn["transactionValue"] == -25.00 assert pending_txn["description"] == "Gas Station" async def test_process_transactions_missing_date_error(self, database_service): """Test processing transaction with missing date raises error.""" account_info = {"institution_id": "TEST_BANK"} transaction_data = { "transactions": { "booked": [ { "internalTransactionId": "txn-001", # Missing both bookingDate and valueDate "transactionAmount": {"amount": "-10.50", "currency": "EUR"}, } ], "pending": [], } } with pytest.raises(ValueError, match="No valid date found in transaction"): database_service.process_transactions( "test-account-123", account_info, transaction_data ) async def test_process_transactions_remittance_array(self, database_service): """Test processing transaction with remittance array.""" account_info = {"institution_id": "TEST_BANK"} transaction_data = { "transactions": { "booked": [ { "internalTransactionId": "txn-001", "transactionId": "txn-001", "bookingDate": "2025-09-01", "transactionAmount": {"amount": "-10.50", "currency": "EUR"}, "remittanceInformationUnstructuredArray": ["Line 1", "Line 2"], } ], "pending": [], } } result = database_service.process_transactions( "test-account-123", account_info, transaction_data ) assert len(result) == 1 assert result[0]["description"] == "Line 1,Line 2"