mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-30 05:52:23 +00:00
* merge branch develop with main * bump desktop version * fix the merge * sync Cargo.lock to desktop 2.0.0 * fix cli conftest.py --------- Co-authored-by: deeleeramone <>
226 lines
7.4 KiB
Python
226 lines
7.4 KiB
Python
"""Tests for the Deribit JSON-RPC transport."""
|
|
|
|
import pytest
|
|
from openbb_core.app.model.abstract.error import OpenBBError
|
|
|
|
from openbb_deribit.utils import client
|
|
|
|
|
|
class _Response:
|
|
"""A stand-in for one aiohttp response."""
|
|
|
|
def __init__(self, payload, status=200):
|
|
self.payload = payload
|
|
self.status = status
|
|
|
|
async def json(self, content_type=None):
|
|
"""Return the decoded payload, or raise what a bad body would."""
|
|
if isinstance(self.payload, Exception):
|
|
raise self.payload
|
|
|
|
return self.payload
|
|
|
|
|
|
class _Session:
|
|
"""A stand-in for one aiohttp session."""
|
|
|
|
def __init__(self, response):
|
|
self.response = response
|
|
self.urls: list = []
|
|
|
|
async def get(self, url, timeout=None):
|
|
"""Record the URL and return the canned response."""
|
|
self.urls.append(url)
|
|
|
|
if isinstance(self.response, Exception):
|
|
raise self.response
|
|
|
|
return self.response
|
|
|
|
|
|
@pytest.fixture
|
|
def transport(monkeypatch):
|
|
"""Install a canned session and return it."""
|
|
|
|
def install(response, cached=True):
|
|
session = _Session(response)
|
|
|
|
async def _get_session():
|
|
return session
|
|
|
|
monkeypatch.setattr("openbb_deribit.utils.session.get_session", _get_session)
|
|
monkeypatch.setattr(
|
|
"openbb_deribit.utils.session.get_cached_session", _get_session
|
|
)
|
|
|
|
return session
|
|
|
|
return install
|
|
|
|
|
|
class TestBuildUrl:
|
|
"""``build_url`` writes the query the exchange expects."""
|
|
|
|
def test_no_params(self):
|
|
"""A method with no parameters carries no query string."""
|
|
assert client.build_url("get_currencies") == (
|
|
"https://www.deribit.com/api/v2/public/get_currencies"
|
|
)
|
|
|
|
def test_drops_none(self):
|
|
"""A parameter left unset is not sent."""
|
|
url = client.build_url("get_instruments", {"currency": "BTC", "kind": None})
|
|
|
|
assert url.endswith("get_instruments?currency=BTC")
|
|
|
|
def test_lowercases_booleans(self):
|
|
"""A boolean is sent as the lower-case literal the exchange reads."""
|
|
url = client.build_url("get_instruments", {"expired": True})
|
|
|
|
assert url.endswith("get_instruments?expired=true")
|
|
|
|
def test_encodes_values(self):
|
|
"""A value needing escaping is percent-encoded."""
|
|
url = client.build_url("ticker", {"instrument_name": "BTC-25DEC26-90000-C"})
|
|
|
|
assert "instrument_name=BTC-25DEC26-90000-C" in url
|
|
|
|
def test_empty_params_mapping(self):
|
|
"""An empty mapping is the same as none at all."""
|
|
assert client.build_url("status", {}) == client.build_url("status")
|
|
|
|
|
|
class TestRequest:
|
|
"""``request`` unwraps the JSON-RPC envelope and reports failures."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_result(self, transport):
|
|
"""A successful call returns the result member."""
|
|
session = transport(_Response({"jsonrpc": "2.0", "result": [1, 2]}))
|
|
|
|
assert await client.request("get_currencies") == [1, 2]
|
|
assert session.urls == ["https://www.deribit.com/api/v2/public/get_currencies"]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_uncached_path(self, transport):
|
|
"""Passing ``use_cache=False`` still returns the result."""
|
|
transport(_Response({"result": {"locked": "false"}}))
|
|
|
|
assert await client.request("status", use_cache=False) == {"locked": "false"}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_result(self, transport):
|
|
"""A payload with no result member returns None rather than raising."""
|
|
transport(_Response({"jsonrpc": "2.0"}))
|
|
|
|
assert await client.request("get_time") is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_json_rpc_error(self, transport):
|
|
"""A JSON-RPC error is raised with its message and reason."""
|
|
transport(
|
|
_Response(
|
|
{
|
|
"error": {
|
|
"message": "Invalid params",
|
|
"data": {"reason": "invalid currency"},
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
with pytest.raises(OpenBBError, match="invalid currency"):
|
|
await client.request("get_instruments", {"currency": "XYZ"})
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_json_rpc_error_without_data(self, transport):
|
|
"""An error carrying no data still names its message."""
|
|
transport(_Response({"error": {"message": "not_supported"}}))
|
|
|
|
with pytest.raises(OpenBBError, match="not_supported"):
|
|
await client.request("get_last_trades_by_currency")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_json_rpc_error_as_string(self, transport):
|
|
"""An error the exchange sends as a bare string is reported as-is."""
|
|
transport(_Response({"error": "rate_limited"}))
|
|
|
|
with pytest.raises(OpenBBError, match="rate_limited"):
|
|
await client.request("ticker")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_dict_payload(self, transport):
|
|
"""A body that is not an object is rejected."""
|
|
transport(_Response([1, 2, 3]))
|
|
|
|
with pytest.raises(OpenBBError, match="Unexpected Deribit response"):
|
|
await client.request("get_time")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rate_limited(self, transport):
|
|
"""A 429 names the status and says what to do about it."""
|
|
transport(_Response(None, status=429))
|
|
|
|
with pytest.raises(OpenBBError, match="HTTP 429"):
|
|
await client.request("get_order_book")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_other_bad_status(self, transport):
|
|
"""Any other non-200 names its status."""
|
|
transport(_Response(None, status=503))
|
|
|
|
with pytest.raises(OpenBBError, match="HTTP 503"):
|
|
await client.request("status")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_transport_failure(self, transport):
|
|
"""A transport failure is wrapped, naming the method."""
|
|
transport(TimeoutError("timed out"))
|
|
|
|
with pytest.raises(OpenBBError, match="public/ticker"):
|
|
await client.request("ticker")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_undecodable_body(self, transport):
|
|
"""A body that cannot be decoded is wrapped."""
|
|
transport(_Response(ValueError("not json")))
|
|
|
|
with pytest.raises(OpenBBError, match="ValueError"):
|
|
await client.request("get_time")
|
|
|
|
|
|
class TestGather:
|
|
"""``gather`` runs several calls and keeps the failures in place."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_in_order(self, monkeypatch):
|
|
"""Results come back in the order the calls were given."""
|
|
|
|
async def _request(method, params=None, use_cache=True):
|
|
return params["n"]
|
|
|
|
monkeypatch.setattr(client, "request", _request)
|
|
results = await client.gather(
|
|
[("ticker", {"n": 1}), ("ticker", {"n": 2}), ("ticker", {"n": 3})]
|
|
)
|
|
|
|
assert results == [1, 2, 3]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_keeps_exceptions(self, monkeypatch):
|
|
"""A call that fails leaves its exception in place of a result."""
|
|
|
|
async def _request(method, params=None, use_cache=True):
|
|
if params["n"] == 2:
|
|
raise OpenBBError("no")
|
|
|
|
return params["n"]
|
|
|
|
monkeypatch.setattr(client, "request", _request)
|
|
results = await client.gather(
|
|
[("ticker", {"n": 1}), ("ticker", {"n": 2})], use_cache=False
|
|
)
|
|
|
|
assert results[0] == 1
|
|
assert isinstance(results[1], OpenBBError)
|