refactor: Unify leggen and leggend packages into single leggen package

- Merge leggend API components into leggen (api/, services/, background/)
- Replace leggend command with 'leggen server' subcommand
- Consolidate configuration systems into leggen.utils.config
- Update environment variables: LEGGEND_API_URL -> LEGGEN_API_URL
- Rename LeggendAPIClient -> LeggenAPIClient
- Update all documentation, Docker configs, and compose files
- Fix all import statements and test references
- Remove duplicate utility files and clean up package structure

All tests passing (101/101), linting clean, server functionality preserved.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Elisiário Couto
2025-09-14 18:02:55 +01:00
committed by Elisiário Couto
parent 0e645d9bae
commit 318ca517f7
50 changed files with 494 additions and 463 deletions

View File

@@ -5,8 +5,8 @@ from datetime import datetime, timedelta
from unittest.mock import Mock, AsyncMock, patch
from fastapi.testclient import TestClient
from leggend.main import create_app
from leggend.services.database_service import DatabaseService
from leggen.commands.server import create_app
from leggen.services.database_service import DatabaseService
class TestAnalyticsFix:
@@ -27,78 +27,112 @@ class TestAnalyticsFix:
# Mock data for 600 transactions (simulating the issue)
mock_transactions = []
for i in range(600):
mock_transactions.append({
"transactionId": f"txn-{i}",
"transactionDate": (datetime.now() - timedelta(days=i % 365)).isoformat(),
"description": f"Transaction {i}",
"transactionValue": 10.0 if i % 2 == 0 else -5.0,
"transactionCurrency": "EUR",
"transactionStatus": "booked",
"accountId": f"account-{i % 3}",
})
mock_transactions.append(
{
"transactionId": f"txn-{i}",
"transactionDate": (
datetime.now() - timedelta(days=i % 365)
).isoformat(),
"description": f"Transaction {i}",
"transactionValue": 10.0 if i % 2 == 0 else -5.0,
"transactionCurrency": "EUR",
"transactionStatus": "booked",
"accountId": f"account-{i % 3}",
}
)
mock_database_service.get_transactions_from_db = AsyncMock(return_value=mock_transactions)
mock_database_service.get_transactions_from_db = AsyncMock(
return_value=mock_transactions
)
# Test that the endpoint calls get_transactions_from_db with limit=None
with patch('leggend.api.routes.transactions.database_service', mock_database_service):
with patch(
"leggen.api.routes.transactions.database_service", mock_database_service
):
app = create_app()
client = TestClient(app)
response = client.get("/api/v1/transactions/stats?days=365")
assert response.status_code == 200
data = response.json()
# Verify that limit=None was passed to get all transactions
mock_database_service.get_transactions_from_db.assert_called_once()
call_args = mock_database_service.get_transactions_from_db.call_args
assert call_args.kwargs.get("limit") is None, "Stats endpoint should pass limit=None to get all transactions"
assert call_args.kwargs.get("limit") is None, (
"Stats endpoint should pass limit=None to get all transactions"
)
# Verify that the response contains stats for all 600 transactions
assert data["success"] is True
stats = data["data"]
assert stats["total_transactions"] == 600, "Should process all 600 transactions, not just 100"
assert stats["total_transactions"] == 600, (
"Should process all 600 transactions, not just 100"
)
# Verify calculations are correct for all transactions
expected_income = sum(txn["transactionValue"] for txn in mock_transactions if txn["transactionValue"] > 0)
expected_expenses = sum(abs(txn["transactionValue"]) for txn in mock_transactions if txn["transactionValue"] < 0)
expected_income = sum(
txn["transactionValue"]
for txn in mock_transactions
if txn["transactionValue"] > 0
)
expected_expenses = sum(
abs(txn["transactionValue"])
for txn in mock_transactions
if txn["transactionValue"] < 0
)
assert stats["total_income"] == expected_income
assert stats["total_expenses"] == expected_expenses
@pytest.mark.asyncio
async def test_analytics_endpoint_returns_all_transactions(self, mock_database_service):
@pytest.mark.asyncio
async def test_analytics_endpoint_returns_all_transactions(
self, mock_database_service
):
"""Test that the new analytics endpoint returns all transactions without pagination"""
# Mock data for 600 transactions
mock_transactions = []
for i in range(600):
mock_transactions.append({
"transactionId": f"txn-{i}",
"transactionDate": (datetime.now() - timedelta(days=i % 365)).isoformat(),
"description": f"Transaction {i}",
"transactionValue": 10.0 if i % 2 == 0 else -5.0,
"transactionCurrency": "EUR",
"transactionStatus": "booked",
"accountId": f"account-{i % 3}",
})
mock_transactions.append(
{
"transactionId": f"txn-{i}",
"transactionDate": (
datetime.now() - timedelta(days=i % 365)
).isoformat(),
"description": f"Transaction {i}",
"transactionValue": 10.0 if i % 2 == 0 else -5.0,
"transactionCurrency": "EUR",
"transactionStatus": "booked",
"accountId": f"account-{i % 3}",
}
)
mock_database_service.get_transactions_from_db = AsyncMock(return_value=mock_transactions)
mock_database_service.get_transactions_from_db = AsyncMock(
return_value=mock_transactions
)
with patch('leggend.api.routes.transactions.database_service', mock_database_service):
with patch(
"leggen.api.routes.transactions.database_service", mock_database_service
):
app = create_app()
client = TestClient(app)
response = client.get("/api/v1/transactions/analytics?days=365")
assert response.status_code == 200
data = response.json()
# Verify that limit=None was passed to get all transactions
mock_database_service.get_transactions_from_db.assert_called_once()
call_args = mock_database_service.get_transactions_from_db.call_args
assert call_args.kwargs.get("limit") is None, "Analytics endpoint should pass limit=None"
assert call_args.kwargs.get("limit") is None, (
"Analytics endpoint should pass limit=None"
)
# Verify that all 600 transactions are returned
assert data["success"] is True
transactions_data = data["data"]
assert len(transactions_data) == 600, "Analytics endpoint should return all 600 transactions"
assert len(transactions_data) == 600, (
"Analytics endpoint should return all 600 transactions"
)

