"""Shared fixtures for /api/v1/swarm-updates tests. The tests never talk to a real worker — ``UpdaterClient`` is monkeypatched to a recording fake. That keeps the tests fast and lets us assert call shapes (tarball-once, per-host dispatch, include_self ordering) without standing up TLS endpoints. """ from __future__ import annotations import uuid as _uuid from datetime import datetime, timezone from typing import Any import httpx import pytest from decnet.web.dependencies import repo async def _add_host( name: str, address: str = "10.0.0.1", *, with_updater: bool = True, status: str = "enrolled", ) -> dict[str, Any]: uuid = str(_uuid.uuid4()) await repo.add_swarm_host({ "uuid": uuid, "name": name, "address": address, "agent_port": 8765, "status": status, "client_cert_fingerprint": "abc123", "updater_cert_fingerprint": "def456" if with_updater else None, "cert_bundle_path": f"/tmp/{name}", "enrolled_at": datetime.now(timezone.utc), "notes": None, }) return {"uuid": uuid, "name": name, "address": address} @pytest.fixture def add_host(): return _add_host @pytest.fixture def fake_updater(monkeypatch): """Install a fake ``UpdaterClient`` + tar builder into every route module. The returned ``Fake`` exposes hooks so individual tests decide per-host behaviour: response codes, exceptions, update-self outcomes, etc. """ class FakeResponse: def __init__(self, status_code: int, body: dict[str, Any] | None = None): self.status_code = status_code self._body = body or {} self.content = b"payload" def json(self) -> dict[str, Any]: return self._body class FakeUpdaterClient: calls: list[tuple[str, str, dict]] = [] # (host_name, method, kwargs) health_responses: dict[str, dict[str, Any]] = {} update_responses: dict[str, FakeResponse | BaseException] = {} update_self_responses: dict[str, FakeResponse | BaseException] = {} rollback_responses: dict[str, FakeResponse | BaseException] = {} def __init__(self, host=None, **_kw): self._name = host["name"] if host else "?" async def __aenter__(self): return self async def __aexit__(self, *exc): return None async def health(self): FakeUpdaterClient.calls.append((self._name, "health", {})) resp = FakeUpdaterClient.health_responses.get(self._name) if isinstance(resp, BaseException): raise resp return resp or {"status": "ok", "releases": []} async def update(self, tarball, sha=""): FakeUpdaterClient.calls.append((self._name, "update", {"tarball": tarball, "sha": sha})) resp = FakeUpdaterClient.update_responses.get(self._name, FakeResponse(200, {"probe": "ok"})) if isinstance(resp, BaseException): raise resp return resp async def update_self(self, tarball, sha=""): FakeUpdaterClient.calls.append((self._name, "update_self", {"tarball": tarball, "sha": sha})) resp = FakeUpdaterClient.update_self_responses.get(self._name, FakeResponse(200)) if isinstance(resp, BaseException): raise resp return resp async def rollback(self): FakeUpdaterClient.calls.append((self._name, "rollback", {})) resp = FakeUpdaterClient.rollback_responses.get(self._name, FakeResponse(200, {"status": "rolled back"})) if isinstance(resp, BaseException): raise resp return resp # Reset class-level state each test — fixtures are function-scoped but # the class dicts survive otherwise. FakeUpdaterClient.calls = [] FakeUpdaterClient.health_responses = {} FakeUpdaterClient.update_responses = {} FakeUpdaterClient.update_self_responses = {} FakeUpdaterClient.rollback_responses = {} for mod in ( "decnet.web.router.swarm_updates.api_list_host_releases", "decnet.web.router.swarm_updates.api_push_update", "decnet.web.router.swarm_updates.api_push_update_self", "decnet.web.router.swarm_updates.api_rollback_host", ): monkeypatch.setattr(f"{mod}.UpdaterClient", FakeUpdaterClient) # Stub the tarball builders so tests don't spend seconds re-tarring the # repo on every assertion. The byte contents don't matter for the route # contract — the updater side is faked. monkeypatch.setattr( "decnet.web.router.swarm_updates.api_push_update.tar_working_tree", lambda root, extra_excludes=None: b"tarball-bytes", ) monkeypatch.setattr( "decnet.web.router.swarm_updates.api_push_update.detect_git_sha", lambda root: "deadbeef", ) monkeypatch.setattr( "decnet.web.router.swarm_updates.api_push_update_self.tar_working_tree", lambda root, extra_excludes=None: b"tarball-bytes", ) monkeypatch.setattr( "decnet.web.router.swarm_updates.api_push_update_self.detect_git_sha", lambda root: "deadbeef", ) return {"client": FakeUpdaterClient, "Response": FakeResponse} @pytest.fixture def connection_drop_exc(): """A realistic 'updater re-exec mid-response' exception.""" return httpx.RemoteProtocolError("server disconnected")