"""Tests for transactions API endpoints.""" from datetime import datetime from unittest.mock import patch import pytest from leggen.api.dependencies import get_transaction_repository @pytest.mark.api class TestTransactionsAPI: """Test transaction-related API endpoints.""" def test_get_all_transactions_success( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test successful retrieval of all transactions from database.""" mock_transactions = [ { "transactionId": "bank-txn-001", # NEW: stable bank-provided ID "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", "accountId": "test-account-123", "rawTransaction": {"transactionId": "bank-txn-001", "some": "data"}, }, { "transactionId": "bank-txn-002", # NEW: stable bank-provided ID "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", "accountId": "test-account-123", "rawTransaction": {"transactionId": "bank-txn-002", "other": "data"}, }, ] mock_transaction_repo.get_transactions.return_value = mock_transactions mock_transaction_repo.get_count.return_value = len(mock_transactions) fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get("/api/v1/transactions?summary_only=true") fastapi_app.dependency_overrides.clear() assert response.status_code == 200 data = response.json() assert len(data["data"]) == 2 # Check first transaction summary transaction = data["data"][0] assert transaction["internal_transaction_id"] == "txn-001" assert transaction["amount"] == -10.50 assert transaction["currency"] == "EUR" assert transaction["description"] == "Coffee Shop Payment" assert transaction["status"] == "booked" assert transaction["account_id"] == "test-account-123" def test_get_all_transactions_full_details( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test retrieval of full transaction details from database.""" mock_transactions = [ { "transactionId": "bank-txn-001", # NEW: stable bank-provided ID "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", "accountId": "test-account-123", "rawTransaction": {"transactionId": "bank-txn-001", "some": "raw_data"}, } ] mock_transaction_repo.get_transactions.return_value = mock_transactions mock_transaction_repo.get_count.return_value = len(mock_transactions) fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get("/api/v1/transactions?summary_only=false") fastapi_app.dependency_overrides.clear() assert response.status_code == 200 data = response.json() assert len(data["data"]) == 1 transaction = data["data"][0] assert transaction["transaction_id"] == "bank-txn-001" # NEW: check stable ID assert transaction["internal_transaction_id"] == "txn-001" assert transaction["institution_id"] == "REVOLUT_REVOLT21" assert transaction["iban"] == "LT313250081177977789" assert "raw_transaction" in transaction def test_get_transactions_with_filters( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test getting transactions with various filters.""" mock_transactions = [ { "transactionId": "bank-txn-001", # NEW: stable bank-provided ID "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", "accountId": "test-account-123", "rawTransaction": {"transactionId": "bank-txn-001", "some": "data"}, } ] mock_transaction_repo.get_transactions.return_value = mock_transactions mock_transaction_repo.get_count.return_value = 1 fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get( "/api/v1/transactions?" "account_id=test-account-123&" "date_from=2025-09-01&" "date_to=2025-09-02&" "min_amount=-50.0&" "max_amount=0.0&" "search=Coffee&" "page=2&" "per_page=10" ) fastapi_app.dependency_overrides.clear() assert response.status_code == 200 # Verify the repository was called with correct filters mock_transaction_repo.get_transactions.assert_called_once_with( account_id="test-account-123", limit=10, offset=10, # (page-1) * per_page = (2-1) * 10 = 10 date_from="2025-09-01", date_to="2025-09-02", min_amount=-50.0, max_amount=0.0, search="Coffee", ) def test_get_transactions_empty_result( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test getting transactions when database returns empty result.""" mock_transaction_repo.get_transactions.return_value = [] mock_transaction_repo.get_count.return_value = 0 fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get("/api/v1/transactions") fastapi_app.dependency_overrides.clear() assert response.status_code == 200 data = response.json() assert len(data["data"]) == 0 assert data["total"] == 0 assert data["page"] == 1 assert data["total_pages"] == 0 def test_get_transactions_database_error( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test handling database error when getting transactions.""" mock_transaction_repo.get_transactions.side_effect = Exception( "Database connection failed" ) fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get("/api/v1/transactions") fastapi_app.dependency_overrides.clear() assert response.status_code == 500 assert "Failed to get transactions" in response.json()["detail"] def test_get_transaction_stats_success( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test successful retrieval of transaction statistics from database.""" mock_transactions = [ { "internalTransactionId": "txn-001", "transactionDate": datetime(2025, 9, 1, 9, 30), "transactionValue": -10.50, "transactionStatus": "booked", "accountId": "test-account-123", }, { "internalTransactionId": "txn-002", "transactionDate": datetime(2025, 9, 2, 14, 15), "transactionValue": 100.00, "transactionStatus": "pending", "accountId": "test-account-123", }, { "internalTransactionId": "txn-003", "transactionDate": datetime(2025, 9, 3, 16, 45), "transactionValue": -25.30, "transactionStatus": "booked", "accountId": "other-account-456", }, ] mock_transaction_repo.get_transactions.return_value = mock_transactions fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get("/api/v1/transactions/stats?days=30") fastapi_app.dependency_overrides.clear() assert response.status_code == 200 data = response.json() assert data["period_days"] == 30 assert data["total_transactions"] == 3 assert data["booked_transactions"] == 2 assert data["pending_transactions"] == 1 assert data["total_income"] == 100.00 assert data["total_expenses"] == 35.80 # abs(-10.50) + abs(-25.30) assert data["net_change"] == 64.20 # 100.00 - 35.80 assert data["accounts_included"] == 2 # Two unique account IDs # Average transaction: ((-10.50) + 100.00 + (-25.30)) / 3 = 64.20 / 3 = 21.4 expected_avg = round(64.20 / 3, 2) assert data["average_transaction"] == expected_avg def test_get_transaction_stats_with_account_filter( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test getting transaction stats filtered by account.""" mock_transactions = [ { "internalTransactionId": "txn-001", "transactionDate": datetime(2025, 9, 1, 9, 30), "transactionValue": -10.50, "transactionStatus": "booked", "accountId": "test-account-123", } ] mock_transaction_repo.get_transactions.return_value = mock_transactions fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get( "/api/v1/transactions/stats?account_id=test-account-123" ) fastapi_app.dependency_overrides.clear() assert response.status_code == 200 # Verify the repository was called with account filter mock_transaction_repo.get_transactions.assert_called_once() call_kwargs = mock_transaction_repo.get_transactions.call_args.kwargs assert call_kwargs["account_id"] == "test-account-123" def test_get_transaction_stats_empty_result( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test getting stats when no transactions match criteria.""" mock_transaction_repo.get_transactions.return_value = [] fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get("/api/v1/transactions/stats") fastapi_app.dependency_overrides.clear() assert response.status_code == 200 data = response.json() assert data["total_transactions"] == 0 assert data["total_income"] == 0.0 assert data["total_expenses"] == 0.0 assert data["net_change"] == 0.0 assert data["average_transaction"] == 0 # Division by zero handled assert data["accounts_included"] == 0 def test_get_transaction_stats_database_error( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test handling database error when getting stats.""" mock_transaction_repo.get_transactions.side_effect = Exception( "Database connection failed" ) fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get("/api/v1/transactions/stats") fastapi_app.dependency_overrides.clear() assert response.status_code == 500 assert "Failed to get transaction stats" in response.json()["detail"] def test_get_transaction_stats_custom_period( self, fastapi_app, api_client, mock_config, mock_auth_token, mock_transaction_repo, ): """Test getting transaction stats for custom time period.""" mock_transactions = [ { "internalTransactionId": "txn-001", "transactionDate": datetime(2025, 9, 1, 9, 30), "transactionValue": -10.50, "transactionStatus": "booked", "accountId": "test-account-123", } ] mock_transaction_repo.get_transactions.return_value = mock_transactions fastapi_app.dependency_overrides[get_transaction_repository] = ( lambda: mock_transaction_repo ) with patch("leggen.utils.config.config", mock_config): response = api_client.get("/api/v1/transactions/stats?days=7") fastapi_app.dependency_overrides.clear() assert response.status_code == 200 data = response.json() assert data["period_days"] == 7 # Verify the date range was calculated correctly for 7 days mock_transaction_repo.get_transactions.assert_called_once() call_kwargs = mock_transaction_repo.get_transactions.call_args.kwargs assert "date_from" in call_kwargs assert "date_to" in call_kwargs