mirror of
https://github.com/fusion44/blitz_api.git
synced 2026-08-13 11:52:45 +02:00
Replace the /sse/subscribe endpoint with a /ws WebSocket endpoint backed by ws_mgr, and port the warmup helper (_send_sse_event -> _send_ws_event, SSE -> Event, broadcast_sse_msg -> broadcast_msg). Also fixes two tests left over from the prior SSE->Event/broadcast_msg rename that still referenced the old names and were failing collection/execution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
198 lines
6.3 KiB
Python
198 lines
6.3 KiB
Python
"""
|
|
Tests for the SSE warmup behavior in app.main.
|
|
|
|
Regression tests for raspiblitz#3608: the Apps tab in the WebUI never loads
|
|
on bitcoin-only nodes because the warmup data was sent under an SSE event
|
|
name the WebUI does not listen to.
|
|
"""
|
|
|
|
from app.api.models import ApiStartupStatus, StartupState
|
|
from app.api.utils import Event
|
|
from app.apps.constants import AppManagementProcessState
|
|
from app.apps.models import AppStatusQueryResult, AppStatusUpdateTaskMessage
|
|
from app.external.result_type.src.result.result import Ok
|
|
|
|
import app.main as main
|
|
|
|
|
|
def _make_app_status_message() -> Ok:
|
|
return Ok(
|
|
AppStatusUpdateTaskMessage(
|
|
state=AppManagementProcessState.SUCCESS,
|
|
message=AppStatusQueryResult(data=[], errors=[], timestamp=1),
|
|
)
|
|
)
|
|
|
|
|
|
def _patch_common(monkeypatch, sent, *, node_type: str, lightning: StartupState):
|
|
async def fake_send(id, event, data):
|
|
sent.append((id, event, data))
|
|
|
|
async def fake_bitcoin_warmup_data():
|
|
return [{"blocks": 1}, {"cpu": 1}]
|
|
|
|
monkeypatch.setattr(main, "node_type", node_type)
|
|
monkeypatch.setattr(
|
|
main,
|
|
"api_startup_status",
|
|
ApiStartupStatus(bitcoin=StartupState.DONE, lightning=lightning),
|
|
)
|
|
monkeypatch.setattr(main, "_send_ws_event", fake_send)
|
|
monkeypatch.setattr(
|
|
main, "get_bitcoin_client_warmup_data", fake_bitcoin_warmup_data
|
|
)
|
|
monkeypatch.setattr(main, "new_connections", ["conn-1"])
|
|
monkeypatch.setattr(main, "warmup_running", False)
|
|
|
|
|
|
async def test_bitcoinonly_warmup_sends_app_status_as_app_state_message(monkeypatch):
|
|
"""The WebUI only listens for Event.APP_STATE_MESSAGE to populate the Apps
|
|
tab. In bitcoin-only mode the warmup app status must be sent under that
|
|
event, exactly like in lightning mode."""
|
|
sent = []
|
|
_patch_common(
|
|
monkeypatch, sent, node_type="none", lightning=StartupState.DISABLED
|
|
)
|
|
|
|
async def fake_warmup_data():
|
|
return [
|
|
{"alias": "test"},
|
|
{"blocks": 1},
|
|
_make_app_status_message(),
|
|
{"cpu": 1},
|
|
]
|
|
|
|
monkeypatch.setattr(
|
|
main, "get_full_client_warmup_data_bitcoinonly", fake_warmup_data
|
|
)
|
|
|
|
await main.warmup_new_connections()
|
|
|
|
events = [event for (_, event, _) in sent]
|
|
assert Event.APP_STATE_MESSAGE in events, (
|
|
f"expected app status under event '{Event.APP_STATE_MESSAGE}', "
|
|
f"got events: {events}"
|
|
)
|
|
|
|
|
|
async def test_bitcoinonly_warmup_data_converts_exceptions(monkeypatch):
|
|
"""A single failing data source must not wipe out the whole warmup
|
|
data set; it must be converted to an error entry like in the
|
|
lightning variant."""
|
|
from app.api import warmup
|
|
|
|
async def failing_system_info():
|
|
raise RuntimeError("system info unavailable")
|
|
|
|
async def fake_btc_info():
|
|
return {"blocks": 1}
|
|
|
|
async def fake_app_status():
|
|
return _make_app_status_message()
|
|
|
|
async def fake_hardware_info():
|
|
return {"cpu": 1}
|
|
|
|
monkeypatch.setattr(warmup, "get_system_info", failing_system_info)
|
|
monkeypatch.setattr(warmup, "get_btc_info", fake_btc_info)
|
|
monkeypatch.setattr(warmup, "_get_app_status_data", fake_app_status)
|
|
monkeypatch.setattr(warmup, "get_hardware_info", fake_hardware_info)
|
|
|
|
res = await warmup.get_full_client_warmup_data_bitcoinonly()
|
|
|
|
assert isinstance(res, list) and len(res) == 4, (
|
|
f"expected a 4-entry warmup data list, got: {res!r}"
|
|
)
|
|
assert res[1] == {"blocks": 1}
|
|
assert res[3] == {"cpu": 1}
|
|
|
|
|
|
async def test_warmup_running_flag_reset_on_error(monkeypatch):
|
|
"""If fetching warmup data blows up, the warmup_running flag must be
|
|
reset — otherwise every future SSE client is permanently starved of
|
|
warmup data until the API restarts."""
|
|
sent = []
|
|
_patch_common(
|
|
monkeypatch, sent, node_type="none", lightning=StartupState.DISABLED
|
|
)
|
|
|
|
async def broken_warmup_data():
|
|
return None # what @logger.catch produces when the gather raises
|
|
|
|
monkeypatch.setattr(
|
|
main, "get_full_client_warmup_data_bitcoinonly", broken_warmup_data
|
|
)
|
|
|
|
await main.warmup_new_connections() # must not raise
|
|
|
|
assert main.warmup_running is False
|
|
|
|
|
|
async def test_app_status_cache_error_triggers_update_when_unlocked(monkeypatch):
|
|
"""Characterization: when the cache read errors and no update lock is
|
|
held, an update task must be triggered so clients eventually get data."""
|
|
from app.api import warmup
|
|
from app.api.error_report.report import Report
|
|
from app.external.result_type.src.result.result import Err
|
|
|
|
class FakeCache:
|
|
async def get_cached_app_status(self):
|
|
return Err(Report("redis unavailable"))
|
|
|
|
class FakeTask:
|
|
def __init__(self):
|
|
self.delayed = 0
|
|
|
|
def delay(self):
|
|
self.delayed += 1
|
|
|
|
async def fake_lock_status(key):
|
|
return Ok(False)
|
|
|
|
fake_task = FakeTask()
|
|
monkeypatch.setattr(warmup, "app_cache", FakeCache())
|
|
monkeypatch.setattr(warmup, "get_lock_status", fake_lock_status)
|
|
monkeypatch.setattr(warmup, "update_app_state_task", fake_task)
|
|
|
|
result = await warmup._get_app_status_data()
|
|
|
|
assert isinstance(result, Ok)
|
|
assert result.ok_value is None
|
|
assert fake_task.delayed == 1, "update task must be triggered"
|
|
|
|
|
|
async def test_warmup_ready_does_not_fetch_partial_data(monkeypatch):
|
|
"""When the API is fully initialized (lightning DISABLED counts as
|
|
initialized), warmup must not fall through into the partial-data
|
|
branches and re-fetch bitcoin data."""
|
|
sent = []
|
|
_patch_common(
|
|
monkeypatch, sent, node_type="none", lightning=StartupState.DISABLED
|
|
)
|
|
|
|
called = []
|
|
|
|
async def tracking_bitcoin_warmup_data():
|
|
called.append(True)
|
|
return [{"blocks": 1}, {"cpu": 1}]
|
|
|
|
async def fake_warmup_data():
|
|
return [
|
|
{"alias": "test"},
|
|
{"blocks": 1},
|
|
_make_app_status_message(),
|
|
{"cpu": 1},
|
|
]
|
|
|
|
monkeypatch.setattr(
|
|
main, "get_bitcoin_client_warmup_data", tracking_bitcoin_warmup_data
|
|
)
|
|
monkeypatch.setattr(
|
|
main, "get_full_client_warmup_data_bitcoinonly", fake_warmup_data
|
|
)
|
|
|
|
await main.warmup_new_connections()
|
|
|
|
assert not called, (
|
|
"fully-initialized warmup must not call get_bitcoin_client_warmup_data"
|
|
)
|