View File

@@ -43,13 +43,13 @@ class TestAccountsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.accounts.database_service.get_accounts_from_db",
"leggen.api.routes.accounts.database_service.get_accounts_from_db",
return_value=mock_accounts,
),
patch(
"leggend.api.routes.accounts.database_service.get_balances_from_db",
"leggen.api.routes.accounts.database_service.get_balances_from_db",
return_value=mock_balances,
),
):
@@ -98,13 +98,13 @@ class TestAccountsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.accounts.database_service.get_account_details_from_db",
"leggen.api.routes.accounts.database_service.get_account_details_from_db",
return_value=mock_account,
),
patch(
"leggend.api.routes.accounts.database_service.get_balances_from_db",
"leggen.api.routes.accounts.database_service.get_balances_from_db",
return_value=mock_balances,
),
):
@@ -148,9 +148,9 @@ class TestAccountsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.accounts.database_service.get_balances_from_db",
"leggen.api.routes.accounts.database_service.get_balances_from_db",
return_value=mock_balances,
),
):
@@ -191,13 +191,13 @@ class TestAccountsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.accounts.database_service.get_transactions_from_db",
"leggen.api.routes.accounts.database_service.get_transactions_from_db",
return_value=mock_transactions,
),
patch(
"leggend.api.routes.accounts.database_service.get_transaction_count_from_db",
"leggen.api.routes.accounts.database_service.get_transaction_count_from_db",
return_value=1,
),
):
@@ -243,13 +243,13 @@ class TestAccountsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.accounts.database_service.get_transactions_from_db",
"leggen.api.routes.accounts.database_service.get_transactions_from_db",
return_value=mock_transactions,
),
patch(
"leggend.api.routes.accounts.database_service.get_transaction_count_from_db",
"leggen.api.routes.accounts.database_service.get_transaction_count_from_db",
return_value=1,
),
):
@@ -273,9 +273,9 @@ class TestAccountsAPI:
):
"""Test handling of non-existent account."""
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.accounts.database_service.get_account_details_from_db",
"leggen.api.routes.accounts.database_service.get_account_details_from_db",
return_value=None,
),
):

View File

@@ -27,7 +27,7 @@ class TestBanksAPI:
return_value=httpx.Response(200, json=sample_bank_data)
)
with patch("leggend.config.config", mock_config):
with patch("leggen.utils.config.config", mock_config):
response = api_client.get("/api/v1/banks/institutions?country=PT")
assert response.status_code == 200
@@ -52,7 +52,7 @@ class TestBanksAPI:
return_value=httpx.Response(200, json=[])
)
with patch("leggend.config.config", mock_config):
with patch("leggen.utils.config.config", mock_config):
response = api_client.get("/api/v1/banks/institutions?country=XX")
# Should still work but return empty or filtered results
@@ -86,7 +86,7 @@ class TestBanksAPI:
"redirect_url": "http://localhost:8000/",
}
with patch("leggend.config.config", mock_config):
with patch("leggen.utils.config.config", mock_config):
response = api_client.post("/api/v1/banks/connect", json=request_data)
assert response.status_code == 200
@@ -122,7 +122,7 @@ class TestBanksAPI:
return_value=httpx.Response(200, json=requisitions_data)
)
with patch("leggend.config.config", mock_config):
with patch("leggen.utils.config.config", mock_config):
response = api_client.get("/api/v1/banks/status")
assert response.status_code == 200
@@ -155,7 +155,7 @@ class TestBanksAPI:
return_value=httpx.Response(401, json={"detail": "Invalid credentials"})
)
with patch("leggend.config.config", mock_config):
with patch("leggen.utils.config.config", mock_config):
response = api_client.get("/api/v1/banks/institutions")
assert response.status_code == 500

