blackopsrepl's picture
.
d9f5c15
"""
Tests for REST API endpoints.
Tests that configuration is properly received and applied.
"""
import pytest
from fastapi.testclient import TestClient
from portfolio_optimization.rest_api import app
from portfolio_optimization.domain import (
PortfolioOptimizationPlanModel,
StockSelectionModel,
SolverConfigModel,
)
@pytest.fixture
def client():
"""Create a test client for the FastAPI app."""
return TestClient(app)
class TestDemoDataEndpoints:
"""Tests for demo data endpoints."""
def test_list_demo_data(self, client):
"""GET /demo-data should return available datasets."""
response = client.get("/demo-data")
assert response.status_code == 200
data = response.json()
assert "SMALL" in data
assert "LARGE" in data
def test_get_small_demo_data(self, client):
"""GET /demo-data/SMALL should return 25 stocks."""
response = client.get("/demo-data/SMALL")
assert response.status_code == 200
data = response.json()
assert "stocks" in data
assert len(data["stocks"]) == 25
def test_get_large_demo_data(self, client):
"""GET /demo-data/LARGE should return 51 stocks."""
response = client.get("/demo-data/LARGE")
assert response.status_code == 200
data = response.json()
assert "stocks" in data
assert len(data["stocks"]) == 51
class TestSolverConfigEndpoints:
"""Tests for solver configuration handling."""
def test_plan_model_accepts_solver_config(self):
"""PortfolioOptimizationPlanModel should accept solverConfig."""
model = PortfolioOptimizationPlanModel(
stocks=[
StockSelectionModel(
stockId="AAPL",
stockName="Apple",
sector="Technology",
predictedReturn=0.12,
selected=None
)
],
targetPositionCount=20,
maxSectorPercentage=0.25,
solverConfig=SolverConfigModel(terminationSeconds=60)
)
assert model.solver_config is not None
assert model.solver_config.termination_seconds == 60
def test_plan_model_serializes_solver_config(self):
"""solverConfig should serialize with camelCase aliases."""
model = PortfolioOptimizationPlanModel(
stocks=[],
solverConfig=SolverConfigModel(terminationSeconds=90)
)
data = model.model_dump(by_alias=True)
assert "solverConfig" in data
assert data["solverConfig"]["terminationSeconds"] == 90
def test_plan_model_deserializes_solver_config(self):
"""solverConfig should deserialize from JSON."""
json_data = {
"stocks": [
{
"stockId": "AAPL",
"stockName": "Apple",
"sector": "Technology",
"predictedReturn": 0.12,
"selected": None
}
],
"targetPositionCount": 15,
"maxSectorPercentage": 0.30,
"solverConfig": {
"terminationSeconds": 120
}
}
model = PortfolioOptimizationPlanModel.model_validate(json_data)
assert model.target_position_count == 15
assert model.max_sector_percentage == 0.30
assert model.solver_config is not None
assert model.solver_config.termination_seconds == 120
def test_plan_without_solver_config(self):
"""Plan should work without solverConfig (uses defaults)."""
json_data = {
"stocks": [],
"targetPositionCount": 20,
"maxSectorPercentage": 0.25
}
model = PortfolioOptimizationPlanModel.model_validate(json_data)
assert model.solver_config is None # None is OK, will use default 30s
def test_post_portfolio_with_solver_config(self, client):
"""POST /portfolios should accept solverConfig in request body."""
# First get demo data
demo_response = client.get("/demo-data/SMALL")
plan_data = demo_response.json()
# Add solver config
plan_data["solverConfig"] = {
"terminationSeconds": 10 # Use short time for test
}
# Submit for solving
response = client.post("/portfolios", json=plan_data)
assert response.status_code == 200
job_id = response.json()
assert job_id is not None
assert len(job_id) > 0
# Stop solving immediately (we just want to verify config was accepted)
stop_response = client.delete(f"/portfolios/{job_id}")
assert stop_response.status_code == 200