""" Unit tests for Bybit provider — raw JSON → domain event parsing. """ import pytest from app.domain.events import EventType from app.providers.bybit import BybitProvider @pytest.fixture def provider(): return BybitProvider() # ── Trade parsing ────────────────────────────────────────────────────── def test_parse_trade_basic(provider): """Basic publicTrade parsing.""" raw = { "topic": "publicTrade.BTCUSDT", "data": [ { "s": "BTCUSDT", "S": "Buy", "v": "0.001", "p": "70500.5", "T": 1672515782136, "i": "trade123", } ], } event = provider._parse(raw) assert event is not None assert event.event_type == EventType.TRADE assert event.symbol == "BTCUSDT" assert event.price == 70500.5 assert event.size == 0.001 assert event.side == "buy" assert event.trade_id == "trade123" assert event.provider == "bybit" def test_parse_trade_sell_side(provider): """Sell side trade.""" raw = { "topic": "publicTrade.ETHUSDT", "data": [ { "s": "ETHUSDT", "S": "Sell", "v": "10.5", "p": "2100.00", "T": 1672515782136, "i": "t456", } ], } event = provider._parse(raw) assert event.side == "sell" assert event.symbol == "ETHUSDT" def test_parse_trade_batch_takes_last(provider): """Multiple trades in a batch — takes the last one.""" raw = { "topic": "publicTrade.BTCUSDT", "data": [ {"s": "BTCUSDT", "S": "Buy", "v": "0.001", "p": "70000.0", "T": 100, "i": "first"}, {"s": "BTCUSDT", "S": "Sell", "v": "0.01", "p": "70100.0", "T": 200, "i": "last"}, ], } event = provider._parse(raw) assert event.trade_id == "last" assert event.price == 70100.0 def test_parse_trade_timestamp(provider): """Exchange timestamp is correctly parsed.""" raw = { "topic": "publicTrade.BTCUSDT", "data": [ {"s": "BTCUSDT", "S": "Buy", "v": "1", "p": "70000", "T": 1672515782136, "i": "x"}, ], } event = provider._parse(raw) assert event.ts_exchange is not None assert event.ts_exchange.year >= 2022 # ── Ticker (quote) parsing ───────────────────────────────────────────── def test_parse_ticker_basic(provider): """Bybit tickers → QuoteEvent.""" raw = { "topic": "tickers.BTCUSDT", "data": { "symbol": "BTCUSDT", "bid1Price": "70000.5", "bid1Size": "1.5", "ask1Price": "70001.0", "ask1Size": "2.0", "ts": "1672515782136", }, } event = provider._parse(raw) assert event is not None assert event.event_type == EventType.QUOTE assert event.symbol == "BTCUSDT" assert event.bid == 70000.5 assert event.ask == 70001.0 assert event.bid_size == 1.5 assert event.ask_size == 2.0 assert event.provider == "bybit" def test_parse_ticker_missing_bid(provider): """Ticker without bid → returns None.""" raw = { "topic": "tickers.BTCUSDT", "data": {"symbol": "BTCUSDT"}, } event = provider._parse(raw) assert event is None # ── Edge cases ───────────────────────────────────────────────────────── def test_parse_unknown_topic(provider): """Unknown topic → None.""" raw = {"topic": "some_unknown.BTCUSDT", "data": {}} event = provider._parse(raw) assert event is None def test_parse_pong_skipped(provider): """Pong/subscribe messages are not events.""" raw = {"op": "pong", "success": True} # _parse would not be called for op messages (handled in stream()), # but let's verify _parse returns None for incomplete data event = provider._parse(raw) assert event is None def test_parse_empty_trade_data(provider): """Empty trade data array → None.""" raw = {"topic": "publicTrade.BTCUSDT", "data": []} event = provider._parse(raw) assert event is None