View File

@@ -5,16 +5,16 @@ import requests
import requests_mock
from unittest.mock import patch
from leggen.api_client import LeggendAPIClient
from leggen.api_client import LeggenAPIClient
@pytest.mark.cli
class TestLeggendAPIClient:
class TestLeggenAPIClient:
"""Test the CLI API client."""
def test_health_check_success(self):
"""Test successful health check."""
client = LeggendAPIClient("http://localhost:8000")
client = LeggenAPIClient("http://localhost:8000")
with requests_mock.Mocker() as m:
m.get("http://localhost:8000/health", json={"status": "healthy"})
@@ -24,7 +24,7 @@ class TestLeggendAPIClient:
def test_health_check_failure(self):
"""Test health check failure."""
client = LeggendAPIClient("http://localhost:8000")
client = LeggenAPIClient("http://localhost:8000")
with requests_mock.Mocker() as m:
m.get("http://localhost:8000/health", status_code=500)
@@ -34,7 +34,7 @@ class TestLeggendAPIClient:
def test_get_institutions_success(self, sample_bank_data):
"""Test getting institutions via API client."""
client = LeggendAPIClient("http://localhost:8000")
client = LeggenAPIClient("http://localhost:8000")
api_response = {
"success": True,
@@ -51,7 +51,7 @@ class TestLeggendAPIClient:
def test_get_accounts_success(self, sample_account_data):
"""Test getting accounts via API client."""
client = LeggendAPIClient("http://localhost:8000")
client = LeggenAPIClient("http://localhost:8000")
api_response = {
"success": True,
@@ -68,7 +68,7 @@ class TestLeggendAPIClient:
def test_trigger_sync_success(self):
"""Test triggering sync via API client."""
client = LeggendAPIClient("http://localhost:8000")
client = LeggenAPIClient("http://localhost:8000")
api_response = {
"success": True,
@@ -84,14 +84,14 @@ class TestLeggendAPIClient:
def test_connection_error_handling(self):
"""Test handling of connection errors."""
client = LeggendAPIClient("http://localhost:9999") # Non-existent service
client = LeggenAPIClient("http://localhost:9999") # Non-existent service
with pytest.raises((requests.ConnectionError, requests.RequestException)):
client.get_accounts()
def test_http_error_handling(self):
"""Test handling of HTTP errors."""
client = LeggendAPIClient("http://localhost:8000")
client = LeggenAPIClient("http://localhost:8000")
with requests_mock.Mocker() as m:
m.get(
@@ -106,19 +106,19 @@ class TestLeggendAPIClient:
def test_custom_api_url(self):
"""Test using custom API URL."""
custom_url = "http://custom-host:9000"
client = LeggendAPIClient(custom_url)
client = LeggenAPIClient(custom_url)
assert client.base_url == custom_url
def test_environment_variable_url(self):
"""Test using environment variable for API URL."""
with patch.dict("os.environ", {"LEGGEND_API_URL": "http://env-host:7000"}):
client = LeggendAPIClient()
with patch.dict("os.environ", {"LEGGEN_API_URL": "http://env-host:7000"}):
client = LeggenAPIClient()
assert client.base_url == "http://env-host:7000"
def test_sync_with_options(self):
"""Test sync with various options."""
client = LeggendAPIClient("http://localhost:8000")
client = LeggenAPIClient("http://localhost:8000")
api_response = {
"success": True,
@@ -135,7 +135,7 @@ class TestLeggendAPIClient:
def test_get_scheduler_config(self):
"""Test getting scheduler configuration."""
client = LeggendAPIClient("http://localhost:8000")
client = LeggenAPIClient("http://localhost:8000")
api_response = {
"success": True,

View File

@@ -43,13 +43,13 @@ class TestTransactionsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.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",
"leggen.api.routes.transactions.database_service.get_transaction_count_from_db",
return_value=2,
),
):
@@ -90,13 +90,13 @@ class TestTransactionsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.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",
"leggen.api.routes.transactions.database_service.get_transaction_count_from_db",
return_value=1,
),
):
@@ -135,13 +135,13 @@ class TestTransactionsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.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",
"leggen.api.routes.transactions.database_service.get_transaction_count_from_db",
return_value=1,
),
):
@@ -178,13 +178,13 @@ class TestTransactionsAPI:
):
"""Test getting transactions when database returns empty result."""
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.api.routes.transactions.database_service.get_transactions_from_db",
return_value=[],
),
patch(
"leggend.api.routes.transactions.database_service.get_transaction_count_from_db",
"leggen.api.routes.transactions.database_service.get_transaction_count_from_db",
return_value=0,
),
):
@@ -203,9 +203,9 @@ class TestTransactionsAPI:
):
"""Test handling database error when getting transactions."""
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.api.routes.transactions.database_service.get_transactions_from_db",
side_effect=Exception("Database connection failed"),
),
):
@@ -243,9 +243,9 @@ class TestTransactionsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.api.routes.transactions.database_service.get_transactions_from_db",
return_value=mock_transactions,
),
):
@@ -284,9 +284,9 @@ class TestTransactionsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.api.routes.transactions.database_service.get_transactions_from_db",
return_value=mock_transactions,
) as mock_get_transactions,
):
@@ -306,9 +306,9 @@ class TestTransactionsAPI:
):
"""Test getting stats when no transactions match criteria."""
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.api.routes.transactions.database_service.get_transactions_from_db",
return_value=[],
),
):
@@ -331,9 +331,9 @@ class TestTransactionsAPI:
):
"""Test handling database error when getting stats."""
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.api.routes.transactions.database_service.get_transactions_from_db",
side_effect=Exception("Database connection failed"),
),
):
@@ -357,9 +357,9 @@ class TestTransactionsAPI:
]
with (
patch("leggend.config.config", mock_config),
patch("leggen.utils.config.config", mock_config),
patch(
"leggend.api.routes.transactions.database_service.get_transactions_from_db",
"leggen.api.routes.transactions.database_service.get_transactions_from_db",
return_value=mock_transactions,
) as mock_get_transactions,
):

