import pytest from unittest.mock import patch, MagicMock from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.main import app from app.core.database import get_db, Base from app.services.currency_service import currency_service # Test database setup SQLALCHEMY_DATABASE_URL = "sqlite:///./test_currency.db" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) def override_get_db(): try: db = TestingSessionLocal() yield db finally: db.close() app.dependency_overrides[get_db] = override_get_db client = TestClient(app) def setup_database(): """Setup test database""" Base.metadata.create_all(bind=engine) def cleanup_database(): """Clean up test database""" Base.metadata.drop_all(bind=engine) class TestCurrencyService: """Test suite for Currency Service""" def setup_method(self): """Setup for each test method""" # Clear cache before each test currency_service.cache = {} currency_service.cache_expiry = {} def test_same_currency_conversion(self): """Test conversion between same currency returns 1.0""" rate = currency_service.get_exchange_rate("USD", "USD") assert rate == 1.0 rate = currency_service.get_exchange_rate("EUR", "EUR") assert rate == 1.0 def test_fallback_rates(self): """Test fallback exchange rates are used when API fails""" # Test USD to EUR conversion using fallback rates rate = currency_service.get_exchange_rate("USD", "EUR") expected_rate = currency_service.fallback_rates["USD"] / currency_service.fallback_rates["EUR"] assert rate == expected_rate @patch('app.services.currency_service.requests.get') def test_api_rate_fetching(self, mock_get): """Test successful API rate fetching""" # Mock successful API response mock_response = MagicMock() mock_response.json.return_value = { "conversion_rates": { "EUR": 0.85, "GBP": 0.73 } } mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response # Test USD to EUR conversion rate = currency_service.get_exchange_rate("USD", "EUR") assert rate == 0.85 # Test USD to GBP conversion rate = currency_service.get_exchange_rate("USD", "GBP") assert rate == 0.73 @patch('app.services.currency_service.requests.get') def test_api_failure_fallback(self, mock_get): """Test fallback to static rates when API fails""" # Mock API failure mock_get.side_effect = Exception("API Error") # Test conversion falls back to static rates rate = currency_service.get_exchange_rate("USD", "EUR") expected_rate = currency_service.fallback_rates["USD"] / currency_service.fallback_rates["EUR"] assert rate == expected_rate def test_caching_mechanism(self): """Test that exchange rates are cached""" # Mock the API to track calls with patch('app.services.currency_service.requests.get') as mock_get: mock_response = MagicMock() mock_response.json.return_value = {"conversion_rates": {"EUR": 0.85}} mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response # First call should hit API rate1 = currency_service.get_exchange_rate("USD", "EUR") assert mock_get.call_count == 1 assert rate1 == 0.85 # Second call should use cache rate2 = currency_service.get_exchange_rate("USD", "EUR") assert mock_get.call_count == 1 # Should not increase assert rate2 == 0.85 def test_reverse_rate_calculation(self): """Test calculation of reverse rates from cached rates""" # Mock API to cache USD to EUR rate with patch('app.services.currency_service.requests.get') as mock_get: mock_response = MagicMock() mock_response.json.return_value = {"conversion_rates": {"EUR": 0.85}} mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response # Cache USD to EUR rate currency_service.get_exchange_rate("USD", "EUR") # Clear the mock to test reverse calculation mock_get.reset_mock() # Test EUR to USD uses reverse calculation rate = currency_service.get_exchange_rate("EUR", "USD") assert mock_get.call_count == 0 # Should not hit API assert rate == 1.0 / 0.85 # Reverse of cached rate def test_amount_conversion(self): """Test amount conversion between currencies""" # Test with USD to EUR at 0.85 rate with patch.object(currency_service, 'get_exchange_rate', return_value=0.85): converted_amount = currency_service.convert_amount(100.0, "USD", "EUR") assert converted_amount == 85.0 # Test with zero amount converted_amount = currency_service.convert_amount(0.0, "USD", "EUR") assert converted_amount == 0.0 # Test with negative amount converted_amount = currency_service.convert_amount(-50.0, "USD", "EUR") assert converted_amount == 0.0 def test_currency_formatting(self): """Test currency formatting with symbols""" # Test USD formatting formatted = currency_service.format_currency(123.45, "USD") assert formatted == "$123.45" # Test EUR formatting formatted = currency_service.format_currency(99.99, "EUR") assert formatted == "€99.99" # Test JPY formatting (no decimal places) formatted = currency_service.format_currency(1234, "JPY") assert formatted == "¥1234" # Test CNY formatting (no decimal places) formatted = currency_service.format_currency(5678, "CNY") assert formatted == "¥5678" def test_supported_currencies(self): """Test getting supported currencies list""" currencies = currency_service.get_supported_currencies() assert "USD" in currencies assert "EUR" in currencies assert "GBP" in currencies assert "JPY" in currencies assert currencies["USD"] == "US Dollar" assert currencies["EUR"] == "Euro" class TestCurrencyAPI: """Test suite for Currency API endpoints""" @pytest.fixture(autouse=True) def setup(self): """Setup for each test""" setup_database() yield cleanup_database() @patch('app.services.currency_service.requests.get') def test_currency_conversion_endpoint(self, mock_get): """Test currency conversion API endpoint""" # Mock API response mock_response = MagicMock() mock_response.json.return_value = { "conversion_rates": { "EUR": 0.85, "GBP": 0.73 } } mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response # Test conversion endpoint response = client.get("/api/currency/convert?amount=100&from_currency=USD&to_currency=EUR") assert response.status_code == 200 data = response.json() assert data["amount"] == 100.0 assert data["from_currency"] == "USD" assert data["to_currency"] == "EUR" assert data["exchange_rate"] == 0.85 assert data["converted_amount"] == 85.0 @patch('app.services.currency_service.requests.get') def test_batch_conversion_endpoint(self, mock_get): """Test batch currency conversion API endpoint""" # Mock API response mock_response = MagicMock() mock_response.json.return_value = { "conversion_rates": { "EUR": 0.85, "GBP": 0.73 } } mock_response.raise_for_status.return_value = None mock_get.return_value = mock_response # Test batch conversion conversion_data = { "amount": 100.0, "from_currency": "USD", "to_currencies": ["EUR", "GBP", "JPY"] } response = client.post("/api/currency/convert/batch", json=conversion_data) assert response.status_code == 200 data = response.json() assert data["amount"] == 100.0 assert data["from_currency"] == "USD" assert len(data["conversions"]) == 3 # Check individual conversions conversions = {conv["currency"]: conv["converted_amount"] for conv in data["conversions"]} assert conversions["EUR"] == 85.0 assert conversions["GBP"] == 73.0 # JPY should use fallback rate expected_jpy = currency_service.fallback_rates["USD"] / currency_service.fallback_rates["JPY"] * 100 assert conversions["JPY"] == expected_jpy def test_supported_currencies_endpoint(self): """Test supported currencies API endpoint""" response = client.get("/api/currency/supported") assert response.status_code == 200 data = response.json() assert "currencies" in data currencies = data["currencies"] assert "USD" in currencies assert "EUR" in currencies assert currencies["USD"] == "US Dollar" def test_currency_formatting_endpoint(self): """Test currency formatting API endpoint""" response = client.get("/api/currency/format?amount=123.45¤cy_code=USD") assert response.status_code == 200 data = response.json() assert data["amount"] == 123.45 assert data["currency_code"] == "USD" assert data["formatted_amount"] == "$123.45" def test_conversion_validation(self): """Test conversion endpoint validation""" # Test missing parameters response = client.get("/api/currency/convert?amount=100&from_currency=USD") assert response.status_code == 422 # Test invalid amount response = client.get("/api/currency/convert?amount=-50&from_currency=USD&to_currency=EUR") assert response.status_code == 400 # Test invalid currency codes response = client.get("/api/currency/convert?amount=100&from_currency=INVALID&to_currency=EUR") assert response.status_code == 400 if __name__ == "__main__": pytest.main([__file__, "-v"])