mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-01 17:17:24 -04:00
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.
420 lines
14 KiB
Python
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
|