253 lines
7.7 KiB
Python
253 lines
7.7 KiB
Python
import importlib
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
def _build_app(monkeypatch):
|
|
monkeypatch.setenv("APP_ENV", "test")
|
|
monkeypatch.setenv("DISABLE_STARTUP_TASKS", "1")
|
|
monkeypatch.setenv("DB_HOST", "localhost")
|
|
monkeypatch.setenv("DB_NAME", "trading_db")
|
|
monkeypatch.setenv("DB_USER", "trader")
|
|
monkeypatch.setenv("DB_PASSWORD", "test-password")
|
|
monkeypatch.setenv("CORS_ORIGINS", "http://localhost:3000")
|
|
monkeypatch.setenv("BROKER_TOKEN_KEY", "test-broker-token-key")
|
|
monkeypatch.setenv("RESET_OTP_SECRET", "test-reset-secret")
|
|
|
|
import app.main as app_main
|
|
|
|
importlib.reload(app_main)
|
|
return app_main.create_app()
|
|
|
|
|
|
def test_disconnect_route_stops_live_strategy_and_disconnects_broker(monkeypatch):
|
|
app = _build_app(monkeypatch)
|
|
client = TestClient(app)
|
|
|
|
import app.routers.broker as broker_router
|
|
|
|
calls = {
|
|
"disconnect": [],
|
|
"clear_session": [],
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
broker_router,
|
|
"_require_user",
|
|
lambda _request: {"id": "user-1", "username": "user@example.com"},
|
|
)
|
|
monkeypatch.setattr(
|
|
broker_router,
|
|
"block_live_strategy_for_broker_disconnect",
|
|
lambda user_id, reason="broker_disconnected": {
|
|
"run_id": "run-1",
|
|
"status": "STOPPED",
|
|
"reason": reason,
|
|
"user_id": user_id,
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
broker_router,
|
|
"disconnect_user_broker",
|
|
lambda user_id: calls["disconnect"].append(user_id),
|
|
)
|
|
monkeypatch.setattr(
|
|
broker_router,
|
|
"clear_zerodha_session",
|
|
lambda user_id: calls["clear_session"].append(user_id),
|
|
)
|
|
monkeypatch.setattr(broker_router, "send_email_async", lambda *_args, **_kwargs: None)
|
|
|
|
response = client.post("/api/broker/disconnect", cookies={"session_id": "session-1"})
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {
|
|
"connected": False,
|
|
"brokerState": "DISCONNECTED",
|
|
"strategy": {
|
|
"run_id": "run-1",
|
|
"status": "STOPPED",
|
|
"reason": "broker_disconnected",
|
|
"user_id": "user-1",
|
|
},
|
|
}
|
|
assert calls["disconnect"] == ["user-1"]
|
|
assert calls["clear_session"] == ["user-1"]
|
|
|
|
|
|
def test_block_live_strategy_for_broker_disconnect_auto_stops_active_live_run(monkeypatch):
|
|
import app.services.strategy_service as strategy_service
|
|
|
|
calls = []
|
|
|
|
monkeypatch.setattr(strategy_service, "_effective_running_run_id", lambda _user_id: "run-1")
|
|
monkeypatch.setattr(strategy_service, "_load_config", lambda _user_id, _run_id: {"mode": "LIVE"})
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"stop_engine",
|
|
lambda user_id, run_id, timeout=10.0: calls.append(("stop_engine", user_id, run_id, timeout)),
|
|
)
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"deactivate_strategy_config",
|
|
lambda user_id, run_id: calls.append(("deactivate", user_id, run_id)),
|
|
)
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"stop_run",
|
|
lambda user_id, run_id, reason="user_request": calls.append(("stop_run", user_id, run_id, reason)),
|
|
)
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"_write_status",
|
|
lambda user_id, run_id, status: calls.append(("write_status", user_id, run_id, status)),
|
|
)
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"_set_run_status_or_raise",
|
|
lambda user_id, run_id, status, meta=None: calls.append(("set_run_status", user_id, run_id, status, meta)),
|
|
)
|
|
|
|
result = strategy_service.block_live_strategy_for_broker_disconnect(
|
|
"user-1",
|
|
reason="broker_disconnected",
|
|
)
|
|
|
|
assert result == {
|
|
"run_id": "run-1",
|
|
"status": "STOPPED",
|
|
"reason": "broker_disconnected",
|
|
}
|
|
assert ("stop_engine", "user-1", "run-1", 10.0) in calls
|
|
assert ("deactivate", "user-1", "run-1") in calls
|
|
assert ("stop_run", "user-1", "run-1", "broker_disconnected") in calls
|
|
assert ("write_status", "user-1", "run-1", "STOPPED") in calls
|
|
|
|
|
|
def test_strategy_status_marks_broker_disconnected_block(monkeypatch):
|
|
import app.services.strategy_service as strategy_service
|
|
|
|
monkeypatch.setattr(strategy_service, "_effective_running_run_id", lambda _user_id: None)
|
|
monkeypatch.setattr(strategy_service, "get_active_run_id", lambda _user_id: "run-1")
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"_load_config",
|
|
lambda _user_id, _run_id: {
|
|
"strategy": "Golden Nifty",
|
|
"mode": "LIVE",
|
|
"broker": "ZERODHA",
|
|
"active": True,
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"_get_run_row",
|
|
lambda _user_id, _run_id: {"status": "STOPPED", "meta": {}, "started_at": None, "stopped_at": None},
|
|
)
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"_broker_block_state",
|
|
lambda _user_id, _cfg: {
|
|
"blocked": True,
|
|
"reason": "broker_disconnected",
|
|
"broker_state": "DISCONNECTED",
|
|
"broker": "ZERODHA",
|
|
},
|
|
)
|
|
|
|
class FakeCursor:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def execute(self, _sql, _params):
|
|
return None
|
|
|
|
def fetchone(self):
|
|
return None
|
|
|
|
class FakeConnection:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def cursor(self):
|
|
return FakeCursor()
|
|
|
|
monkeypatch.setattr(strategy_service, "db_connection", lambda: FakeConnection())
|
|
|
|
status = strategy_service.get_strategy_status("user-1")
|
|
|
|
assert status["status"] == "STOPPED"
|
|
assert status["strategy_blocked"] is True
|
|
assert status["strategy_block_reason"] == "broker_disconnected"
|
|
assert status["broker_state"] == "DISCONNECTED"
|
|
assert status["broker"] == "ZERODHA"
|
|
|
|
|
|
def test_strategy_status_clears_broker_block_after_reconnect(monkeypatch):
|
|
import app.services.strategy_service as strategy_service
|
|
|
|
monkeypatch.setattr(strategy_service, "_effective_running_run_id", lambda _user_id: None)
|
|
monkeypatch.setattr(strategy_service, "get_active_run_id", lambda _user_id: "run-1")
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"_load_config",
|
|
lambda _user_id, _run_id: {
|
|
"strategy": "Golden Nifty",
|
|
"mode": "LIVE",
|
|
"broker": "ZERODHA",
|
|
"active": True,
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"_get_run_row",
|
|
lambda _user_id, _run_id: {"status": "STOPPED", "meta": {}, "started_at": None, "stopped_at": None},
|
|
)
|
|
monkeypatch.setattr(
|
|
strategy_service,
|
|
"_broker_block_state",
|
|
lambda _user_id, _cfg: {
|
|
"blocked": False,
|
|
"reason": None,
|
|
"broker_state": "VALID",
|
|
"broker": "ZERODHA",
|
|
},
|
|
)
|
|
|
|
class FakeCursor:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def execute(self, _sql, _params):
|
|
return None
|
|
|
|
def fetchone(self):
|
|
return None
|
|
|
|
class FakeConnection:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
def cursor(self):
|
|
return FakeCursor()
|
|
|
|
monkeypatch.setattr(strategy_service, "db_connection", lambda: FakeConnection())
|
|
|
|
status = strategy_service.get_strategy_status("user-1")
|
|
|
|
assert status["strategy_blocked"] is False
|
|
assert status["strategy_block_reason"] is None
|
|
assert status["broker_state"] == "VALID"
|