# Tests for graceful handling of malformed JSON in HZ values. # Validates: Requirements 12.3 # The TUI MUST NOT crash when any individual HZ key contains malformed JSON. # Malformed JSON MUST result in all fields being None (no crash, no exception). import asyncio import sys import os import types from unittest.mock import MagicMock, patch, AsyncMock import pytest # --------------------------------------------------------------------------- # Ensure the TUI module is importable without textual/hazelcast/httpx installed # --------------------------------------------------------------------------- sys.path.insert(0, os.path.dirname(__file__)) for _mod in ("textual", "textual.app", "textual.widgets", "textual.containers", "httpx", "hazelcast"): if _mod not in sys.modules: sys.modules[_mod] = types.ModuleType(_mod) import textual.app as _textual_app import textual.widgets as _textual_widgets import textual.containers as _textual_containers _textual_app.App = object _textual_app.ComposeResult = object _textual_widgets.Static = object _textual_widgets.VerticalScroll = object _textual_containers.Horizontal = object from dolphin_tui import ( color_age, fmt_float, fmt_pnl, DataSnapshot, DolphinDataFetcher, ) # --------------------------------------------------------------------------- # Malformed JSON inputs to test # --------------------------------------------------------------------------- MALFORMED_JSON_INPUTS = [ "{bad json", "not-json", "null", "[]", "123", "", "{", "}", "{'key': 'value'}", # single quotes — invalid JSON "undefined", "NaN", ] # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_fetcher(): return DolphinDataFetcher() def _make_mock_map_returning(value): """Return a mock IMap whose .get(key).result() returns the given value.""" future = MagicMock() future.result.return_value = value hz_map = MagicMock() hz_map.get.return_value = future hz_map.key_set.return_value = future return hz_map def _make_fetcher_with_malformed_json(malformed: str): """Create a DolphinDataFetcher whose hz_client returns malformed JSON for every map key.""" fetcher = DolphinDataFetcher() fetcher.hz_connected = True mock_map = _make_mock_map_returning(malformed) map_future = MagicMock() map_future.result.return_value = mock_map mock_client = MagicMock() mock_client.get_map.return_value = map_future fetcher.hz_client = mock_client return fetcher def _run(coro): return asyncio.get_event_loop().run_until_complete(coro) def _fetch_with_malformed_json(malformed: str): fetcher = _make_fetcher_with_malformed_json(malformed) with patch.object(fetcher, "fetch_prefect", new=AsyncMock(return_value=(False, []))): with patch.object(fetcher, "tail_log", return_value=[]): with patch.object(fetcher, "_start_reconnect", return_value=None): return _run(fetcher.fetch()) # --------------------------------------------------------------------------- # _parse_scan: malformed JSON -> all None, no exception # --------------------------------------------------------------------------- @pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS) def test_parse_scan_malformed_no_crash(bad_json): """_parse_scan must not raise on malformed JSON.""" fetcher = _make_fetcher() result = fetcher._parse_scan(bad_json) # must not raise assert isinstance(result, dict) @pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS) def test_parse_scan_malformed_returns_none_fields(bad_json): """_parse_scan must return all-None fields for malformed JSON.""" fetcher = _make_fetcher() result = fetcher._parse_scan(bad_json) for key in ("scan_number", "vel_div", "w50_velocity", "w750_velocity", "instability_50", "scan_bridge_ts", "scan_age_s"): assert result[key] is None, f"_parse_scan({bad_json!r})[{key!r}] should be None" assert result["asset_prices"] == {} # --------------------------------------------------------------------------- # _parse_safety: malformed JSON -> all None, no exception # --------------------------------------------------------------------------- @pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS) def test_parse_safety_malformed_no_crash(bad_json): """_parse_safety must not raise on malformed JSON.""" fetcher = _make_fetcher() result = fetcher._parse_safety(bad_json) # must not raise assert isinstance(result, dict) @pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS) def test_parse_safety_malformed_returns_none_fields(bad_json): """_parse_safety must return all-None fields for malformed JSON.""" fetcher = _make_fetcher() result = fetcher._parse_safety(bad_json) for key in ("posture", "rm", "cat1", "cat2", "cat3", "cat4", "cat5"): assert result[key] is None, f"_parse_safety({bad_json!r})[{key!r}] should be None" # --------------------------------------------------------------------------- # _parse_heartbeat: malformed JSON -> all None, no exception # --------------------------------------------------------------------------- @pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS) def test_parse_heartbeat_malformed_no_crash(bad_json): """_parse_heartbeat must not raise on malformed JSON.""" fetcher = _make_fetcher() result = fetcher._parse_heartbeat(bad_json) # must not raise assert isinstance(result, dict) @pytest.mark.parametrize("bad_json", MALFORMED_JSON_INPUTS) def test_parse_heartbeat_malformed_returns_none_fields(bad_json): """_parse_heartbeat must return all-None fields for malformed JSON.""" fetcher = _make_fetcher() result = fetcher._parse_heartbeat(bad_json) for key in ("heartbeat_ts", "heartbeat_phase", "heartbeat_flow", "heartbeat_age_s"): assert result[key] is None, f"_parse_heartbeat({bad_json!r})[{key!r}] should be None" # --------------------------------------------------------------------------- # fetch() with malformed JSON from HZ -> DataSnapshot, no crash # --------------------------------------------------------------------------- @pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""]) def test_fetch_malformed_json_returns_datasnapshot(bad_json): """fetch() must return a DataSnapshot even when HZ returns malformed JSON.""" snap = _fetch_with_malformed_json(bad_json) assert isinstance(snap, DataSnapshot) @pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""]) def test_fetch_malformed_json_scan_fields_none(bad_json): """fetch() with malformed JSON must produce None scan fields.""" snap = _fetch_with_malformed_json(bad_json) assert snap.scan_number is None assert snap.vel_div is None assert snap.w50_velocity is None assert snap.instability_50 is None assert snap.scan_bridge_ts is None assert snap.scan_age_s is None @pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""]) def test_fetch_malformed_json_safety_fields_none(bad_json): """fetch() with malformed JSON must produce None safety fields.""" snap = _fetch_with_malformed_json(bad_json) assert snap.posture is None assert snap.rm is None assert snap.cat1 is None @pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""]) def test_fetch_malformed_json_heartbeat_fields_none(bad_json): """fetch() with malformed JSON must produce None heartbeat fields.""" snap = _fetch_with_malformed_json(bad_json) assert snap.heartbeat_ts is None assert snap.heartbeat_phase is None assert snap.heartbeat_age_s is None @pytest.mark.parametrize("bad_json", ["{bad json", "not-json", "null", "[]", "123", ""]) def test_fetch_malformed_json_no_crash(bad_json): """fetch() must not raise any exception when HZ returns malformed JSON.""" # If this doesn't raise, the test passes snap = _fetch_with_malformed_json(bad_json) assert snap is not None # --------------------------------------------------------------------------- # fmt_float, fmt_pnl, color_age handle None gracefully (parse errors -> None fields) # --------------------------------------------------------------------------- def test_fmt_float_handles_none_from_parse_error(): """fmt_float(None) must return '--' — parse errors produce None fields.""" assert fmt_float(None) == "--" def test_fmt_pnl_handles_none_from_parse_error(): """fmt_pnl(None) must return ('white', '--') — parse errors produce None fields.""" color, text = fmt_pnl(None) assert text == "--" assert color == "white" def test_color_age_handles_none_from_parse_error(): """color_age(None) must return ('dim', 'N/A') — parse errors produce None fields.""" color, text = color_age(None) assert text == "N/A" assert color == "dim" def test_all_none_snapshot_fmt_float_no_crash(): """All float fields on an all-None DataSnapshot must format without crashing.""" snap = DataSnapshot() for field_name in ( "vel_div", "w50_velocity", "w750_velocity", "instability_50", "acb_boost", "acb_beta", "funding_btc", "dvol_btc", "fng", "vix", "capital", "pnl", "rm", ): val = getattr(snap, field_name) result = fmt_float(val) assert result == "--", f"fmt_float({field_name}=None) should be '--', got {result!r}" def test_all_none_snapshot_fmt_pnl_no_crash(): """PnL fields on an all-None DataSnapshot must format without crashing.""" snap = DataSnapshot() for field_name in ("pnl", "nautilus_pnl"): val = getattr(snap, field_name) color, text = fmt_pnl(val) assert text == "--" assert color == "white" def test_all_none_snapshot_color_age_no_crash(): """Age fields on an all-None DataSnapshot must format without crashing.""" snap = DataSnapshot() for field_name in ("scan_age_s", "exf_age_s", "esof_age_s", "heartbeat_age_s"): val = getattr(snap, field_name) color, text = color_age(val) assert text == "N/A" assert color == "dim"