From d2d3457fa0d497d97058646a40bfd3d96e0445ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 19 Apr 2026 08:03:53 +0000 Subject: [PATCH] Handle GitHub token revoke timeout safely Co-authored-by: Armen Zambrano G. --- src/github_app.py | 18 ++++++++++---- tests/test_github_app.py | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 tests/test_github_app.py diff --git a/src/github_app.py b/src/github_app.py index c18b6a9..4cbc071 100644 --- a/src/github_app.py +++ b/src/github_app.py @@ -4,12 +4,15 @@ from __future__ import annotations import contextlib +import logging import time from typing import Generator import jwt import requests +logger = logging.getLogger(__name__) + class GithubAppToken: def __init__(self, private_key, app_id) -> None: @@ -29,10 +32,17 @@ def get_token(self, installation_id: int) -> Generator[str, None, None]: # This token expires in an hour yield resp["token"] finally: - requests.delete( - "https://api.github.com/installation/token", - headers={"Authorization": f"token {resp['token']}"}, - ) + try: + requests.delete( + "https://api.github.com/installation/token", + headers={"Authorization": f"token {resp['token']}"}, + ) + except requests.RequestException: + # Cleanup should never hide the original failure from request handling. + logger.warning( + "Failed to revoke temporary GitHub installation token", + exc_info=True, + ) def get_jwt_token(self, private_key, app_id): payload = { diff --git a/tests/test_github_app.py b/tests/test_github_app.py new file mode 100644 index 0000000..5759b39 --- /dev/null +++ b/tests/test_github_app.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from src.github_app import GithubAppToken + + +def _build_response(token: str) -> MagicMock: + response = MagicMock() + response.raise_for_status.return_value = None + response.json.return_value = {"token": token} + return response + + +@patch.object(GithubAppToken, "get_authentication_header", return_value={}) +@patch("src.github_app.requests.delete") +@patch("src.github_app.requests.post") +def test_get_token_revokes_installation_token( + mock_post: MagicMock, + mock_delete: MagicMock, + _mock_get_authentication_header: MagicMock, +): + mock_post.return_value = _build_response("installation-token") + token_manager = GithubAppToken(private_key="unused", app_id="123") + + with token_manager.get_token(installation_id=42) as token: + assert token == "installation-token" + + mock_delete.assert_called_once_with( + "https://api.github.com/installation/token", + headers={"Authorization": "token installation-token"}, + ) + + +@patch.object(GithubAppToken, "get_authentication_header", return_value={}) +@patch("src.github_app.requests.delete") +@patch("src.github_app.requests.post") +def test_cleanup_timeout_does_not_mask_primary_exception( + mock_post: MagicMock, + mock_delete: MagicMock, + _mock_get_authentication_header: MagicMock, +): + mock_post.return_value = _build_response("installation-token") + mock_delete.side_effect = requests.ConnectTimeout("cleanup timeout") + token_manager = GithubAppToken(private_key="unused", app_id="123") + + with pytest.raises(requests.ConnectTimeout, match="primary timeout"): + with token_manager.get_token(installation_id=42): + raise requests.ConnectTimeout("primary timeout")