mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 20:42:39 +00:00
- 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.
375 lines
14 KiB
Python
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
|