quantum-hybrid-portfolio / scripts /test_api_integration.py
rocRevyAreGoals15's picture
Initial HF Space deployment
9e89154
Raw
History Blame Contribute Delete
20 kB
#!/usr/bin/env python3
"""
Comprehensive API Integration Test Suite
Tests all API endpoints with full dependency verification.
Run this after installing all dependencies:
pip install -r requirements.txt
Usage:
python scripts/test_api_integration.py [--base-url http://localhost:5000]
"""
import os
import sys
import time
import json
import logging
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field
# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
import requests
except ImportError:
print("❌ requests library not installed. Run: pip install requests")
sys.exit(1)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@dataclass
class TestResult:
"""Result of a single test."""
name: str
passed: bool = False
status_code: Optional[int] = None
error: Optional[str] = None
duration_ms: float = 0.0
details: Dict[str, Any] = field(default_factory=dict)
@dataclass
class TestReport:
"""Overall test report."""
total: int = 0
passed: int = 0
failed: int = 0
skipped: int = 0
results: List[TestResult] = field(default_factory=list)
def add_result(self, result: TestResult):
self.total += 1
if result.passed:
self.passed += 1
elif result.error and "SKIPPED" in result.error:
self.skipped += 1
else:
self.failed += 1
self.results.append(result)
def print_summary(self):
print("\n" + "=" * 60)
print("TEST SUMMARY")
print("=" * 60)
print(f"Total: {self.total}")
print(f"✅ Passed: {self.passed}")
print(f"❌ Failed: {self.failed}")
print(f"⏭️ Skipped: {self.skipped}")
print("=" * 60)
if self.failed > 0:
print("\nFailed tests:")
for result in self.results:
if not result.passed and result.error and "SKIPPED" not in result.error:
print(f" ❌ {result.name}: {result.error}")
print(f"\nSuccess rate: {self.passed/self.total*100:.1f}%" if self.total > 0 else "No tests run")
class APIIntegrationTester:
"""Comprehensive API integration tester."""
def __init__(self, base_url: str, api_key: Optional[str] = None):
self.base_url = base_url.rstrip('/')
self.api_key = api_key or os.getenv('API_KEY', '')
self.session = requests.Session()
self.session.headers.update({
'Content-Type': 'application/json',
'X-API-Key': self.api_key,
})
self.report = TestReport()
def _make_request(
self,
method: str,
endpoint: str,
data: Optional[Dict] = None,
timeout: int = 30
) -> requests.Response:
"""Make HTTP request with error handling."""
url = f"{self.base_url}{endpoint}"
start_time = time.time()
try:
if method.upper() == 'GET':
response = self.session.get(url, params=data, timeout=timeout)
elif method.upper() == 'POST':
response = self.session.post(url, json=data, timeout=timeout)
else:
raise ValueError(f"Unsupported method: {method}")
duration_ms = (time.time() - start_time) * 1000
return response
except requests.exceptions.Timeout:
duration_ms = (time.time() - start_time) * 1000
raise TimeoutError(f"Request timed out after {timeout}s")
except requests.exceptions.ConnectionError as e:
duration_ms = (time.time() - start_time) * 1000
raise ConnectionError(f"Connection failed: {e}")
def test_health_check(self) -> TestResult:
"""Test health check endpoint."""
result = TestResult(name="Health Check")
try:
response = self._make_request('GET', '/api/health')
result.status_code = response.status_code
result.duration_ms = response.elapsed.total_seconds() * 1000
if response.status_code == 200:
data = response.json()
result.passed = data.get('data', {}).get('status') in ['healthy', 'degraded']
result.details = data.get('data', {})
if not result.passed:
result.error = f"Unhealthy status: {data.get('data', {}).get('status')}"
else:
result.passed = False
result.error = f"Unexpected status code: {response.status_code}"
except Exception as e:
result.passed = False
result.error = str(e)
return result
def test_config_endpoints(self) -> List[TestResult]:
"""Test configuration endpoints."""
results = []
# Test objectives config
result = TestResult(name="Config: Objectives")
try:
response = self._make_request('GET', '/api/config/objectives')
result.status_code = response.status_code
result.duration_ms = response.elapsed.total_seconds() * 1000
if response.status_code == 200:
data = response.json()
result.passed = 'data' in data and len(data['data']) > 0
if not result.passed:
result.error = "Empty or invalid response"
else:
result.passed = False
result.error = f"Status code: {response.status_code}"
except Exception as e:
result.passed = False
result.error = str(e)
results.append(result)
# Test presets config
result = TestResult(name="Config: Presets")
try:
response = self._make_request('GET', '/api/config/presets')
result.status_code = response.status_code
result.duration_ms = response.elapsed.total_seconds() * 1000
if response.status_code == 200:
data = response.json()
result.passed = 'data' in data and len(data['data']) > 0
if not result.passed:
result.error = "Empty or invalid response"
else:
result.passed = False
result.error = f"Status code: {response.status_code}"
except Exception as e:
result.passed = False
result.error = str(e)
results.append(result)
# IBM Runtime workloads (requires API key; may be 400 if no IBM token)
result = TestResult(name="Config: IBM Quantum workloads")
try:
if not self.api_key:
result.passed = True
result.error = "SKIPPED: no API_KEY for protected route"
else:
response = self._make_request(
"GET",
"/api/config/ibm-quantum/workloads",
data={"limit": 5},
)
result.status_code = response.status_code
result.duration_ms = response.elapsed.total_seconds() * 1000
if response.status_code == 200:
data = response.json()
result.passed = "data" in data and "workloads" in data.get("data", {})
elif response.status_code in (400, 502, 503):
result.passed = True
result.details["note"] = "no IBM token or IBM SDK unavailable"
else:
result.passed = False
result.error = f"Unexpected status: {response.status_code}"
except Exception as e:
result.passed = False
result.error = str(e)
results.append(result)
return results
def test_market_data(self) -> TestResult:
"""Test market data endpoint."""
result = TestResult(name="Market Data Fetch")
try:
# Test with a small set of tickers
response = self._make_request(
'POST',
'/api/market-data',
data={'tickers': ['AAPL', 'MSFT', 'GOOGL']},
timeout=60
)
result.status_code = response.status_code
result.duration_ms = response.elapsed.total_seconds() * 1000
if response.status_code == 200:
data = response.json()
result.passed = 'data' in data
if result.passed:
result.details['tickers_count'] = len(data.get('data', {}).get('prices', {}))
else:
result.passed = False
result.error = f"Status code: {response.status_code}"
except TimeoutError:
result.passed = False
result.error = "TIMEOUT: Request took longer than 60s (SKIPPED - network issue)"
except Exception as e:
result.passed = False
result.error = str(e)
return result
def test_portfolio_optimization(self) -> List[TestResult]:
"""Test portfolio optimization endpoints."""
results = []
# Test with mock data
optimization_tests = [
('markowitz', 'Max Sharpe (Markowitz)'),
('hrp', 'Hierarchical Risk Parity'),
('qubo_sa', 'QUBO + Simulated Annealing'),
('hybrid', 'Hybrid Pipeline'),
]
for objective, test_name in optimization_tests:
result = TestResult(name=f"Optimization: {test_name}")
try:
response = self._make_request(
'POST',
'/api/portfolio/optimize',
data={
'tickers': ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META'],
'objective': objective,
'weight_min': 0.05,
'weight_max': 0.50,
},
timeout=120
)
result.status_code = response.status_code
result.duration_ms = response.elapsed.total_seconds() * 1000
if response.status_code == 200:
data = response.json()
result.passed = 'data' in data
if result.passed:
result.details['sharpe_ratio'] = data.get('data', {}).get('sharpe_ratio')
result.details['execution_time_ms'] = data.get('meta', {}).get('duration_ms')
else:
result.passed = False
result.error = f"Status code: {response.status_code}"
except TimeoutError:
result.passed = False
result.error = f"TIMEOUT: {objective} took longer than 120s (SKIPPED - slow computation)"
except Exception as e:
result.passed = False
result.error = str(e)
results.append(result)
return results
def test_backtest(self) -> TestResult:
"""Test backtesting endpoint."""
result = TestResult(name="Backtest")
try:
response = self._make_request(
'POST',
'/api/portfolio/backtest',
data={
'tickers': ['AAPL', 'MSFT', 'GOOGL'],
'start_date': '2023-01-01',
'end_date': '2023-12-31',
'objective': 'max_sharpe',
'rebalance_frequency': 'monthly',
},
timeout=120
)
result.status_code = response.status_code
result.duration_ms = response.elapsed.total_seconds() * 1000
if response.status_code == 200:
data = response.json()
result.passed = 'data' in data
if result.passed:
result.details['results_count'] = len(data.get('data', {}).get('results', []))
result.details['summary_metrics'] = data.get('data', {}).get('summary_metrics', {})
else:
result.passed = False
result.error = f"Status code: {response.status_code}"
except TimeoutError:
result.passed = False
result.error = "TIMEOUT: Backtest took longer than 120s (SKIPPED - slow computation)"
except Exception as e:
result.passed = False
result.error = str(e)
return result
def test_efficient_frontier(self) -> TestResult:
"""Test efficient frontier endpoint."""
result = TestResult(name="Efficient Frontier")
try:
response = self._make_request(
'POST',
'/api/portfolio/efficient-frontier',
data={
'tickers': ['AAPL', 'MSFT', 'GOOGL', 'AMZN'],
},
timeout=60
)
result.status_code = response.status_code
result.duration_ms = response.elapsed.total_seconds() * 1000
if response.status_code == 200:
data = response.json()
result.passed = 'data' in data and 'frontier_points' in data.get('data', {})
if result.passed:
result.details['frontier_points'] = len(data.get('data', {}).get('frontier_points', []))
else:
result.passed = False
result.error = f"Status code: {response.status_code}"
except Exception as e:
result.passed = False
result.error = str(e)
return result
def test_metrics_endpoint(self) -> TestResult:
"""Test Prometheus metrics endpoint."""
result = TestResult(name="Prometheus Metrics")
try:
response = self._make_request('GET', '/metrics')
result.status_code = response.status_code
result.duration_ms = response.elapsed.total_seconds() * 1000
if response.status_code == 200:
# Check if response contains Prometheus format
result.passed = 'http_requests_total' in response.text or 'python_info' in response.text
if not result.passed:
result.error = "Response doesn't contain expected Prometheus metrics"
else:
result.passed = False
result.error = f"Status code: {response.status_code}"
except Exception as e:
result.passed = False
result.error = str(e)
return result
def run_all_tests(self) -> TestReport:
"""Run all integration tests."""
logger.info("Starting API integration tests...")
logger.info(f"Base URL: {self.base_url}")
# Test 1: Health check
logger.info("Testing health check...")
self.report.add_result(self.test_health_check())
# Test 2: Config endpoints
logger.info("Testing config endpoints...")
for result in self.test_config_endpoints():
self.report.add_result(result)
# Test 3: Market data
logger.info("Testing market data...")
self.report.add_result(self.test_market_data())
# Test 4: Portfolio optimization
logger.info("Testing portfolio optimization...")
for result in self.test_portfolio_optimization():
self.report.add_result(result)
# Test 5: Backtest
logger.info("Testing backtest...")
self.report.add_result(self.test_backtest())
# Test 6: Efficient frontier
logger.info("Testing efficient frontier...")
self.report.add_result(self.test_efficient_frontier())
# Test 7: Metrics
logger.info("Testing metrics endpoint...")
self.report.add_result(self.test_metrics_endpoint())
return self.report
def check_dependencies() -> Dict[str, bool]:
"""Check if all required dependencies are installed."""
deps = {
'requests': False,
'numpy': False,
'pandas': False,
'flask': False,
'scipy': False,
'sklearn': False,
'braket': False,
}
import importlib
for dep in deps:
try:
if dep == 'sklearn':
importlib.import_module('sklearn')
else:
importlib.import_module(dep)
deps[dep] = True
except ImportError:
pass
return deps
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(description='API Integration Test Suite')
parser.add_argument(
'--base-url',
default='http://localhost:5000',
help='Base URL of the API (default: http://localhost:5000)'
)
parser.add_argument(
'--api-key',
default='',
help='API key for authentication'
)
parser.add_argument(
'--check-deps-only',
action='store_true',
help='Only check dependencies, don\'t run tests'
)
args = parser.parse_args()
print("=" * 60)
print("QUANTUM HYBRID PORTFOLIO - API INTEGRATION TESTS")
print("=" * 60)
# Check dependencies
print("\n📦 Checking dependencies...")
deps = check_dependencies()
all_deps_installed = True
for dep, installed in deps.items():
status = "✅" if installed else "❌"
print(f" {status} {dep}")
if not installed and dep in ['requests', 'numpy', 'pandas', 'flask']:
all_deps_installed = False
if args.check_deps_only:
return 0 if all_deps_installed else 1
if not all_deps_installed:
print("\n⚠️ Some critical dependencies are missing.")
print(" Run: pip install -r requirements.txt")
return 1
print("\n" + "=" * 60)
# Run tests
tester = APIIntegrationTester(
base_url=args.base_url,
api_key=args.api_key
)
try:
report = tester.run_all_tests()
report.print_summary()
# Save detailed report
report_path = 'test_report.json'
with open(report_path, 'w') as f:
json.dump({
'total': report.total,
'passed': report.passed,
'failed': report.failed,
'skipped': report.skipped,
'results': [
{
'name': r.name,
'passed': r.passed,
'status_code': r.status_code,
'error': r.error,
'duration_ms': r.duration_ms,
'details': r.details,
}
for r in report.results
],
}, f, indent=2)
print(f"\n📄 Detailed report saved to: {report_path}")
return 0 if report.failed == 0 else 1
except ConnectionError as e:
print(f"\n❌ Failed to connect to API at {args.base_url}")
print(f" Error: {e}")
print("\n Make sure the API server is running:")
print(" python api.py")
return 1
except Exception as e:
print(f"\n❌ Unexpected error: {e}")
import traceback
traceback.print_exc()
return 1
if __name__ == '__main__':
sys.exit(main())