"""Without internet, the firmware check logs the outage once, not every failure. Each check of each printer model fetches the Bambu Lab wiki and download page. Offline, every one of those fetches logged a full ERROR (and every model a WARNING): measured, 3 polls of 3 models gave 12 ERROR and 9 WARNING lines saying the same thing. The outage is now logged once as a WARNING, failures during it at debug, and the recovery once at INFO. """ from __future__ import annotations import logging from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest from backend.app.services.firmware_check import FirmwareCheckService LOGGER = "backend.app.services.firmware_check" class _Offline: """Both HTTP clients failing at the transport, as without internet.""" def __init__(self, service: FirmwareCheckService): self.service = service self.online = False self.curl = MagicMock() self.curl.get = AsyncMock(side_effect=self._curl_get) async def _curl_get(self, _url): if not self.online: raise RuntimeError("Failed to perform, curl: (7) Couldn't connect to server") return MagicMock(status_code=503, text="") async def httpx_get(self, url, **_kwargs): if not self.online: raise httpx.ConnectError("All connection attempts failed") return httpx.Response(200, text='

x

', request=httpx.Request("GET", url)) def _records(caplog, level: int) -> list[logging.LogRecord]: return [r for r in caplog.records if r.name == LOGGER and r.levelno == level] @pytest.fixture def offline(tmp_path): service = FirmwareCheckService() net = _Offline(service) with ( patch("backend.app.services.firmware_check._data_dir", tmp_path), patch.object(service, "_get_bambulab_client", return_value=net.curl), patch.object(service._client, "get", new=net.httpx_get), ): yield service, net @pytest.mark.asyncio async def test_an_outage_is_logged_once(offline, caplog): service, _net = offline caplog.set_level(logging.DEBUG, logger=LOGGER) for _poll in range(3): for model in ("X1C", "P1S", "H2D"): await service.check_for_update(model, "01.00.00.00") assert _records(caplog, logging.ERROR) == [] # Once per host: the wiki and bambulab.com fail independently. warnings = [r.getMessage() for r in _records(caplog, logging.WARNING)] assert len(warnings) == 2, warnings assert {w.split()[0] for w in warnings} == {"wiki.bambulab.com", "bambulab.com"} assert all("unreachable" in w for w in warnings) @pytest.mark.asyncio async def test_the_recovery_is_logged_and_a_new_outage_warns_again(offline, caplog): service, net = offline caplog.set_level(logging.DEBUG, logger=LOGGER) await service.check_for_update("X1C", "01.00.00.00") net.online = True await service.check_for_update("P1S", "01.00.00.00") recovered = [r.getMessage() for r in _records(caplog, logging.INFO) if "reachable again" in r.getMessage()] assert {m.split()[0] for m in recovered} == {"wiki.bambulab.com", "bambulab.com"} net.online = False caplog.clear() await service.check_for_update("H2D", "01.00.00.00") assert len(_records(caplog, logging.WARNING)) == 2 @pytest.mark.asyncio async def test_one_host_down_is_logged_once_without_false_recoveries(offline, caplog): """The wiki answers while bambulab.com does not: one warning for that host, no 'reachable again' from the other.""" service, _net = offline caplog.set_level(logging.DEBUG, logger=LOGGER) async def wiki_up_only(url, **_kwargs): return httpx.Response(200, text='

x

', request=httpx.Request("GET", url)) with patch.object(service._client, "get", new=wiki_up_only): for _poll in range(3): for model in ("X1C", "P1S", "H2D"): await service.check_for_update(model, "01.00.00.00") assert len(_records(caplog, logging.WARNING)) == 1 assert [r for r in _records(caplog, logging.INFO) if "reachable again" in r.getMessage()] == [] @pytest.mark.asyncio @pytest.mark.parametrize("error", [httpx.TooManyRedirects("loop"), httpx.DecodingError("bad encoding")]) async def test_a_wiki_error_that_is_not_an_outage_still_degrades_quietly(offline, error): service, _net = offline async def failing_wiki(_url, **_kwargs): raise error with patch.object(service._client, "get", new=failing_wiki): result = await service.check_for_update("X1C", "01.00.00.00") assert isinstance(result, dict)