from types import SimpleNamespace import pytest from backend.app.connectors.portainer import PortainerConnector from backend.app.core.config import settings CONTROLLER_OWNED_CI_NAMES = ( "itworx-ci-checks", "itworx-ci-backend", "itworx-ci-frontend", "itworx-ci-security", "itworx-ci-browser", "itworx-ci-build", "itworx-security-runner", "itworx-validation-runner-2", "itworx-validation-runner-3", "itworx-docker-runner", "itworx-browser-runner", "itworx-validation-runner-burst-1", "gitea-runner-dind", ) def _connector_with_containers(monkeypatch, containers): monkeypatch.setitem(settings.apis, "PORTAINER_URL", "https://portainer.example.test") monkeypatch.setitem(settings.apis, "PORTAINER_API_KEY", "test-key") monkeypatch.setitem(settings.apis, "PORTAINER_ENDPOINT_ID", "9") connector = PortainerConnector() monkeypatch.setattr( connector, "list_containers", lambda: {"status": "ok", "containers": containers}, ) calls = [] def request(method: str, path: str, **kwargs): calls.append((method, path, kwargs)) return SimpleNamespace(status_code=204) monkeypatch.setattr(connector, "_request", request) return connector, calls def test_restart_request_allows_docker_grace_period_without_shortening_container_timeout(monkeypatch): previous = dict(settings.apis) settings.apis.update({ "PORTAINER_URL": "https://portainer.example.test", "PORTAINER_API_KEY": "test-key", "PORTAINER_ENDPOINT_ID": "9", }) connector = PortainerConnector() monkeypatch.setattr( connector, "list_containers", lambda: { "status": "ok", "containers": [{"id": "user-container-id", "name": "jellyfin", "appId": "jellyfin"}], }, ) calls: list[tuple[str, str, dict]] = [] def request(method: str, path: str, **kwargs): calls.append((method, path, kwargs)) return SimpleNamespace(status_code=204) monkeypatch.setattr(connector, "_request", request) try: result = connector.restart_container("jellyfin") finally: settings.apis.clear() settings.apis.update(previous) assert result["status"] == "ok" assert calls == [( "POST", "/api/endpoints/9/docker/containers/user-container-id/restart", {"timeout": 30.0}, )] @pytest.mark.parametrize("container_name", CONTROLLER_OWNED_CI_NAMES) def test_restart_blocks_fixed_controller_owned_ci_containers_by_name(monkeypatch, container_name): connector, calls = _connector_with_containers( monkeypatch, [{"id": "0123456789abcdef0123456789abcdef", "name": container_name, "appId": "ci-stack"}], ) result = connector.restart_container(container_name) assert result == {"status": "blocked", "reason": "controller_owned_ci_lifecycle"} assert calls == [] @pytest.mark.parametrize( "target", ( "itworx-ci-backend", "0123456789abcdef0123456789abcdef", "01234567", "backend-ci-stack", ), ) def test_restart_blocks_controller_owned_container_selected_by_alias(monkeypatch, target): connector, calls = _connector_with_containers( monkeypatch, [{ "id": "0123456789abcdef0123456789abcdef", "name": "itworx-ci-backend", "appId": "backend-ci-stack", }], ) result = connector.restart_container(target) assert result == {"status": "blocked", "reason": "controller_owned_ci_lifecycle"} assert calls == [] @pytest.mark.parametrize("container_name", ("itworx-unraid-deploy", "jellyfin")) def test_restart_still_allows_unrelated_deploy_and_user_containers(monkeypatch, container_name): container_id = f"id-{container_name}" connector, calls = _connector_with_containers( monkeypatch, [{"id": container_id, "name": container_name, "appId": container_name}], ) result = connector.restart_container(container_name) assert result == { "status": "ok", "container": container_id, "target": container_name, "action": "container.restart", } assert calls == [( "POST", f"/api/endpoints/9/docker/containers/{container_id}/restart", {"timeout": 30.0}, )]