View File

@@ -3,7 +3,7 @@
import pytest
from unittest.mock import patch
from leggend.config import Config
from leggen.utils.config import Config
@pytest.mark.unit

View File

@@ -14,7 +14,6 @@ class MockContext:
"""Mock context for testing."""
@pytest.mark.unit
class TestConfigurablePaths:
"""Test configurable path management."""

View File

@@ -4,7 +4,7 @@ import pytest
from unittest.mock import patch
from datetime import datetime
from leggend.services.database_service import DatabaseService
from leggen.services.database_service import DatabaseService
@pytest.fixture

View File

@@ -4,7 +4,7 @@ import pytest
from unittest.mock import patch, AsyncMock, MagicMock
from datetime import datetime
from leggend.background.scheduler import BackgroundScheduler
from leggen.background.scheduler import BackgroundScheduler
@pytest.mark.unit
@@ -20,8 +20,8 @@ class TestBackgroundScheduler:
def scheduler(self):
"""Create scheduler instance for testing."""
with (
patch("leggend.background.scheduler.SyncService"),
patch("leggend.background.scheduler.config") as mock_config,
patch("leggen.background.scheduler.SyncService"),
patch("leggen.background.scheduler.config") as mock_config,
):
mock_config.scheduler_config = {
"sync": {"enabled": True, "hour": 3, "minute": 0}
@@ -37,7 +37,7 @@ class TestBackgroundScheduler:
def test_scheduler_start_default_config(self, scheduler, mock_config):
"""Test starting scheduler with default configuration."""
with patch("leggend.config.config") as mock_config_obj:
with patch("leggen.utils.config.config") as mock_config_obj:
mock_config_obj.scheduler_config = mock_config
# Mock the job that gets added
@@ -58,7 +58,7 @@ class TestBackgroundScheduler:
with (
patch.object(scheduler, "scheduler") as mock_scheduler,
patch("leggend.background.scheduler.config") as mock_config_obj,
patch("leggen.background.scheduler.config") as mock_config_obj,
):
mock_config_obj.scheduler_config = disabled_config
mock_scheduler.running = False
@@ -79,7 +79,7 @@ class TestBackgroundScheduler:
}
}
with patch("leggend.config.config") as mock_config_obj:
with patch("leggen.utils.config.config") as mock_config_obj:
mock_config_obj.scheduler_config = cron_config
scheduler.start()
@@ -97,7 +97,7 @@ class TestBackgroundScheduler:
with (
patch.object(scheduler, "scheduler") as mock_scheduler,
patch("leggend.background.scheduler.config") as mock_config_obj,
patch("leggen.background.scheduler.config") as mock_config_obj,
):
mock_config_obj.scheduler_config = invalid_cron_config
mock_scheduler.running = False
@@ -187,7 +187,7 @@ class TestBackgroundScheduler:
def test_scheduler_job_max_instances(self, scheduler, mock_config):
"""Test that sync jobs have max_instances=1."""
with patch("leggend.config.config") as mock_config_obj:
with patch("leggen.utils.config.config") as mock_config_obj:
mock_config_obj.scheduler_config = mock_config
scheduler.start()