from types import SimpleNamespace from sqlmodel import Session, SQLModel, create_engine from backend.app.services import action_execution from backend.app.services.action_execution import approval_execution_supported from backend.app.services.action_registry import ( ACTION_RISK, ACTIONS, action_default_target, ) from backend.app.services.privatemuse_webhook_repair import ( PrivateMuseWebhookRepair, _ENDPOINT, _REPAIR_SCRIPT, ) class FakeResponse: def __init__(self, payload): self.payload = payload def json(self): return self.payload class FakePortainer: def __init__(self, exit_code=0): self.exit_code = exit_code self.calls = [] def get_container_status(self, name): assert name == "gitea-deploy-control" return { "status": "ok", "container": { "id": "controller-id", "name": "gitea-deploy-control", "state": "running", }, } def _config(self): return SimpleNamespace(endpoint_id="3") def _request(self, method, path, **kwargs): self.calls.append((method, path, kwargs)) if path.endswith("/containers/controller-id/exec"): return FakeResponse({"Id": "exec-webhook"}) if path.endswith("/exec/exec-webhook/start"): return FakeResponse({}) if path.endswith("/exec/exec-webhook/json"): return FakeResponse({"Running": False, "ExitCode": self.exit_code}) raise AssertionError(path) def test_privatemuse_webhook_repair_is_fixed_high_risk_typed_action(): action = "deployment.webhook.repair_privatemuse" assert action in ACTIONS assert ACTION_RISK[action] == "high" assert action_default_target(action) == "jens/privatemuse" assert approval_execution_supported(action) is True def test_privatemuse_webhook_repair_executes_only_fixed_contract(): fake = FakePortainer() result = PrivateMuseWebhookRepair(fake).run() assert result == { "status": "repaired", "repository": "jens/privatemuse", "endpoint": _ENDPOINT, "branch": "main", "event": "push", } command = fake.calls[0][2]["json"]["Cmd"] assert command[:3] == ["python3", "-c", _REPAIR_SCRIPT] assert command[3:] == ["jens/privatemuse", _ENDPOINT] def test_privatemuse_webhook_repair_script_never_returns_credentials(): assert "runner_guard_admin_token" in _REPAIR_SCRIPT assert 'secret_root = Path("/controller-config/webhook-secrets")' in _REPAIR_SCRIPT assert 'secret_path = secret_root / "jens__privatemuse"' in _REPAIR_SCRIPT assert "secretMaterialReturned" not in _REPAIR_SCRIPT assert 'print(f"privatemuse_runnerless_webhook_ready hook_id={hook_id}"' in _REPAIR_SCRIPT assert "print(secret" not in _REPAIR_SCRIPT assert "print(token" not in _REPAIR_SCRIPT def test_privatemuse_webhook_repair_proves_effective_controller_reachability(): assert "gitea_config_path" not in _REPAIR_SCRIPT assert '"docker", "exec", "Gitea"' in _REPAIR_SCRIPT assert '"http://198.19.0.1:1299/health"' in _REPAIR_SCRIPT assert 'health_payload.get("status") != "ok"' in _REPAIR_SCRIPT assert 'health_payload.get("protocolVersion") != "itworx-controller-request/v2"' in _REPAIR_SCRIPT def test_privatemuse_webhook_repair_is_fail_closed_and_non_destructive(): assert 'repository != "jens/privatemuse"' in _REPAIR_SCRIPT assert 'project.get("branch") != "main"' in _REPAIR_SCRIPT assert '"Managed validation / full (push)"' in _REPAIR_SCRIPT assert 'url.endswith("/v1/gitea/push/jens/privatemuse")' in _REPAIR_SCRIPT assert "len(exact) > 1 or semantic_other" in _REPAIR_SCRIPT assert 'request("PATCH", f"/repos/Jens/PrivateMuse/hooks/{hook_id}", payload)' in _REPAIR_SCRIPT assert 'request(\n "POST",\n "/repos/Jens/PrivateMuse/hooks",' in _REPAIR_SCRIPT for forbidden in ( "docker restart", "docker stop", "docker rm", "docker compose", 'request("DELETE"', ): assert forbidden not in _REPAIR_SCRIPT def test_privatemuse_webhook_repair_maps_validation_failures(): result = PrivateMuseWebhookRepair(FakePortainer(exit_code=34)).run() assert result == { "status": "validation_failed", "reason": "PrivateMuse has ambiguous or stale runnerless webhook endpoints", } def test_privatemuse_webhook_repair_rejects_any_other_action_target(): result = action_execution.execute_governed_action( None, # type: ignore[arg-type] action="deployment.webhook.repair_privatemuse", target="jens/not-privatemuse", source="chatgpt-mcp", risk="high", reason="Regression test for exact-target enforcement.", original_policy_decision="allowed", ) assert result["success"] is False assert result["result"] == "blocked" assert result["message"] == "PrivateMuse webhook repair accepts only jens/privatemuse." def test_privatemuse_webhook_repair_dispatches_fixed_executor(monkeypatch, tmp_path): engine = create_engine(f"sqlite:///{tmp_path / 'webhook-action.db'}") SQLModel.metadata.create_all(engine) class FakeRepair: def run(self): return { "status": "repaired", "repository": "jens/privatemuse", "endpoint": _ENDPOINT, "branch": "main", "event": "push", } import backend.app.services.privatemuse_webhook_repair as repair_module monkeypatch.setattr(repair_module, "PrivateMuseWebhookRepair", FakeRepair) with Session(engine) as session: result = action_execution.execute_governed_action( session, action="deployment.webhook.repair_privatemuse", target="jens/privatemuse", source="chatgpt-mcp", risk="high", reason="Repair the fixed PrivateMuse runnerless hook.", original_policy_decision="approval_required", ) assert result["success"] is True assert result["status"] == "success" assert result["policyDecision"] == "allowed" assert result["originalPolicyDecision"] == "approval_required" assert result["jobId"].startswith("job-") assert result["auditId"].startswith("audit-")