Files
AI-Trader/tests/integration/test_api_endpoints.py
Bill 61baf3f90f test: fix remaining integration test for new results endpoint
Update test_results_filters_by_job_id to expect 404 when no data exists,
aligning with the new endpoint behavior where queries with no matching
data return 404 instead of 200 with empty results.

Also add design and implementation plan documents for reference.
2025-11-07 19:46:49 -05:00

420 lines
14 KiB
Python

"""
Integration tests for FastAPI endpoints.
Coverage target: 90%+
Tests verify:
- POST /simulate/trigger: Job creation and trigger
- GET /simulate/status/{job_id}: Job status retrieval
- GET /results: Results querying with filters
- GET /health: Health check endpoint
- Error handling and validation
"""
import pytest
from fastapi.testclient import TestClient
from pathlib import Path
import json
@pytest.fixture
def api_client(clean_db, tmp_path):
"""Create FastAPI test client with clean database."""
from api.main import create_app
# Create test config
test_config = tmp_path / "test_config.json"
test_config.write_text(json.dumps({
"agent_type": "BaseAgent",
"date_range": {"init_date": "2025-01-16", "end_date": "2025-01-17"},
"models": [
{"name": "Test Model", "basemodel": "gpt-4", "signature": "gpt-4", "enabled": True}
],
"agent_config": {"max_steps": 30, "initial_cash": 10000.0},
"log_config": {"log_path": "./data/agent_data"}
}))
app = create_app(db_path=clean_db)
# Enable test mode to prevent background worker from starting
app.state.test_mode = True
client = TestClient(app)
client.test_config_path = str(test_config)
client.db_path = clean_db
return client
@pytest.mark.integration
class TestSimulateTriggerEndpoint:
"""Test POST /simulate/trigger endpoint."""
def test_trigger_creates_job(self, api_client):
"""Should create job and return job_id."""
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-17",
"models": ["gpt-4"]
})
assert response.status_code == 200
data = response.json()
assert "job_id" in data
assert data["status"] == "pending"
assert data["total_model_days"] == 2
def test_trigger_single_date(self, api_client):
"""Should create job for single date."""
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-16",
"models": ["gpt-4"]
})
assert response.status_code == 200
data = response.json()
assert data["total_model_days"] == 1
def test_trigger_resume_mode_cold_start(self, api_client):
"""Should use end_date as single day when no existing data (cold start)."""
response = api_client.post("/simulate/trigger", json={
"start_date": None,
"end_date": "2025-01-16",
"models": ["gpt-4"]
})
assert response.status_code == 200
data = response.json()
assert data["total_model_days"] == 1
assert "resume mode" in data["message"]
def test_trigger_requires_end_date(self, api_client):
"""Should reject request with missing end_date."""
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "",
"models": ["gpt-4"]
})
assert response.status_code == 422
assert "end_date" in str(response.json()["detail"]).lower()
def test_trigger_rejects_null_end_date(self, api_client):
"""Should reject request with null end_date."""
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": None,
"models": ["gpt-4"]
})
assert response.status_code == 422
def test_trigger_validates_models(self, api_client):
"""Should use enabled models from config when models not specified."""
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-16"
# models not specified - should use enabled models from config
})
assert response.status_code == 200
data = response.json()
assert data["total_model_days"] >= 1
def test_trigger_empty_models_uses_config(self, api_client):
"""Should use enabled models from config when models is empty list."""
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-16",
"models": [] # Empty list - should use enabled models from config
})
assert response.status_code == 200
data = response.json()
assert data["total_model_days"] >= 1
def test_trigger_enforces_single_job_limit(self, api_client):
"""Should reject trigger when job already running."""
# Create first job
api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-16",
"models": ["gpt-4"]
})
# Try to create second job
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-17",
"end_date": "2025-01-17",
"models": ["gpt-4"]
})
assert response.status_code == 400
assert "already running" in response.json()["detail"].lower()
def test_trigger_idempotent_behavior(self, api_client):
"""Should skip already completed dates when replace_existing=false."""
# This test would need a completed job first
# For now, just verify the parameter is accepted
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-16",
"models": ["gpt-4"],
"replace_existing": False
})
assert response.status_code == 200
def test_trigger_replace_existing_flag(self, api_client):
"""Should accept replace_existing flag."""
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-16",
"models": ["gpt-4"],
"replace_existing": True
})
assert response.status_code == 200
@pytest.mark.integration
class TestSimulateStatusEndpoint:
"""Test GET /simulate/status/{job_id} endpoint."""
def test_status_returns_job_info(self, api_client):
"""Should return job status and progress."""
# Create job
create_response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-16",
"models": ["gpt-4"]
})
job_id = create_response.json()["job_id"]
# Get status
response = api_client.get(f"/simulate/status/{job_id}")
assert response.status_code == 200
data = response.json()
assert data["job_id"] == job_id
assert data["status"] == "pending"
assert "progress" in data
assert data["progress"]["total_model_days"] == 1
def test_status_returns_404_for_nonexistent_job(self, api_client):
"""Should return 404 for unknown job_id."""
response = api_client.get("/simulate/status/nonexistent-job-id")
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
def test_status_includes_model_day_details(self, api_client):
"""Should include model-day execution details."""
# Create job
create_response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-17",
"models": ["gpt-4"]
})
job_id = create_response.json()["job_id"]
# Get status
response = api_client.get(f"/simulate/status/{job_id}")
assert response.status_code == 200
data = response.json()
assert "details" in data
assert len(data["details"]) == 2 # 2 dates
assert all("date" in detail for detail in data["details"])
assert all("model" in detail for detail in data["details"])
assert all("status" in detail for detail in data["details"])
@pytest.mark.integration
class TestResultsEndpoint:
"""Test GET /results endpoint."""
def test_results_returns_404_when_no_data(self, api_client):
"""Should return 404 when no data exists for default date range."""
response = api_client.get("/results")
# With new endpoint, no data returns 404
assert response.status_code == 404
assert "No trading data found" in response.json()["detail"]
def test_results_filters_by_job_id(self, api_client):
"""Should filter results by job_id."""
# Create job
create_response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16",
"end_date": "2025-01-16",
"models": ["gpt-4"]
})
job_id = create_response.json()["job_id"]
# Query results - no data exists yet, should return 404
response = api_client.get(f"/results?job_id={job_id}")
# No data exists, should return 404
assert response.status_code == 404
def test_results_filters_by_date(self, api_client):
"""Should filter results by date."""
response = api_client.get("/results?start_date=2025-01-16&end_date=2025-01-16")
# No data exists, should return 404
assert response.status_code == 404
def test_results_filters_by_model(self, api_client):
"""Should filter results by model."""
response = api_client.get("/results?model=gpt-4")
# No data exists, should return 404
assert response.status_code == 404
def test_results_combines_multiple_filters(self, api_client):
"""Should support multiple filter parameters."""
response = api_client.get("/results?start_date=2025-01-16&end_date=2025-01-16&model=gpt-4")
# No data exists, should return 404
assert response.status_code == 404
def test_results_includes_position_data(self, api_client):
"""Should include position and holdings data."""
# This test will pass once we have actual data
response = api_client.get("/results")
# No data exists, should return 404
assert response.status_code == 404
@pytest.mark.integration
class TestHealthEndpoint:
"""Test GET /health endpoint."""
def test_health_returns_ok(self, api_client):
"""Should return healthy status."""
response = api_client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "healthy"
def test_health_includes_database_check(self, api_client):
"""Should verify database connectivity."""
response = api_client.get("/health")
assert response.status_code == 200
data = response.json()
assert "database" in data
assert data["database"] == "connected"
def test_health_includes_system_info(self, api_client):
"""Should include system information."""
response = api_client.get("/health")
assert response.status_code == 200
data = response.json()
assert "version" in data or "timestamp" in data
@pytest.mark.integration
class TestErrorHandling:
"""Test error handling across endpoints."""
def test_invalid_json_returns_422(self, api_client):
"""Should handle malformed JSON."""
response = api_client.post(
"/simulate/trigger",
data="invalid json",
headers={"Content-Type": "application/json"}
)
assert response.status_code == 422
def test_missing_required_fields_returns_422(self, api_client):
"""Should validate required fields."""
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-01-16"
# Missing end_date
})
assert response.status_code == 422
def test_invalid_job_id_format_returns_404(self, api_client):
"""Should handle invalid job_id format gracefully."""
response = api_client.get("/simulate/status/invalid-format")
assert response.status_code == 404
@pytest.mark.integration
class TestAsyncDownload:
"""Test async price download behavior."""
def test_trigger_endpoint_fast_response(self, api_client):
"""Test that /simulate/trigger responds quickly without downloading data."""
import time
start_time = time.time()
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-10-01",
"end_date": "2025-10-01",
"models": ["gpt-4"]
})
elapsed = time.time() - start_time
# Should respond in less than 2 seconds (allowing for DB operations)
assert elapsed < 2.0
assert response.status_code == 200
assert "job_id" in response.json()
def test_trigger_endpoint_no_price_download(self, api_client):
"""Test that endpoint doesn't import or use PriceDataManager."""
import api.main
# Verify PriceDataManager is not imported in api.main
assert not hasattr(api.main, 'PriceDataManager'), \
"PriceDataManager should not be imported in api.main"
# Endpoint should still create job successfully
response = api_client.post("/simulate/trigger", json={
"start_date": "2025-10-01",
"end_date": "2025-10-01",
"models": ["gpt-4"]
})
assert response.status_code == 200
assert "job_id" in response.json()
def test_status_endpoint_returns_warnings(self, api_client):
"""Test that /simulate/status returns warnings field."""
from api.database import initialize_database
from api.job_manager import JobManager
# Create job with warnings
db_path = api_client.db_path
job_manager = JobManager(db_path=db_path)
job_result = job_manager.create_job(
config_path="config.json",
date_range=["2025-10-01"],
models=["gpt-5"]
)
job_id = job_result["job_id"]
# Add warnings
warnings = ["Rate limited", "Skipped 1 date"]
job_manager.add_job_warnings(job_id, warnings)
# Get status
response = api_client.get(f"/simulate/status/{job_id}")
assert response.status_code == 200
data = response.json()
assert "warnings" in data
assert data["warnings"] == warnings
# Coverage target: 90%+ for api/main.py