diff --git a/src/github_app.py b/src/github_app.py index c18b6a9..80ff13d 100644 --- a/src/github_app.py +++ b/src/github_app.py @@ -4,12 +4,16 @@ from __future__ import annotations import contextlib +import logging import time from typing import Generator import jwt import requests +GITHUB_API_TIMEOUT_SECONDS = 10 +logger = logging.getLogger(__name__) + class GithubAppToken: def __init__(self, private_key, app_id) -> None: @@ -22,6 +26,7 @@ def get_token(self, installation_id: int) -> Generator[str, None, None]: req = requests.post( url=f"https://api.github.com/app/installations/{installation_id}/access_tokens", headers=self.headers, + timeout=GITHUB_API_TIMEOUT_SECONDS, ) req.raise_for_status() resp = req.json() @@ -29,10 +34,14 @@ 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']}"}, + timeout=GITHUB_API_TIMEOUT_SECONDS, + ) + except requests.RequestException: + logger.warning("Failed to revoke 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..8e28a35 --- /dev/null +++ b/tests/test_github_app.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from unittest.mock import Mock +from unittest.mock import patch + +import requests + +from src.github_app import GITHUB_API_TIMEOUT_SECONDS +from src.github_app import GithubAppToken + + +def test_get_token_suppresses_installation_token_revoke_timeout(caplog): + app_token = GithubAppToken.__new__(GithubAppToken) + app_token.headers = {"Authorization": "Bearer jwt"} + + post_response = Mock() + post_response.json.return_value = {"token": "installation-token"} + + with patch("src.github_app.requests.post", return_value=post_response) as mock_post: + with patch( + "src.github_app.requests.delete", + side_effect=requests.ConnectTimeout(), + ) as mock_delete: + with app_token.get_token(installation_id=123) as token: + assert token == "installation-token" + + post_response.raise_for_status.assert_called_once() + mock_post.assert_called_once_with( + url="https://api.github.com/app/installations/123/access_tokens", + headers={"Authorization": "Bearer jwt"}, + timeout=GITHUB_API_TIMEOUT_SECONDS, + ) + mock_delete.assert_called_once_with( + "https://api.github.com/installation/token", + headers={"Authorization": "token installation-token"}, + timeout=GITHUB_API_TIMEOUT_SECONDS, + ) + assert "Failed to revoke GitHub installation token" in caplog.text