mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 11:22:21 +00:00
chore: Implement code review suggestions and format code.
This commit is contained in:
committed by
Elisiário Couto
parent
47164e8546
commit
de3da84dff
@@ -1,4 +1,5 @@
|
||||
"""Tests for accounts API endpoints."""
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
import httpx
|
||||
@@ -8,48 +9,47 @@ from unittest.mock import patch
|
||||
@pytest.mark.api
|
||||
class TestAccountsAPI:
|
||||
"""Test account-related API endpoints."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_get_all_accounts_success(self, api_client, mock_config, mock_auth_token, sample_account_data):
|
||||
def test_get_all_accounts_success(
|
||||
self, api_client, mock_config, mock_auth_token, sample_account_data
|
||||
):
|
||||
"""Test successful retrieval of all accounts."""
|
||||
requisitions_data = {
|
||||
"results": [
|
||||
{
|
||||
"id": "req-123",
|
||||
"accounts": ["test-account-123"]
|
||||
}
|
||||
]
|
||||
"results": [{"id": "req-123", "accounts": ["test-account-123"]}]
|
||||
}
|
||||
|
||||
|
||||
balances_data = {
|
||||
"balances": [
|
||||
{
|
||||
"balanceAmount": {"amount": "100.50", "currency": "EUR"},
|
||||
"balanceType": "interimAvailable",
|
||||
"lastChangeDateTime": "2025-09-01T09:30:00Z"
|
||||
"lastChangeDateTime": "2025-09-01T09:30:00Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Mock GoCardless API calls
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/requisitions/").mock(
|
||||
return_value=httpx.Response(200, json=requisitions_data)
|
||||
)
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/").mock(
|
||||
return_value=httpx.Response(200, json=sample_account_data)
|
||||
)
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/balances/").mock(
|
||||
return_value=httpx.Response(200, json=balances_data)
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/"
|
||||
).mock(return_value=httpx.Response(200, json=sample_account_data))
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/balances/"
|
||||
).mock(return_value=httpx.Response(200, json=balances_data))
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get("/api/v1/accounts")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
@@ -61,33 +61,37 @@ class TestAccountsAPI:
|
||||
assert account["balances"][0]["amount"] == 100.50
|
||||
|
||||
@respx.mock
|
||||
def test_get_account_details_success(self, api_client, mock_config, mock_auth_token, sample_account_data):
|
||||
def test_get_account_details_success(
|
||||
self, api_client, mock_config, mock_auth_token, sample_account_data
|
||||
):
|
||||
"""Test successful retrieval of specific account details."""
|
||||
balances_data = {
|
||||
"balances": [
|
||||
{
|
||||
"balanceAmount": {"amount": "250.75", "currency": "EUR"},
|
||||
"balanceType": "interimAvailable"
|
||||
"balanceType": "interimAvailable",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Mock GoCardless API calls
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/").mock(
|
||||
return_value=httpx.Response(200, json=sample_account_data)
|
||||
)
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/balances/").mock(
|
||||
return_value=httpx.Response(200, json=balances_data)
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/"
|
||||
).mock(return_value=httpx.Response(200, json=sample_account_data))
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/balances/"
|
||||
).mock(return_value=httpx.Response(200, json=balances_data))
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get("/api/v1/accounts/test-account-123")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
@@ -97,35 +101,39 @@ class TestAccountsAPI:
|
||||
assert len(account["balances"]) == 1
|
||||
|
||||
@respx.mock
|
||||
def test_get_account_balances_success(self, api_client, mock_config, mock_auth_token):
|
||||
def test_get_account_balances_success(
|
||||
self, api_client, mock_config, mock_auth_token
|
||||
):
|
||||
"""Test successful retrieval of account balances."""
|
||||
balances_data = {
|
||||
"balances": [
|
||||
{
|
||||
"balanceAmount": {"amount": "1000.00", "currency": "EUR"},
|
||||
"balanceType": "interimAvailable",
|
||||
"lastChangeDateTime": "2025-09-01T10:00:00Z"
|
||||
"lastChangeDateTime": "2025-09-01T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"balanceAmount": {"amount": "950.00", "currency": "EUR"},
|
||||
"balanceType": "expected"
|
||||
}
|
||||
"balanceAmount": {"amount": "950.00", "currency": "EUR"},
|
||||
"balanceType": "expected",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Mock GoCardless API
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/balances/").mock(
|
||||
return_value=httpx.Response(200, json=balances_data)
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/balances/"
|
||||
).mock(return_value=httpx.Response(200, json=balances_data))
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get("/api/v1/accounts/test-account-123/balances")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
@@ -135,29 +143,40 @@ class TestAccountsAPI:
|
||||
assert data["data"][0]["balance_type"] == "interimAvailable"
|
||||
|
||||
@respx.mock
|
||||
def test_get_account_transactions_success(self, api_client, mock_config, mock_auth_token, sample_account_data, sample_transaction_data):
|
||||
def test_get_account_transactions_success(
|
||||
self,
|
||||
api_client,
|
||||
mock_config,
|
||||
mock_auth_token,
|
||||
sample_account_data,
|
||||
sample_transaction_data,
|
||||
):
|
||||
"""Test successful retrieval of account transactions."""
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Mock GoCardless API calls
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/").mock(
|
||||
return_value=httpx.Response(200, json=sample_account_data)
|
||||
)
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/transactions/").mock(
|
||||
return_value=httpx.Response(200, json=sample_transaction_data)
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
response = api_client.get("/api/v1/accounts/test-account-123/transactions?summary_only=true")
|
||||
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/"
|
||||
).mock(return_value=httpx.Response(200, json=sample_account_data))
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/transactions/"
|
||||
).mock(return_value=httpx.Response(200, json=sample_transaction_data))
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get(
|
||||
"/api/v1/accounts/test-account-123/transactions?summary_only=true"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 1
|
||||
|
||||
|
||||
transaction = data["data"][0]
|
||||
assert transaction["internal_transaction_id"] == "txn-123"
|
||||
assert transaction["amount"] == -10.50
|
||||
@@ -165,29 +184,40 @@ class TestAccountsAPI:
|
||||
assert transaction["description"] == "Coffee Shop Payment"
|
||||
|
||||
@respx.mock
|
||||
def test_get_account_transactions_full_details(self, api_client, mock_config, mock_auth_token, sample_account_data, sample_transaction_data):
|
||||
def test_get_account_transactions_full_details(
|
||||
self,
|
||||
api_client,
|
||||
mock_config,
|
||||
mock_auth_token,
|
||||
sample_account_data,
|
||||
sample_transaction_data,
|
||||
):
|
||||
"""Test retrieval of full transaction details."""
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Mock GoCardless API calls
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/").mock(
|
||||
return_value=httpx.Response(200, json=sample_account_data)
|
||||
)
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/transactions/").mock(
|
||||
return_value=httpx.Response(200, json=sample_transaction_data)
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
response = api_client.get("/api/v1/accounts/test-account-123/transactions?summary_only=false")
|
||||
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/"
|
||||
).mock(return_value=httpx.Response(200, json=sample_account_data))
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/test-account-123/transactions/"
|
||||
).mock(return_value=httpx.Response(200, json=sample_transaction_data))
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get(
|
||||
"/api/v1/accounts/test-account-123/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["internal_transaction_id"] == "txn-123"
|
||||
assert transaction["institution_id"] == "REVOLUT_REVOLT21"
|
||||
@@ -200,14 +230,18 @@ class TestAccountsAPI:
|
||||
with respx.mock:
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/accounts/nonexistent/").mock(
|
||||
|
||||
respx.get(
|
||||
"https://bankaccountdata.gocardless.com/api/v2/accounts/nonexistent/"
|
||||
).mock(
|
||||
return_value=httpx.Response(404, json={"detail": "Account not found"})
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get("/api/v1/accounts/nonexistent")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests for banks API endpoints."""
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
import httpx
|
||||
@@ -10,23 +11,27 @@ from leggend.services.gocardless_service import GoCardlessService
|
||||
@pytest.mark.api
|
||||
class TestBanksAPI:
|
||||
"""Test bank-related API endpoints."""
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_get_institutions_success(self, api_client, mock_config, mock_auth_token, sample_bank_data):
|
||||
def test_get_institutions_success(
|
||||
self, api_client, mock_config, mock_auth_token, sample_bank_data
|
||||
):
|
||||
"""Test successful retrieval of bank institutions."""
|
||||
# Mock GoCardless token creation/refresh
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Mock GoCardless institutions API
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/institutions/").mock(
|
||||
return_value=httpx.Response(200, json=sample_bank_data)
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get("/api/v1/banks/institutions?country=PT")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
@@ -39,17 +44,19 @@ class TestBanksAPI:
|
||||
"""Test institutions endpoint with invalid country code."""
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Mock empty institutions response for invalid country
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/institutions/").mock(
|
||||
return_value=httpx.Response(200, json=[])
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get("/api/v1/banks/institutions?country=XX")
|
||||
|
||||
|
||||
# Should still work but return empty or filtered results
|
||||
assert response.status_code in [200, 404]
|
||||
|
||||
@@ -61,27 +68,29 @@ class TestBanksAPI:
|
||||
"institution_id": "REVOLUT_REVOLT21",
|
||||
"status": "CR",
|
||||
"created": "2025-09-02T00:00:00Z",
|
||||
"link": "https://example.com/auth"
|
||||
"link": "https://example.com/auth",
|
||||
}
|
||||
|
||||
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Mock GoCardless requisitions API
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/requisitions/").mock(
|
||||
return_value=httpx.Response(200, json=requisition_data)
|
||||
)
|
||||
|
||||
|
||||
request_data = {
|
||||
"institution_id": "REVOLUT_REVOLT21",
|
||||
"redirect_url": "http://localhost:8000/"
|
||||
"redirect_url": "http://localhost:8000/",
|
||||
}
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.post("/api/v1/banks/connect", json=request_data)
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
@@ -95,27 +104,29 @@ class TestBanksAPI:
|
||||
"results": [
|
||||
{
|
||||
"id": "req-123",
|
||||
"institution_id": "REVOLUT_REVOLT21",
|
||||
"institution_id": "REVOLUT_REVOLT21",
|
||||
"status": "LN",
|
||||
"created": "2025-09-02T00:00:00Z",
|
||||
"accounts": ["acc-123"]
|
||||
"accounts": ["acc-123"],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Mock GoCardless token creation
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(200, json={"access": "test-token", "refresh": "test-refresh"})
|
||||
return_value=httpx.Response(
|
||||
200, json={"access": "test-token", "refresh": "test-refresh"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Mock GoCardless requisitions API
|
||||
respx.get("https://bankaccountdata.gocardless.com/api/v2/requisitions/").mock(
|
||||
return_value=httpx.Response(200, json=requisitions_data)
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get("/api/v1/banks/status")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
@@ -126,12 +137,12 @@ class TestBanksAPI:
|
||||
def test_get_supported_countries(self, api_client):
|
||||
"""Test supported countries endpoint."""
|
||||
response = api_client.get("/api/v1/banks/countries")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) > 0
|
||||
|
||||
|
||||
# Check some expected countries
|
||||
country_codes = [country["code"] for country in data["data"]]
|
||||
assert "PT" in country_codes
|
||||
@@ -145,10 +156,10 @@ class TestBanksAPI:
|
||||
respx.post("https://bankaccountdata.gocardless.com/api/v2/token/new/").mock(
|
||||
return_value=httpx.Response(401, json={"detail": "Invalid credentials"})
|
||||
)
|
||||
|
||||
with patch('leggend.config.config', mock_config):
|
||||
|
||||
with patch("leggend.config.config", mock_config):
|
||||
response = api_client.get("/api/v1/banks/institutions")
|
||||
|
||||
|
||||
assert response.status_code == 500
|
||||
data = response.json()
|
||||
assert "Failed to get institutions" in data["detail"]
|
||||
assert "Failed to get institutions" in data["detail"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests for CLI API client."""
|
||||
|
||||
import pytest
|
||||
import requests_mock
|
||||
from unittest.mock import patch
|
||||
@@ -13,36 +14,36 @@ class TestLeggendAPIClient:
|
||||
def test_health_check_success(self):
|
||||
"""Test successful health check."""
|
||||
client = LeggendAPIClient("http://localhost:8000")
|
||||
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("http://localhost:8000/health", json={"status": "healthy"})
|
||||
|
||||
|
||||
result = client.health_check()
|
||||
assert result is True
|
||||
|
||||
def test_health_check_failure(self):
|
||||
"""Test health check failure."""
|
||||
client = LeggendAPIClient("http://localhost:8000")
|
||||
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("http://localhost:8000/health", status_code=500)
|
||||
|
||||
|
||||
result = client.health_check()
|
||||
assert result is False
|
||||
|
||||
def test_get_institutions_success(self, sample_bank_data):
|
||||
"""Test getting institutions via API client."""
|
||||
client = LeggendAPIClient("http://localhost:8000")
|
||||
|
||||
|
||||
api_response = {
|
||||
"success": True,
|
||||
"data": sample_bank_data,
|
||||
"message": "Found 2 institutions for PT"
|
||||
"message": "Found 2 institutions for PT",
|
||||
}
|
||||
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("http://localhost:8000/api/v1/banks/institutions", json=api_response)
|
||||
|
||||
|
||||
result = client.get_institutions("PT")
|
||||
assert len(result) == 2
|
||||
assert result[0]["id"] == "REVOLUT_REVOLT21"
|
||||
@@ -50,16 +51,16 @@ class TestLeggendAPIClient:
|
||||
def test_get_accounts_success(self, sample_account_data):
|
||||
"""Test getting accounts via API client."""
|
||||
client = LeggendAPIClient("http://localhost:8000")
|
||||
|
||||
|
||||
api_response = {
|
||||
"success": True,
|
||||
"data": [sample_account_data],
|
||||
"message": "Retrieved 1 accounts"
|
||||
"message": "Retrieved 1 accounts",
|
||||
}
|
||||
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("http://localhost:8000/api/v1/accounts", json=api_response)
|
||||
|
||||
|
||||
result = client.get_accounts()
|
||||
assert len(result) == 1
|
||||
assert result[0]["id"] == "test-account-123"
|
||||
@@ -67,34 +68,37 @@ class TestLeggendAPIClient:
|
||||
def test_trigger_sync_success(self):
|
||||
"""Test triggering sync via API client."""
|
||||
client = LeggendAPIClient("http://localhost:8000")
|
||||
|
||||
|
||||
api_response = {
|
||||
"success": True,
|
||||
"data": {"sync_started": True, "force": False},
|
||||
"message": "Started sync for all accounts"
|
||||
"message": "Started sync for all accounts",
|
||||
}
|
||||
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.post("http://localhost:8000/api/v1/sync", json=api_response)
|
||||
|
||||
|
||||
result = client.trigger_sync()
|
||||
assert result["sync_started"] is True
|
||||
|
||||
def test_connection_error_handling(self):
|
||||
"""Test handling of connection errors."""
|
||||
client = LeggendAPIClient("http://localhost:9999") # Non-existent service
|
||||
|
||||
|
||||
with pytest.raises(Exception):
|
||||
client.get_accounts()
|
||||
|
||||
def test_http_error_handling(self):
|
||||
"""Test handling of HTTP errors."""
|
||||
client = LeggendAPIClient("http://localhost:8000")
|
||||
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("http://localhost:8000/api/v1/accounts", status_code=500,
|
||||
json={"detail": "Internal server error"})
|
||||
|
||||
m.get(
|
||||
"http://localhost:8000/api/v1/accounts",
|
||||
status_code=500,
|
||||
json={"detail": "Internal server error"},
|
||||
)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
client.get_accounts()
|
||||
|
||||
@@ -102,28 +106,28 @@ class TestLeggendAPIClient:
|
||||
"""Test using custom API URL."""
|
||||
custom_url = "http://custom-host:9000"
|
||||
client = LeggendAPIClient(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'}):
|
||||
with patch.dict("os.environ", {"LEGGEND_API_URL": "http://env-host:7000"}):
|
||||
client = LeggendAPIClient()
|
||||
assert client.base_url == "http://env-host:7000"
|
||||
|
||||
def test_sync_with_options(self):
|
||||
"""Test sync with various options."""
|
||||
client = LeggendAPIClient("http://localhost:8000")
|
||||
|
||||
|
||||
api_response = {
|
||||
"success": True,
|
||||
"data": {"sync_started": True, "force": True},
|
||||
"message": "Started sync for 2 specific accounts"
|
||||
"message": "Started sync for 2 specific accounts",
|
||||
}
|
||||
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.post("http://localhost:8000/api/v1/sync", json=api_response)
|
||||
|
||||
|
||||
result = client.trigger_sync(account_ids=["acc1", "acc2"], force=True)
|
||||
assert result["sync_started"] is True
|
||||
assert result["force"] is True
|
||||
@@ -131,20 +135,20 @@ class TestLeggendAPIClient:
|
||||
def test_get_scheduler_config(self):
|
||||
"""Test getting scheduler configuration."""
|
||||
client = LeggendAPIClient("http://localhost:8000")
|
||||
|
||||
|
||||
api_response = {
|
||||
"success": True,
|
||||
"data": {
|
||||
"enabled": True,
|
||||
"hour": 3,
|
||||
"minute": 0,
|
||||
"next_scheduled_sync": "2025-09-03T03:00:00Z"
|
||||
}
|
||||
"next_scheduled_sync": "2025-09-03T03:00:00Z",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("http://localhost:8000/api/v1/sync/scheduler", json=api_response)
|
||||
|
||||
|
||||
result = client.get_scheduler_config()
|
||||
assert result["enabled"] is True
|
||||
assert result["hour"] == 3
|
||||
assert result["hour"] == 3
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests for configuration management."""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
@@ -23,25 +24,24 @@ class TestConfig:
|
||||
"gocardless": {
|
||||
"key": "test-key",
|
||||
"secret": "test-secret",
|
||||
"url": "https://test.example.com"
|
||||
"url": "https://test.example.com",
|
||||
},
|
||||
"database": {
|
||||
"sqlite": True
|
||||
}
|
||||
"database": {"sqlite": True},
|
||||
}
|
||||
|
||||
|
||||
config_file = temp_config_dir / "config.toml"
|
||||
with open(config_file, "wb") as f:
|
||||
import tomli_w
|
||||
|
||||
tomli_w.dump(config_data, f)
|
||||
|
||||
|
||||
config = Config()
|
||||
# Reset singleton state for testing
|
||||
config._config = None
|
||||
config._config_path = None
|
||||
|
||||
|
||||
result = config.load_config(str(config_file))
|
||||
|
||||
|
||||
assert result == config_data
|
||||
assert config.gocardless_config["key"] == "test-key"
|
||||
assert config.database_config["sqlite"] is True
|
||||
@@ -50,87 +50,84 @@ class TestConfig:
|
||||
"""Test handling of missing configuration file."""
|
||||
config = Config()
|
||||
config._config = None # Reset for test
|
||||
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
config.load_config("/nonexistent/config.toml")
|
||||
|
||||
def test_save_config_success(self, temp_config_dir):
|
||||
"""Test successful configuration saving."""
|
||||
config_data = {
|
||||
"gocardless": {
|
||||
"key": "new-key",
|
||||
"secret": "new-secret"
|
||||
}
|
||||
}
|
||||
|
||||
config_data = {"gocardless": {"key": "new-key", "secret": "new-secret"}}
|
||||
|
||||
config_file = temp_config_dir / "new_config.toml"
|
||||
config = Config()
|
||||
config._config = None
|
||||
|
||||
|
||||
config.save_config(config_data, str(config_file))
|
||||
|
||||
|
||||
# Verify file was created and contains correct data
|
||||
assert config_file.exists()
|
||||
|
||||
|
||||
import tomllib
|
||||
|
||||
with open(config_file, "rb") as f:
|
||||
saved_data = tomllib.load(f)
|
||||
|
||||
|
||||
assert saved_data == config_data
|
||||
|
||||
def test_update_config_success(self, temp_config_dir):
|
||||
"""Test updating configuration values."""
|
||||
initial_config = {
|
||||
"gocardless": {"key": "old-key"},
|
||||
"database": {"sqlite": True}
|
||||
"database": {"sqlite": True},
|
||||
}
|
||||
|
||||
|
||||
config_file = temp_config_dir / "config.toml"
|
||||
with open(config_file, "wb") as f:
|
||||
import tomli_w
|
||||
|
||||
tomli_w.dump(initial_config, f)
|
||||
|
||||
|
||||
config = Config()
|
||||
config._config = None
|
||||
config.load_config(str(config_file))
|
||||
|
||||
|
||||
config.update_config("gocardless", "key", "new-key")
|
||||
|
||||
|
||||
assert config.gocardless_config["key"] == "new-key"
|
||||
|
||||
|
||||
# Verify it was saved to file
|
||||
import tomllib
|
||||
|
||||
with open(config_file, "rb") as f:
|
||||
saved_data = tomllib.load(f)
|
||||
assert saved_data["gocardless"]["key"] == "new-key"
|
||||
|
||||
def test_update_section_success(self, temp_config_dir):
|
||||
"""Test updating entire configuration section."""
|
||||
initial_config = {
|
||||
"database": {"sqlite": True}
|
||||
}
|
||||
|
||||
initial_config = {"database": {"sqlite": True}}
|
||||
|
||||
config_file = temp_config_dir / "config.toml"
|
||||
with open(config_file, "wb") as f:
|
||||
import tomli_w
|
||||
|
||||
tomli_w.dump(initial_config, f)
|
||||
|
||||
|
||||
config = Config()
|
||||
config._config = None
|
||||
config.load_config(str(config_file))
|
||||
|
||||
|
||||
new_db_config = {"sqlite": False, "path": "./custom.db"}
|
||||
config.update_section("database", new_db_config)
|
||||
|
||||
|
||||
assert config.database_config == new_db_config
|
||||
|
||||
def test_scheduler_config_defaults(self):
|
||||
"""Test scheduler configuration with defaults."""
|
||||
config = Config()
|
||||
config._config = {} # Empty config
|
||||
|
||||
|
||||
scheduler_config = config.scheduler_config
|
||||
|
||||
|
||||
assert scheduler_config["sync"]["enabled"] is True
|
||||
assert scheduler_config["sync"]["hour"] == 3
|
||||
assert scheduler_config["sync"]["minute"] == 0
|
||||
@@ -144,16 +141,16 @@ class TestConfig:
|
||||
"enabled": False,
|
||||
"hour": 6,
|
||||
"minute": 30,
|
||||
"cron": "0 6 * * 1-5"
|
||||
"cron": "0 6 * * 1-5",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
config = Config()
|
||||
config._config = custom_config
|
||||
|
||||
|
||||
scheduler_config = config.scheduler_config
|
||||
|
||||
|
||||
assert scheduler_config["sync"]["enabled"] is False
|
||||
assert scheduler_config["sync"]["hour"] == 6
|
||||
assert scheduler_config["sync"]["minute"] == 30
|
||||
@@ -161,26 +158,28 @@ class TestConfig:
|
||||
|
||||
def test_environment_variable_config_path(self):
|
||||
"""Test using environment variable for config path."""
|
||||
with patch.dict('os.environ', {'LEGGEN_CONFIG_FILE': '/custom/path/config.toml'}):
|
||||
with patch.dict(
|
||||
"os.environ", {"LEGGEN_CONFIG_FILE": "/custom/path/config.toml"}
|
||||
):
|
||||
config = Config()
|
||||
config._config = None
|
||||
|
||||
with patch('builtins.open', side_effect=FileNotFoundError):
|
||||
|
||||
with patch("builtins.open", side_effect=FileNotFoundError):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
config.load_config()
|
||||
|
||||
def test_notifications_config(self):
|
||||
"""Test notifications configuration access."""
|
||||
"""Test notifications configuration access."""
|
||||
custom_config = {
|
||||
"notifications": {
|
||||
"discord": {"webhook": "https://discord.webhook", "enabled": True},
|
||||
"telegram": {"token": "bot-token", "chat_id": 123}
|
||||
"telegram": {"token": "bot-token", "chat_id": 123},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
config = Config()
|
||||
config._config = custom_config
|
||||
|
||||
|
||||
notifications = config.notifications_config
|
||||
assert notifications["discord"]["webhook"] == "https://discord.webhook"
|
||||
assert notifications["telegram"]["token"] == "bot-token"
|
||||
@@ -190,13 +189,13 @@ class TestConfig:
|
||||
custom_config = {
|
||||
"filters": {
|
||||
"case-insensitive": {"salary": "SALARY", "bills": "BILL"},
|
||||
"amount_threshold": 100.0
|
||||
"amount_threshold": 100.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
config = Config()
|
||||
config._config = custom_config
|
||||
|
||||
|
||||
filters = config.filters_config
|
||||
assert filters["case-insensitive"]["salary"] == "SALARY"
|
||||
assert filters["amount_threshold"] == 100.0
|
||||
assert filters["amount_threshold"] == 100.0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests for background scheduler."""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import Mock, patch, AsyncMock, MagicMock
|
||||
@@ -16,23 +17,19 @@ class TestBackgroundScheduler:
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
"""Mock configuration for scheduler tests."""
|
||||
return {
|
||||
"sync": {
|
||||
"enabled": True,
|
||||
"hour": 3,
|
||||
"minute": 0,
|
||||
"cron": None
|
||||
}
|
||||
}
|
||||
return {"sync": {"enabled": True, "hour": 3, "minute": 0, "cron": None}}
|
||||
|
||||
@pytest.fixture
|
||||
def scheduler(self):
|
||||
"""Create scheduler instance for testing."""
|
||||
with patch('leggend.background.scheduler.SyncService'), \
|
||||
patch('leggend.background.scheduler.config') as mock_config:
|
||||
|
||||
mock_config.scheduler_config = {"sync": {"enabled": True, "hour": 3, "minute": 0}}
|
||||
|
||||
with (
|
||||
patch("leggend.background.scheduler.SyncService"),
|
||||
patch("leggend.background.scheduler.config") as mock_config,
|
||||
):
|
||||
mock_config.scheduler_config = {
|
||||
"sync": {"enabled": True, "hour": 3, "minute": 0}
|
||||
}
|
||||
|
||||
# Create scheduler and replace its AsyncIO scheduler with a mock
|
||||
scheduler = BackgroundScheduler()
|
||||
mock_scheduler = MagicMock()
|
||||
@@ -43,35 +40,34 @@ 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("leggend.config.config") as mock_config_obj:
|
||||
mock_config_obj.scheduler_config = mock_config
|
||||
|
||||
|
||||
# Mock the job that gets added
|
||||
mock_job = MagicMock()
|
||||
mock_job.id = "daily_sync"
|
||||
scheduler.scheduler.get_jobs.return_value = [mock_job]
|
||||
|
||||
|
||||
scheduler.start()
|
||||
|
||||
|
||||
# Verify scheduler.start() was called
|
||||
scheduler.scheduler.start.assert_called_once()
|
||||
# Verify add_job was called
|
||||
# Verify add_job was called
|
||||
scheduler.scheduler.add_job.assert_called_once()
|
||||
|
||||
def test_scheduler_start_disabled(self, scheduler):
|
||||
"""Test scheduler behavior when sync is disabled."""
|
||||
disabled_config = {
|
||||
"sync": {"enabled": False}
|
||||
}
|
||||
|
||||
with patch.object(scheduler, 'scheduler') as mock_scheduler, \
|
||||
patch('leggend.background.scheduler.config') as mock_config_obj:
|
||||
|
||||
disabled_config = {"sync": {"enabled": False}}
|
||||
|
||||
with (
|
||||
patch.object(scheduler, "scheduler") as mock_scheduler,
|
||||
patch("leggend.background.scheduler.config") as mock_config_obj,
|
||||
):
|
||||
mock_config_obj.scheduler_config = disabled_config
|
||||
mock_scheduler.running = False
|
||||
|
||||
|
||||
scheduler.start()
|
||||
|
||||
|
||||
# Verify scheduler.start() was called
|
||||
mock_scheduler.start.assert_called_once()
|
||||
# Verify add_job was NOT called for disabled sync
|
||||
@@ -82,39 +78,35 @@ class TestBackgroundScheduler:
|
||||
cron_config = {
|
||||
"sync": {
|
||||
"enabled": True,
|
||||
"cron": "0 6 * * 1-5" # 6 AM on weekdays
|
||||
"cron": "0 6 * * 1-5", # 6 AM on weekdays
|
||||
}
|
||||
}
|
||||
|
||||
with patch('leggend.config.config') as mock_config_obj:
|
||||
|
||||
with patch("leggend.config.config") as mock_config_obj:
|
||||
mock_config_obj.scheduler_config = cron_config
|
||||
|
||||
|
||||
scheduler.start()
|
||||
|
||||
|
||||
# Verify scheduler.start() and add_job were called
|
||||
scheduler.scheduler.start.assert_called_once()
|
||||
scheduler.scheduler.add_job.assert_called_once()
|
||||
# Verify job was added with correct ID
|
||||
call_args = scheduler.scheduler.add_job.call_args
|
||||
assert call_args.kwargs['id'] == 'daily_sync'
|
||||
assert call_args.kwargs["id"] == "daily_sync"
|
||||
|
||||
def test_scheduler_start_invalid_cron(self, scheduler):
|
||||
"""Test handling of invalid cron expressions."""
|
||||
invalid_cron_config = {
|
||||
"sync": {
|
||||
"enabled": True,
|
||||
"cron": "invalid cron"
|
||||
}
|
||||
}
|
||||
|
||||
with patch.object(scheduler, 'scheduler') as mock_scheduler, \
|
||||
patch('leggend.background.scheduler.config') as mock_config_obj:
|
||||
|
||||
invalid_cron_config = {"sync": {"enabled": True, "cron": "invalid cron"}}
|
||||
|
||||
with (
|
||||
patch.object(scheduler, "scheduler") as mock_scheduler,
|
||||
patch("leggend.background.scheduler.config") as mock_config_obj,
|
||||
):
|
||||
mock_config_obj.scheduler_config = invalid_cron_config
|
||||
mock_scheduler.running = False
|
||||
|
||||
|
||||
scheduler.start()
|
||||
|
||||
|
||||
# With invalid cron, scheduler.start() should not be called due to early return
|
||||
# and add_job should not be called
|
||||
mock_scheduler.start.assert_not_called()
|
||||
@@ -123,24 +115,20 @@ class TestBackgroundScheduler:
|
||||
def test_scheduler_shutdown(self, scheduler):
|
||||
"""Test scheduler shutdown."""
|
||||
scheduler.scheduler.running = True
|
||||
|
||||
|
||||
scheduler.shutdown()
|
||||
|
||||
|
||||
scheduler.scheduler.shutdown.assert_called_once()
|
||||
|
||||
def test_reschedule_sync(self, scheduler, mock_config):
|
||||
"""Test rescheduling sync job."""
|
||||
scheduler.scheduler.running = True
|
||||
|
||||
|
||||
# Reschedule with new config
|
||||
new_config = {
|
||||
"enabled": True,
|
||||
"hour": 6,
|
||||
"minute": 30
|
||||
}
|
||||
|
||||
new_config = {"enabled": True, "hour": 6, "minute": 30}
|
||||
|
||||
scheduler.reschedule_sync(new_config)
|
||||
|
||||
|
||||
# Verify remove_job and add_job were called
|
||||
scheduler.scheduler.remove_job.assert_called_once_with("daily_sync")
|
||||
scheduler.scheduler.add_job.assert_called_once()
|
||||
@@ -148,11 +136,11 @@ class TestBackgroundScheduler:
|
||||
def test_reschedule_sync_disable(self, scheduler, mock_config):
|
||||
"""Test disabling sync via reschedule."""
|
||||
scheduler.scheduler.running = True
|
||||
|
||||
|
||||
# Disable sync
|
||||
disabled_config = {"enabled": False}
|
||||
scheduler.reschedule_sync(disabled_config)
|
||||
|
||||
|
||||
# Job should be removed but not re-added
|
||||
scheduler.scheduler.remove_job.assert_called_once_with("daily_sync")
|
||||
scheduler.scheduler.add_job.assert_not_called()
|
||||
@@ -162,9 +150,9 @@ class TestBackgroundScheduler:
|
||||
mock_job = MagicMock()
|
||||
mock_job.next_run_time = datetime(2025, 9, 2, 3, 0)
|
||||
scheduler.scheduler.get_job.return_value = mock_job
|
||||
|
||||
|
||||
next_time = scheduler.get_next_sync_time()
|
||||
|
||||
|
||||
assert next_time is not None
|
||||
assert isinstance(next_time, datetime)
|
||||
scheduler.scheduler.get_job.assert_called_once_with("daily_sync")
|
||||
@@ -172,9 +160,9 @@ class TestBackgroundScheduler:
|
||||
def test_get_next_sync_time_no_job(self, scheduler):
|
||||
"""Test getting next sync time when no job is scheduled."""
|
||||
scheduler.scheduler.get_job.return_value = None
|
||||
|
||||
|
||||
next_time = scheduler.get_next_sync_time()
|
||||
|
||||
|
||||
assert next_time is None
|
||||
scheduler.scheduler.get_job.assert_called_once_with("daily_sync")
|
||||
|
||||
@@ -183,9 +171,9 @@ class TestBackgroundScheduler:
|
||||
"""Test successful sync job execution."""
|
||||
mock_sync_service = AsyncMock()
|
||||
scheduler.sync_service = mock_sync_service
|
||||
|
||||
|
||||
await scheduler._run_sync()
|
||||
|
||||
|
||||
mock_sync_service.sync_all_accounts.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -194,18 +182,18 @@ class TestBackgroundScheduler:
|
||||
mock_sync_service = AsyncMock()
|
||||
mock_sync_service.sync_all_accounts.side_effect = Exception("Sync failed")
|
||||
scheduler.sync_service = mock_sync_service
|
||||
|
||||
|
||||
# Should not raise exception, just log error
|
||||
await scheduler._run_sync()
|
||||
|
||||
|
||||
mock_sync_service.sync_all_accounts.assert_called_once()
|
||||
|
||||
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("leggend.config.config") as mock_config_obj:
|
||||
mock_config_obj.scheduler_config = mock_config
|
||||
scheduler.start()
|
||||
|
||||
|
||||
# Verify add_job was called with max_instances=1
|
||||
call_args = scheduler.scheduler.add_job.call_args
|
||||
assert call_args.kwargs['max_instances'] == 1
|
||||
assert call_args.kwargs["max_instances"] == 1
|
||||
|
||||
Reference in New Issue
Block a user