"""Slow Docker calls must fail with a message, not a bare 500. A volume or image prune that deletes a lot of data easily runs past docker-py's 60s read timeout. requests' ``ReadTimeout`` is not a ``DockerException``, so it slipped past ``safe_call`` and the UI only showed "Request failed with status code 500". Prunes now use a client with a long timeout, and any transport timeout that still happens is turned into a ``DockerError`` (-> 502 + detail). """ from __future__ import annotations import pytest import requests def test_safe_call_wraps_read_timeout(): from docker_client import DockerError, safe_call def slow(): raise requests.exceptions.ReadTimeout("Read timed out. (read timeout=60)") with pytest.raises(DockerError) as info: safe_call(slow) assert info.value.error == "docker_timeout" def test_safe_call_wraps_connection_error(): from docker_client import DockerError, safe_call def gone(): raise requests.exceptions.ConnectionError("socket went away") with pytest.raises(DockerError) as info: safe_call(gone) assert info.value.error == "docker_unavailable" def test_volume_prune_timeout_is_a_502_with_detail(as_admin, monkeypatch): from services import volume_service class Volumes: def prune(self, filters=None): raise requests.exceptions.ReadTimeout("Read timed out.") class Client: volumes = Volumes() monkeypatch.setattr(volume_service, "get_long_client", lambda: Client()) r = as_admin.post("/api/volumes/prune") assert r.status_code == 502 assert r.json()["error"] == "docker_timeout" @pytest.mark.parametrize( "module, fn, args", [ ("volume_service", "prune_volumes", ()), ("network_service", "prune_networks", ()), ], ) def test_prunes_use_the_long_timeout_client(monkeypatch, module, fn, args): import importlib svc = importlib.import_module(f"services.{module}") used = [] class Any_: def prune(self, filters=None): used.append("long") return {} class Client: volumes = images = networks = Any_() def short(): raise AssertionError("prune must not use the 60s client") monkeypatch.setattr(svc, "get_client", short) monkeypatch.setattr(svc, "get_long_client", lambda: Client()) getattr(svc, fn)(*args) assert used == ["long"] # The image prune goes image by image and has its own tests in # test_image_prune.py, including that it uses the long-timeout client.