380 lines
13 KiB
Python
380 lines
13 KiB
Python
|
|
"""Tests for autopilot API endpoints."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from unittest.mock import Mock, patch, MagicMock, AsyncMock
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
|
||
|
|
from backend.main import app
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def client():
|
||
|
|
"""Test client fixture."""
|
||
|
|
return TestClient(app)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_autopilot():
|
||
|
|
"""Mock autopilot instance."""
|
||
|
|
autopilot = Mock()
|
||
|
|
autopilot.symbol = "BTC/USD"
|
||
|
|
autopilot.is_running = False
|
||
|
|
autopilot.get_status.return_value = {
|
||
|
|
"symbol": "BTC/USD",
|
||
|
|
"running": False,
|
||
|
|
"interval": 60.0,
|
||
|
|
"has_market_data": True,
|
||
|
|
"market_data_length": 100,
|
||
|
|
"headlines_count": 5,
|
||
|
|
"last_sentiment_score": 0.5,
|
||
|
|
"last_pattern": "head_and_shoulders",
|
||
|
|
"last_signal": None,
|
||
|
|
}
|
||
|
|
autopilot.last_signal = None
|
||
|
|
autopilot.analyze_once.return_value = None
|
||
|
|
return autopilot
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_intelligent_autopilot():
|
||
|
|
"""Mock intelligent autopilot instance."""
|
||
|
|
autopilot = Mock()
|
||
|
|
autopilot.symbol = "BTC/USD"
|
||
|
|
autopilot.is_running = False
|
||
|
|
autopilot.enable_auto_execution = False
|
||
|
|
autopilot.get_status.return_value = {
|
||
|
|
"symbol": "BTC/USD",
|
||
|
|
"timeframe": "1h",
|
||
|
|
"running": False,
|
||
|
|
"selected_strategy": None,
|
||
|
|
"trades_today": 0,
|
||
|
|
"max_trades_per_day": 10,
|
||
|
|
"min_confidence_threshold": 0.75,
|
||
|
|
"enable_auto_execution": False,
|
||
|
|
"last_analysis": None,
|
||
|
|
"model_info": {},
|
||
|
|
}
|
||
|
|
return autopilot
|
||
|
|
|
||
|
|
|
||
|
|
class TestUnifiedAutopilotEndpoints:
|
||
|
|
"""Tests for unified autopilot endpoints."""
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_autopilot_mode_info')
|
||
|
|
def test_get_modes(self, mock_get_mode_info, client):
|
||
|
|
"""Test getting autopilot mode information."""
|
||
|
|
mock_get_mode_info.return_value = {
|
||
|
|
"modes": {
|
||
|
|
"pattern": {"name": "Pattern-Based Autopilot"},
|
||
|
|
"intelligent": {"name": "ML-Based Autopilot"},
|
||
|
|
},
|
||
|
|
"comparison": {},
|
||
|
|
}
|
||
|
|
|
||
|
|
response = client.get("/api/autopilot/modes")
|
||
|
|
assert response.status_code == 200
|
||
|
|
data = response.json()
|
||
|
|
assert "modes" in data
|
||
|
|
assert "pattern" in data["modes"]
|
||
|
|
assert "intelligent" in data["modes"]
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_autopilot')
|
||
|
|
@patch('backend.api.autopilot.run_autopilot_loop')
|
||
|
|
def test_start_unified_pattern_mode(
|
||
|
|
self, mock_run_loop, mock_get_autopilot, client, mock_autopilot
|
||
|
|
):
|
||
|
|
"""Test starting unified autopilot in pattern mode."""
|
||
|
|
mock_get_autopilot.return_value = mock_autopilot
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/start-unified",
|
||
|
|
json={
|
||
|
|
"symbol": "BTC/USD",
|
||
|
|
"mode": "pattern",
|
||
|
|
"auto_execute": False,
|
||
|
|
"interval": 60.0,
|
||
|
|
"pattern_order": 5,
|
||
|
|
"auto_fetch_news": True,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
data = response.json()
|
||
|
|
assert data["status"] == "started"
|
||
|
|
assert data["mode"] == "pattern"
|
||
|
|
assert data["symbol"] == "BTC/USD"
|
||
|
|
assert data["auto_execute"] is False
|
||
|
|
mock_get_autopilot.assert_called_once()
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_intelligent_autopilot')
|
||
|
|
def test_start_unified_intelligent_mode(
|
||
|
|
self, mock_get_intelligent, client, mock_intelligent_autopilot
|
||
|
|
):
|
||
|
|
"""Test starting unified autopilot in intelligent mode."""
|
||
|
|
mock_get_intelligent.return_value = mock_intelligent_autopilot
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/start-unified",
|
||
|
|
json={
|
||
|
|
"symbol": "BTC/USD",
|
||
|
|
"mode": "intelligent",
|
||
|
|
"auto_execute": True,
|
||
|
|
"exchange_id": 1,
|
||
|
|
"timeframe": "1h",
|
||
|
|
"interval": 60.0,
|
||
|
|
"paper_trading": True,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
data = response.json()
|
||
|
|
assert data["status"] == "started"
|
||
|
|
assert data["mode"] == "intelligent"
|
||
|
|
assert data["symbol"] == "BTC/USD"
|
||
|
|
assert data["auto_execute"] is True
|
||
|
|
assert mock_intelligent_autopilot.enable_auto_execution is True
|
||
|
|
mock_get_intelligent.assert_called_once()
|
||
|
|
|
||
|
|
def test_start_unified_invalid_mode(self, client):
|
||
|
|
"""Test starting unified autopilot with invalid mode."""
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/start-unified",
|
||
|
|
json={
|
||
|
|
"symbol": "BTC/USD",
|
||
|
|
"mode": "invalid_mode",
|
||
|
|
"auto_execute": False,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 400
|
||
|
|
assert "Invalid mode" in response.json()["detail"]
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_autopilot')
|
||
|
|
def test_stop_unified_pattern_mode(
|
||
|
|
self, mock_get_autopilot, client, mock_autopilot
|
||
|
|
):
|
||
|
|
"""Test stopping unified autopilot in pattern mode."""
|
||
|
|
mock_get_autopilot.return_value = mock_autopilot
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/stop-unified?symbol=BTC/USD&mode=pattern"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
data = response.json()
|
||
|
|
assert data["status"] == "stopped"
|
||
|
|
assert data["symbol"] == "BTC/USD"
|
||
|
|
assert data["mode"] == "pattern"
|
||
|
|
mock_autopilot.stop.assert_called_once()
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_intelligent_autopilot')
|
||
|
|
def test_stop_unified_intelligent_mode(
|
||
|
|
self, mock_get_intelligent, client, mock_intelligent_autopilot
|
||
|
|
):
|
||
|
|
"""Test stopping unified autopilot in intelligent mode."""
|
||
|
|
mock_get_intelligent.return_value = mock_intelligent_autopilot
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/stop-unified?symbol=BTC/USD&mode=intelligent&timeframe=1h"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
data = response.json()
|
||
|
|
assert data["status"] == "stopped"
|
||
|
|
assert data["symbol"] == "BTC/USD"
|
||
|
|
assert data["mode"] == "intelligent"
|
||
|
|
mock_intelligent_autopilot.stop.assert_called_once()
|
||
|
|
|
||
|
|
def test_stop_unified_invalid_mode(self, client):
|
||
|
|
"""Test stopping unified autopilot with invalid mode."""
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/stop-unified?symbol=BTC/USD&mode=invalid_mode"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 400
|
||
|
|
assert "Invalid mode" in response.json()["detail"]
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_autopilot')
|
||
|
|
def test_get_unified_status_pattern_mode(
|
||
|
|
self, mock_get_autopilot, client, mock_autopilot
|
||
|
|
):
|
||
|
|
"""Test getting unified autopilot status in pattern mode."""
|
||
|
|
mock_get_autopilot.return_value = mock_autopilot
|
||
|
|
|
||
|
|
response = client.get(
|
||
|
|
"/api/autopilot/status-unified/BTC/USD?mode=pattern"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
data = response.json()
|
||
|
|
assert data["symbol"] == "BTC/USD"
|
||
|
|
assert data["mode"] == "pattern"
|
||
|
|
assert "running" in data
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_intelligent_autopilot')
|
||
|
|
def test_get_unified_status_intelligent_mode(
|
||
|
|
self, mock_get_intelligent, client, mock_intelligent_autopilot
|
||
|
|
):
|
||
|
|
"""Test getting unified autopilot status in intelligent mode."""
|
||
|
|
mock_get_intelligent.return_value = mock_intelligent_autopilot
|
||
|
|
|
||
|
|
response = client.get(
|
||
|
|
"/api/autopilot/status-unified/BTC/USD?mode=intelligent&timeframe=1h"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
data = response.json()
|
||
|
|
assert data["symbol"] == "BTC/USD"
|
||
|
|
assert data["mode"] == "intelligent"
|
||
|
|
assert "running" in data
|
||
|
|
|
||
|
|
def test_get_unified_status_invalid_mode(self, client):
|
||
|
|
"""Test getting unified autopilot status with invalid mode."""
|
||
|
|
response = client.get(
|
||
|
|
"/api/autopilot/status-unified/BTC/USD?mode=invalid_mode"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 400
|
||
|
|
assert "Invalid mode" in response.json()["detail"]
|
||
|
|
|
||
|
|
|
||
|
|
class TestModeSelection:
|
||
|
|
"""Tests for mode selection logic."""
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_autopilot_mode_info')
|
||
|
|
def test_mode_info_structure(self, mock_get_mode_info):
|
||
|
|
"""Test that mode info has correct structure."""
|
||
|
|
from src.autopilot import get_autopilot_mode_info
|
||
|
|
|
||
|
|
mode_info = get_autopilot_mode_info()
|
||
|
|
|
||
|
|
assert "modes" in mode_info
|
||
|
|
assert "pattern" in mode_info["modes"]
|
||
|
|
assert "intelligent" in mode_info["modes"]
|
||
|
|
assert "comparison" in mode_info
|
||
|
|
|
||
|
|
# Check pattern mode structure
|
||
|
|
pattern = mode_info["modes"]["pattern"]
|
||
|
|
assert "name" in pattern
|
||
|
|
assert "description" in pattern
|
||
|
|
assert "how_it_works" in pattern
|
||
|
|
assert "best_for" in pattern
|
||
|
|
assert "tradeoffs" in pattern
|
||
|
|
assert "features" in pattern
|
||
|
|
assert "requirements" in pattern
|
||
|
|
|
||
|
|
# Check intelligent mode structure
|
||
|
|
intelligent = mode_info["modes"]["intelligent"]
|
||
|
|
assert "name" in intelligent
|
||
|
|
assert "description" in intelligent
|
||
|
|
assert "how_it_works" in intelligent
|
||
|
|
assert "best_for" in intelligent
|
||
|
|
assert "tradeoffs" in intelligent
|
||
|
|
assert "features" in intelligent
|
||
|
|
assert "requirements" in intelligent
|
||
|
|
|
||
|
|
# Check comparison structure
|
||
|
|
comparison = mode_info["comparison"]
|
||
|
|
assert "transparency" in comparison
|
||
|
|
assert "adaptability" in comparison
|
||
|
|
assert "setup_time" in comparison
|
||
|
|
assert "resource_usage" in comparison
|
||
|
|
|
||
|
|
|
||
|
|
class TestAutoExecution:
|
||
|
|
"""Tests for auto-execution functionality."""
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_intelligent_autopilot')
|
||
|
|
def test_auto_execute_enabled(
|
||
|
|
self, mock_get_intelligent, client, mock_intelligent_autopilot
|
||
|
|
):
|
||
|
|
"""Test that auto-execute is set when enabled."""
|
||
|
|
mock_get_intelligent.return_value = mock_intelligent_autopilot
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/start-unified",
|
||
|
|
json={
|
||
|
|
"symbol": "BTC/USD",
|
||
|
|
"mode": "intelligent",
|
||
|
|
"auto_execute": True,
|
||
|
|
"exchange_id": 1,
|
||
|
|
"timeframe": "1h",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
assert mock_intelligent_autopilot.enable_auto_execution is True
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_intelligent_autopilot')
|
||
|
|
def test_auto_execute_disabled(
|
||
|
|
self, mock_get_intelligent, client, mock_intelligent_autopilot
|
||
|
|
):
|
||
|
|
"""Test that auto-execute is not set when disabled."""
|
||
|
|
mock_get_intelligent.return_value = mock_intelligent_autopilot
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/start-unified",
|
||
|
|
json={
|
||
|
|
"symbol": "BTC/USD",
|
||
|
|
"mode": "intelligent",
|
||
|
|
"auto_execute": False,
|
||
|
|
"exchange_id": 1,
|
||
|
|
"timeframe": "1h",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
# Note: enable_auto_execution may have a default value, so we check it's not True
|
||
|
|
# The actual behavior depends on the implementation
|
||
|
|
|
||
|
|
|
||
|
|
class TestBackwardCompatibility:
|
||
|
|
"""Tests for backward compatibility with old endpoints."""
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_autopilot')
|
||
|
|
@patch('backend.api.autopilot.run_autopilot_loop')
|
||
|
|
def test_old_start_endpoint_still_works(
|
||
|
|
self, mock_run_loop, mock_get_autopilot, client, mock_autopilot
|
||
|
|
):
|
||
|
|
"""Test that old /start endpoint still works (deprecated but functional)."""
|
||
|
|
mock_get_autopilot.return_value = mock_autopilot
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/start",
|
||
|
|
json={
|
||
|
|
"symbol": "BTC/USD",
|
||
|
|
"interval": 60.0,
|
||
|
|
"pattern_order": 5,
|
||
|
|
"auto_fetch_news": True,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
data = response.json()
|
||
|
|
assert data["status"] == "started"
|
||
|
|
assert data["symbol"] == "BTC/USD"
|
||
|
|
|
||
|
|
@patch('backend.api.autopilot.get_intelligent_autopilot')
|
||
|
|
def test_old_intelligent_start_endpoint_still_works(
|
||
|
|
self, mock_get_intelligent, client, mock_intelligent_autopilot
|
||
|
|
):
|
||
|
|
"""Test that old /intelligent/start endpoint still works (deprecated but functional)."""
|
||
|
|
mock_get_intelligent.return_value = mock_intelligent_autopilot
|
||
|
|
|
||
|
|
response = client.post(
|
||
|
|
"/api/autopilot/intelligent/start",
|
||
|
|
json={
|
||
|
|
"symbol": "BTC/USD",
|
||
|
|
"exchange_id": 1,
|
||
|
|
"timeframe": "1h",
|
||
|
|
"interval": 60.0,
|
||
|
|
"paper_trading": True,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert response.status_code == 200
|
||
|
|
data = response.json()
|
||
|
|
assert data["status"] == "started"
|
||
|
|
assert data["symbol"] == "BTC/USD"
|
||
|
|
|