""" Rebalancing API integration tests """ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session @pytest.mark.integration class TestRebalancingAPI: """Rebalancing API endpoint tests""" def test_calculate_rebalancing_success( self, client: TestClient, sample_portfolio, sample_assets ): """Test successful rebalancing calculation""" request_data = { "portfolio_id": str(sample_portfolio.id), "current_holdings": [ {"ticker": "005930", "quantity": 100}, {"ticker": "000660", "quantity": 50}, {"ticker": "035420", "quantity": 30}, ], "cash": 5000000 } response = client.post("/api/v1/rebalancing/calculate", json=request_data) assert response.status_code == 200 data = response.json() # Check response structure assert "portfolio" in data assert "total_value" in data assert "cash" in data assert "recommendations" in data assert "summary" in data # Check summary summary = data["summary"] assert "buy" in summary assert "sell" in summary assert "hold" in summary # Check recommendations recommendations = data["recommendations"] assert len(recommendations) == 3 for rec in recommendations: assert "ticker" in rec assert "name" in rec assert "current_price" in rec assert "current_quantity" in rec assert "current_value" in rec assert "current_ratio" in rec assert "target_ratio" in rec assert "target_value" in rec assert "delta_value" in rec assert "delta_quantity" in rec assert "action" in rec assert rec["action"] in ["buy", "sell", "hold"] def test_calculate_rebalancing_portfolio_not_found( self, client: TestClient ): """Test rebalancing with non-existent portfolio""" import uuid fake_id = str(uuid.uuid4()) request_data = { "portfolio_id": fake_id, "current_holdings": [], "cash": 1000000 } response = client.post("/api/v1/rebalancing/calculate", json=request_data) assert response.status_code == 404 def test_calculate_rebalancing_no_cash_no_holdings( self, client: TestClient, sample_portfolio ): """Test rebalancing with no cash and no holdings""" request_data = { "portfolio_id": str(sample_portfolio.id), "current_holdings": [ {"ticker": "005930", "quantity": 0}, {"ticker": "000660", "quantity": 0}, {"ticker": "035420", "quantity": 0}, ], "cash": 0 } response = client.post("/api/v1/rebalancing/calculate", json=request_data) # Should handle gracefully if response.status_code == 200: data = response.json() assert data["total_value"] == 0 def test_calculate_rebalancing_only_cash( self, client: TestClient, sample_portfolio, sample_assets ): """Test rebalancing with only cash (no holdings)""" request_data = { "portfolio_id": str(sample_portfolio.id), "current_holdings": [ {"ticker": "005930", "quantity": 0}, {"ticker": "000660", "quantity": 0}, {"ticker": "035420", "quantity": 0}, ], "cash": 10000000 } response = client.post("/api/v1/rebalancing/calculate", json=request_data) assert response.status_code == 200 data = response.json() # All should be buy recommendations recommendations = data["recommendations"] buy_count = sum(1 for r in recommendations if r["action"] == "buy") assert buy_count > 0 def test_calculate_rebalancing_missing_holdings( self, client: TestClient, sample_portfolio ): """Test rebalancing with incomplete holdings list""" request_data = { "portfolio_id": str(sample_portfolio.id), "current_holdings": [ {"ticker": "005930", "quantity": 100}, # Missing other tickers ], "cash": 1000000 } response = client.post("/api/v1/rebalancing/calculate", json=request_data) # Should handle missing tickers (treat as 0 quantity) assert response.status_code == 200 def test_calculate_rebalancing_invalid_ticker( self, client: TestClient, sample_portfolio ): """Test rebalancing with invalid ticker in holdings""" request_data = { "portfolio_id": str(sample_portfolio.id), "current_holdings": [ {"ticker": "999999", "quantity": 100}, ], "cash": 1000000 } response = client.post("/api/v1/rebalancing/calculate", json=request_data) # Should fail validation or ignore invalid ticker assert response.status_code in [200, 400, 404]