Files
leggen/tests/unit/test_api_transactions.py
Elisiário Couto 61fafecb78 feat(frontend): adapt to composite key transaction structure
- Update Transaction interface to include stable transaction_id field
- Modify TransactionsList to use stable transaction_id for React keys
- Update API models to handle new transactionId field from database
- Fix API routes to properly map transaction_id in responses
- Update test mocks to include transactionId field
- Ensure backward compatibility with internal_transaction_id

This adapts the frontend to work with the new composite primary key
(accountId, transactionId) structure that prevents duplicate transactions.
2025-09-10 21:11:26 +01:00

375 lines
14 KiB
Python

"""Tests for transactions API endpoints."""
import pytest
from unittest.mock import patch
from datetime import datetime
@pytest.mark.api
class TestTransactionsAPI:
"""Test transaction-related API endpoints."""
def test_get_all_transactions_success(
self, api_client, mock_config, mock_auth_token
):
"""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"},
},
]
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
return_value=mock_transactions,
),
patch(
"leggend.api.routes.transactions.database_service.get_transaction_count_from_db",
return_value=2,
),
):
response = api_client.get("/api/v1/transactions?summary_only=true")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
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, api_client, mock_config, mock_auth_token
):
"""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"},
}
]
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
return_value=mock_transactions,
),
patch(
"leggend.api.routes.transactions.database_service.get_transaction_count_from_db",
return_value=1,
),
):
response = api_client.get("/api/v1/transactions?summary_only=false")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
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, api_client, mock_config, mock_auth_token
):
"""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"},
}
]
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
return_value=mock_transactions,
) as mock_get_transactions,
patch(
"leggend.api.routes.transactions.database_service.get_transaction_count_from_db",
return_value=1,
),
):
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&"
"limit=10&"
"offset=5"
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
# Verify the database service was called with correct filters
mock_get_transactions.assert_called_once_with(
account_id="test-account-123",
limit=10,
offset=5,
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, api_client, mock_config, mock_auth_token
):
"""Test getting transactions when database returns empty result."""
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
return_value=[],
),
patch(
"leggend.api.routes.transactions.database_service.get_transaction_count_from_db",
return_value=0,
),
):
response = api_client.get("/api/v1/transactions")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert len(data["data"]) == 0
assert "0 transactions" in data["message"]
def test_get_transactions_database_error(
self, api_client, mock_config, mock_auth_token
):
"""Test handling database error when getting transactions."""
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
side_effect=Exception("Database connection failed"),
),
):
response = api_client.get("/api/v1/transactions")
assert response.status_code == 500
assert "Failed to get transactions" in response.json()["detail"]
def test_get_transaction_stats_success(
self, api_client, mock_config, mock_auth_token
):
"""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",
},
]
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
return_value=mock_transactions,
),
):
response = api_client.get("/api/v1/transactions/stats?days=30")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
stats = data["data"]
assert stats["period_days"] == 30
assert stats["total_transactions"] == 3
assert stats["booked_transactions"] == 2
assert stats["pending_transactions"] == 1
assert stats["total_income"] == 100.00
assert stats["total_expenses"] == 35.80 # abs(-10.50) + abs(-25.30)
assert stats["net_change"] == 64.20 # 100.00 - 35.80
assert stats["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 stats["average_transaction"] == expected_avg
def test_get_transaction_stats_with_account_filter(
self, api_client, mock_config, mock_auth_token
):
"""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",
}
]
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
return_value=mock_transactions,
) as mock_get_transactions,
):
response = api_client.get(
"/api/v1/transactions/stats?account_id=test-account-123"
)
assert response.status_code == 200
# Verify the database service was called with account filter
mock_get_transactions.assert_called_once()
call_kwargs = mock_get_transactions.call_args.kwargs
assert call_kwargs["account_id"] == "test-account-123"
def test_get_transaction_stats_empty_result(
self, api_client, mock_config, mock_auth_token
):
"""Test getting stats when no transactions match criteria."""
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
return_value=[],
),
):
response = api_client.get("/api/v1/transactions/stats")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
stats = data["data"]
assert stats["total_transactions"] == 0
assert stats["total_income"] == 0.0
assert stats["total_expenses"] == 0.0
assert stats["net_change"] == 0.0
assert stats["average_transaction"] == 0 # Division by zero handled
assert stats["accounts_included"] == 0
def test_get_transaction_stats_database_error(
self, api_client, mock_config, mock_auth_token
):
"""Test handling database error when getting stats."""
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
side_effect=Exception("Database connection failed"),
),
):
response = api_client.get("/api/v1/transactions/stats")
assert response.status_code == 500
assert "Failed to get transaction stats" in response.json()["detail"]
def test_get_transaction_stats_custom_period(
self, api_client, mock_config, mock_auth_token
):
"""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",
}
]
with (
patch("leggend.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
return_value=mock_transactions,
) as mock_get_transactions,
):
response = api_client.get("/api/v1/transactions/stats?days=7")
assert response.status_code == 200
data = response.json()
assert data["data"]["period_days"] == 7
# Verify the date range was calculated correctly for 7 days
mock_get_transactions.assert_called_once()
call_kwargs = mock_get_transactions.call_args.kwargs
assert "date_from" in call_kwargs
assert "date_to" in call_kwargs