import os import sys import types import importlib import pytest from fastapi.testclient import TestClient class FakeResponse: def __init__(self, status_code: int = 200, json_data=None, text_data: str | None = None): self.status_code = status_code self._json_data = json_data self._text_data = text_data def json(self): return self._json_data @property def text(self): if self._text_data is not None: return self._text_data # Best-effort string return "" class FakeSession: """ Test-only Podman session stub. Routes by URL substring to return deterministic JSON. """ def __init__(self): self.calls = [] def get(self, url: str): self.calls.append(("GET", url)) # Pods list (DYNAMIC) if "/libpod/pods/json" in url: return FakeResponse(200, json_data=[]) # Containers list (DYNAMIC) if "/libpod/containers/json" in url: return FakeResponse(200, json_data=[]) # Container inspect (DYNAMIC) if "/libpod/containers/" in url and url.endswith("/json"): # Return some dict to prove JSON return FakeResponse(200, json_data={"Id": "dummy"}) # Container logs (FIXED endpoint expects {"logs": "..."} from wrapper endpoint, not Podman directly, # but app.py fetches Podman logs and then wraps into {"logs": text}. if "/libpod/containers/" in url and "/logs?" in url: return FakeResponse(200, json_data=None, text_data="line1\nline2\n") # Fallback: return JSON dict return FakeResponse(200, json_data={}) def post(self, url: str, **kwargs): self.calls.append(("POST", url, kwargs)) # kube/play passthrough (workloads deploy) if "/libpod/kube/play" in url: # app.py gebruikt SESSION.post(url, data=yaml_content).json() return FakeResponse(200, json_data={"kube": "played"}) # Container start/stop/restart: treat as success if "/libpod/containers/" in url: return FakeResponse(204, json_data={}) # Pod start/stop etc: treat as success if "/libpod/pods/" in url: return FakeResponse(200, json_data={"ok": True}) return FakeResponse(200, json_data={}) def delete(self, url: str): self.calls.append(("DELETE", url)) return FakeResponse(200, json_data={"deleted": True}) @pytest.fixture(scope="session") def app_module(): """ Import app.py as module `app` from /app, while ensuring requests_unixsocket import is safe. No runtime changes to app.py; only test-time monkeypatching later. """ # Ensure /app is importable if "/app" not in sys.path: sys.path.insert(0, "/app") # If requests_unixsocket isn't installed or to avoid real socket usage, provide a minimal stub. if "requests_unixsocket" not in sys.modules: mod = types.ModuleType("requests_unixsocket") mod.Session = lambda: FakeSession() sys.modules["requests_unixsocket"] = mod app = importlib.import_module("app") return app @pytest.fixture() def client(app_module, monkeypatch, tmp_path): """ TestClient with all external side-effects stubbed: - Podman socket calls via app.SESSION - systemctl calls via app.run - filesystem roots via WORKLOADS_DIR + ALLOWLIST_FILE """ # Prepare temp workload tree under tmp_path workloads_dir = tmp_path / "workloads" systemd_dir = workloads_dir / "systemd" systemd_dir.mkdir(parents=True, exist_ok=True) # A defined container (*.container) for /containers-dashboard defined entry (systemd_dir / "sonarr.container").write_text("[Container]\nImage=dummy\n", encoding="utf-8") # Files endpoints operate under WORKLOADS_DIR; UI focuses on systemd subtree # Create a file for /files/read/save/delete (systemd_dir / "test.txt").write_text("hello\n", encoding="utf-8") # Create a non-empty dir for /files/rmdir 409 scenario nonempty_dir = systemd_dir / "nonempty" nonempty_dir.mkdir(parents=True, exist_ok=True) (nonempty_dir / "keep.txt").write_text("x", encoding="utf-8") # Create a yaml to create a defined pod entry for /pods-dashboard (workloads_dir / "mediaserver.yaml").write_text("kind: Pod\nmetadata:\n name: mediaserver\n", encoding="utf-8") # Allowlist file for /systemd/allowlist and allow-mode enforcement allowlist_file = tmp_path / "allowed_units.txt" allowlist_file.write_text("sonarr.service\nmediaserver.service\n", encoding="utf-8") # Patch module globals to point at tmp filesystem (test-only) monkeypatch.setattr(app_module, "WORKLOADS_DIR", str(workloads_dir)) monkeypatch.setattr(app_module, "ALLOWLIST_FILE", str(allowlist_file)) # Stub Podman session object monkeypatch.setattr(app_module, "SESSION", FakeSession()) # Stub systemctl runner def fake_run(cmd): # cmd is a list, e.g. ["systemctl","--user","is-active","mediaserver.service"] cmd_str = " ".join(cmd) if "is-active mediaserver.service" in cmd_str: return 0, "active" if "is-active sonarr.service" in cmd_str: return 0, "inactive" # For POST /{action}/{unit}, return something stable if cmd[:3] == ["systemctl", "--user", "status"]: return 0, "Active: active (running)" if cmd[:3] == ["systemctl", "--user", "daemon-reload"]: return 0, "ok" if cmd[:3] == ["systemctl", "--user", "start"]: return 0, "started" if cmd[:3] == ["systemctl", "--user", "stop"]: return 0, "stopped" if cmd[:3] == ["systemctl", "--user", "restart"]: return 0, "restarted" # fallback return 0, "ok" monkeypatch.setattr(app_module, "run", fake_run) return TestClient(app_module.app)