diff --git a/src/git_commit_guard/__init__.py b/src/git_commit_guard/__init__.py index df7b7f2..05a9975 100644 --- a/src/git_commit_guard/__init__.py +++ b/src/git_commit_guard/__init__.py @@ -411,6 +411,8 @@ def _resolve_github_username(rev, email): except urllib.error.HTTPError as e: if e.code == HTTPStatus.NOT_FOUND: commits_api_404 = True + elif e.code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): + raise except (urllib.error.URLError, TimeoutError): pass if username is None: @@ -454,6 +456,24 @@ def check_signature(rev, result): "git operation timed out — cannot verify signature", check=Check.SIGNATURE, ) + except urllib.error.HTTPError as e: + if e.code == HTTPStatus.UNAUTHORIZED: + result.error( + "GitHub API rejected token (HTTP 401) — " + "GITHUB_TOKEN may be invalid or expired", + check=Check.SIGNATURE, + ) + elif e.code == HTTPStatus.FORBIDDEN: + result.error( + "GitHub API forbidden (HTTP 403) — GITHUB_TOKEN may lack " + "'repo' scope, or you are unauthenticated and rate-limited", + check=Check.SIGNATURE, + ) + else: + result.error( + f"GitHub API error (HTTP {e.code}) — cannot verify signature", + check=Check.SIGNATURE, + ) except (urllib.error.URLError, TimeoutError): result.error( "GitHub API unreachable — cannot verify signature", diff --git a/tests/test_git_commit_guard.py b/tests/test_git_commit_guard.py index d58046f..0c96f9e 100644 --- a/tests/test_git_commit_guard.py +++ b/tests/test_git_commit_guard.py @@ -929,6 +929,84 @@ def test_commits_api_non_404_http_error_falls_through(self): check_signature("abc123", r) assert r.ok + def test_commits_api_401_surfaces_token_message(self): + r = Result() + with ( + patch( + "git_commit_guard._get_author_email", return_value="user@example.com" + ), + patch( + "git_commit_guard._get_github_remote_info", + return_value=("owner", "repo"), + ), + patch( + "git_commit_guard._fetch_github_commit_author", + side_effect=urllib.error.HTTPError( + url="", code=401, msg="Unauthorized", hdrs=None, fp=None + ), + ), + ): + check_signature("abc123", r) + assert not r.ok + assert any("rejected token (HTTP 401)" in msg for _, _, msg in r.errors) + + def test_commits_api_403_surfaces_token_message(self): + r = Result() + with ( + patch( + "git_commit_guard._get_author_email", return_value="user@example.com" + ), + patch( + "git_commit_guard._get_github_remote_info", + return_value=("owner", "repo"), + ), + patch( + "git_commit_guard._fetch_github_commit_author", + side_effect=urllib.error.HTTPError( + url="", code=403, msg="Forbidden", hdrs=None, fp=None + ), + ), + ): + check_signature("abc123", r) + assert not r.ok + assert any("forbidden (HTTP 403)" in msg for _, _, msg in r.errors) + + def test_search_api_403_surfaces_rate_limit_message(self): + r = Result() + with ( + patch( + "git_commit_guard._get_author_email", return_value="corp@example.com" + ), + patch("git_commit_guard._get_github_remote_info", return_value=None), + patch( + "git_commit_guard._fetch_github_username", + side_effect=urllib.error.HTTPError( + url="", code=403, msg="Forbidden", hdrs=None, fp=None + ), + ), + ): + check_signature("abc123", r) + assert not r.ok + assert any("forbidden (HTTP 403)" in msg for _, _, msg in r.errors) + + def test_other_http_error_uses_generic_http_message(self): + r = Result() + with ( + patch( + "git_commit_guard._get_author_email", return_value="corp@example.com" + ), + patch("git_commit_guard._get_github_remote_info", return_value=None), + patch( + "git_commit_guard._fetch_github_username", + side_effect=urllib.error.HTTPError( + url="", code=500, msg="Server Error", hdrs=None, fp=None + ), + ), + ): + check_signature("abc123", r) + assert not r.ok + assert any("GitHub API error (HTTP 500)" in msg for _, _, msg in r.errors) + def test_url_error_fails(self): r = Result() with patch(