feat: Add comprehensive test suite with 46 passing tests

- Add pytest configuration in pyproject.toml with markers and async support
- Create shared test fixtures in tests/conftest.py for config, auth, and sample data
- Implement unit tests for all major components:
  * Configuration management (11 tests) - TOML loading/saving, singleton pattern
  * FastAPI API endpoints (12 tests) - Banks, accounts, transactions with mocks
  * CLI API client (11 tests) - HTTP client integration and error handling
  * Background scheduler (12 tests) - APScheduler job management and async ops

- Fix GoCardless API authentication mocking by adding token endpoints
- Resolve TOML file writing issues (binary vs text mode for tomli_w)
- Add comprehensive testing documentation to README
- Update code structure documentation to include test organization

Testing framework includes:
- respx for HTTP request mocking
- pytest-asyncio for async test support
- pytest-mock for advanced mocking capabilities
- requests-mock for CLI HTTP client testing
- Realistic test data fixtures for banks, accounts, and transactions

🤖 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-02 00:34:54 +01:00
committed by Elisiário Couto
parent 4018b263f2
commit 34e793c75c
11 changed files with 1248 additions and 1 deletions

View File

@@ -0,0 +1,154 @@
"""Tests for banks API endpoints."""
import pytest
import respx
import httpx
from unittest.mock import patch
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):
"""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"})
)
# 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):
response = api_client.get("/api/v1/banks/institutions?country=PT")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert len(data["data"]) == 2
assert data["data"][0]["id"] == "REVOLUT_REVOLT21"
assert data["data"][1]["id"] == "BANCOBPI_BBPIPTPL"
@respx.mock
def test_get_institutions_invalid_country(self, api_client, mock_config):
"""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"})
)
# 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):
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]
@respx.mock
def test_connect_to_bank_success(self, api_client, mock_config, mock_auth_token):
"""Test successful bank connection creation."""
requisition_data = {
"id": "req-123",
"institution_id": "REVOLUT_REVOLT21",
"status": "CR",
"created": "2025-09-02T00:00:00Z",
"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"})
)
# 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/"
}
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
assert data["data"]["id"] == "req-123"
assert data["data"]["institution_id"] == "REVOLUT_REVOLT21"
@respx.mock
def test_get_bank_status_success(self, api_client, mock_config, mock_auth_token):
"""Test successful retrieval of bank connection status."""
requisitions_data = {
"results": [
{
"id": "req-123",
"institution_id": "REVOLUT_REVOLT21",
"status": "LN",
"created": "2025-09-02T00:00:00Z",
"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"})
)
# 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):
response = api_client.get("/api/v1/banks/status")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert len(data["data"]) == 1
assert data["data"][0]["bank_id"] == "REVOLUT_REVOLT21"
assert data["data"][0]["status_display"] == "LINKED"
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
assert "GB" in country_codes
assert "DE" in country_codes
@respx.mock
def test_authentication_failure(self, api_client, mock_config):
"""Test handling of authentication failures."""
# Mock token creation failure
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):
response = api_client.get("/api/v1/banks/institutions")
assert response.status_code == 500
data = response.json()
assert "Failed to get institutions" in data["detail"]