import pytest from app.config import Settings from app.api import positions class _Scalar: def __init__(self, value): self._value = value def scalar_one_or_none(self): return self._value def scalar_one(self): return self._value def scalars(self): return self def all(self): return self._value class _Request: async def json(self): return { "wallet": "0xabc", "timestamp": 123, "signature": "0xsig", } class _Trade: id = 7 wallet_address = "0xabc" closed_at = None hl_order_id = "paper" leverage = 3 asset = "BTC" exit_price = 101.5 pnl_usd = 2.25 class _ClosedTrade(_Trade): """Same as _Trade but with closed_at set, simulating a committed close.""" from datetime import datetime, timezone closed_at = datetime(2026, 1, 1, 0, 0, 0) class _Sub: leverage = 3 hl_api_key = None class _Db: def __init__(self, responses): self._responses = list(responses) async def execute(self, _stmt): return _Scalar(self._responses.pop(0)) @pytest.mark.asyncio async def test_manual_close_returns_close_result(monkeypatch): async def fake_close_and_finalize(**_kwargs): return None monkeypatch.setattr(positions, "verify_signed_request", lambda **_kwargs: None) from app.services import bot_engine monkeypatch.setattr(bot_engine, "close_and_finalize", fake_close_and_finalize) # Responses in order: load trade → load sub → populate_existing re-read # (B45 fix: one query with populate_existing=True instead of old stale cache). # _ClosedTrade has closed_at set so the B46 success guard passes. db = _Db([_Trade(), _Sub(), _ClosedTrade()]) result = await positions.manual_close(7, _Request(), db) assert result.status == "ok" assert result.trade_id == 7 assert result.exit_price == 101.5 assert result.pnl_usd == 2.25 assert result.reason == "manual" @pytest.mark.asyncio async def test_open_positions_requires_signed_wallet_read(monkeypatch): calls = [] def fake_verify(**kwargs): calls.append(kwargs) # get_open_positions now uses verify_signed_request_any (accepts either # view_positions or view_user action), so patch that instead. monkeypatch.setattr(positions, "verify_signed_request_any", fake_verify) db = _Db([[]]) from app.services.signed_request import SignedReadCreds result = await positions.get_open_positions( wallet="0xABC", creds=SignedReadCreds(ts=123, sig="0xsig"), db=db, ) assert result.wallet == "0xabc" assert calls == [{ "actions": [positions.ACTION_VIEW_POSITIONS, positions.ACTION_VIEW_USER], "wallet": "0xabc", "timestamp_ms": 123, "signature": "0xsig", "body": None, "allow_replay": True, }] def test_settings_default_to_production_when_environment_not_explicit(): settings = Settings(database_url="sqlite+aiosqlite:///./test.db") assert settings.environment == "production"