Files
ARIA e1bd1ad9eb Fix server threading, tag URLs, and tag date resolution
- Switch to ThreadingHTTPServer to handle concurrent requests
- Catch BrokenPipeError on response writes to suppress noisy tracebacks
- Change tag URL from /tags/ to /releases/tag/
- Resolve tag dates via commit SHA from tags API + commits endpoint (works for public repos without auth)
- Update tests to match new tag date resolution approach
2026-07-28 12:25:04 +02:00

159 lines
5.6 KiB
Python

"""Tests for the GitHub API client."""
import httpx
import pytest
import respx
from src.github import GitHubClient, AuthenticationError, NotFoundError
@pytest.fixture
def mock_api():
"""Enable respx mocking for test scope."""
with respx.mock:
yield respx
class TestFetchReleases:
def test_fetch_releases(self, mock_api):
mock_api.get("https://api.github.com/repos/owner/repo/releases").mock(
return_value=httpx.Response(200, json=[
{
"tag_name": "v1.0.0",
"name": "Release v1.0.0",
"body": "# Changelog\n\n- Fixed bug",
"published_at": "2025-01-15T10:30:00Z",
"html_url": "https://github.com/owner/repo/releases/tag/v1.0.0",
},
{
"tag_name": "v0.9.0",
"name": None,
"body": "",
"published_at": "2025-01-10T08:00:00Z",
"html_url": "https://github.com/owner/repo/releases/tag/v0.9.0",
},
])
)
client = GitHubClient(token="fake_token")
releases = client.fetch_releases("owner/repo")
client.close()
assert len(releases) == 2
assert releases[0]["tag_name"] == "v1.0.0"
assert releases[0]["title"] == "Release v1.0.0"
assert releases[0]["body"] == "# Changelog\n\n- Fixed bug"
assert releases[1]["title"] == "v0.9.0"
assert releases[1]["body"] == ""
def test_fetch_releases_404(self, mock_api):
mock_api.get("https://api.github.com/repos/no/such/releases").mock(
return_value=httpx.Response(404)
)
client = GitHubClient(token="fake_token")
with pytest.raises(NotFoundError):
client.fetch_releases("no/such")
client.close()
class TestFetchTags:
def test_fetch_tags(self, mock_api):
mock_api.get("https://api.github.com/repos/owner/repo/tags").mock(
return_value=httpx.Response(200, json=[
{"name": "v1.0.0", "commit": {"sha": "aaa111"}},
{"name": "v0.9.0", "commit": {"sha": "bbb222"}},
])
)
mock_api.get("https://api.github.com/repos/owner/repo/commits/aaa111").mock(
return_value=httpx.Response(200, json={
"commit": {"author": {"date": "2025-01-15T10:30:00Z"}}
})
)
mock_api.get("https://api.github.com/repos/owner/repo/commits/bbb222").mock(
return_value=httpx.Response(200, json={
"commit": {"author": {"date": "2025-01-10T08:00:00Z"}}
})
)
client = GitHubClient(token="fake_token")
tags = client.fetch_tags("owner/repo")
client.close()
assert len(tags) == 2
assert tags[0]["tag_name"] == "v1.0.0"
assert tags[0]["title"] == "v1.0.0"
assert tags[0]["body"] == ""
assert tags[0]["published_at"] == "2025-01-15T10:30:00Z"
assert tags[0]["html_url"] == "https://github.com/owner/repo/releases/tag/v1.0.0"
def test_fetch_tags_with_tag_object(self, mock_api):
"""Commit date is fetched directly from commit SHA in tags API."""
mock_api.get("https://api.github.com/repos/owner/repo/tags").mock(
return_value=httpx.Response(200, json=[
{"name": "v2.0.0", "commit": {"sha": "abc123"}},
])
)
mock_api.get("https://api.github.com/repos/owner/repo/commits/abc123").mock(
return_value=httpx.Response(200, json={
"commit": {"author": {"date": "2025-02-01T12:00:00Z"}}
})
)
client = GitHubClient(token="fake_token")
tags = client.fetch_tags("owner/repo")
client.close()
assert tags[0]["published_at"] == "2025-02-01T12:00:00Z"
def test_fetch_tags_fallback_date(self, mock_api):
"""When commit API fails, use current time as fallback."""
mock_api.get("https://api.github.com/repos/owner/repo/tags").mock(
return_value=httpx.Response(200, json=[
{"name": "v1.0.0", "commit": {"sha": "xyz789"}},
])
)
mock_api.get("https://api.github.com/repos/owner/repo/commits/xyz789").mock(
return_value=httpx.Response(404)
)
client = GitHubClient(token="fake_token")
tags = client.fetch_tags("owner/repo")
client.close()
assert tags[0]["published_at"] is not None
class TestRepoExists:
def test_repo_exists_true(self, mock_api):
mock_api.get("https://api.github.com/repos/owner/repo").mock(
return_value=httpx.Response(200, json={"full_name": "owner/repo"})
)
client = GitHubClient(token="fake_token")
assert client.repo_exists("owner/repo") is True
client.close()
def test_repo_exists_false(self, mock_api):
mock_api.get("https://api.github.com/repos/no/such").mock(
return_value=httpx.Response(404)
)
client = GitHubClient(token="fake_token")
assert client.repo_exists("no/such") is False
client.close()
class TestAuthentication:
def test_invalid_token(self, mock_api):
mock_api.get("https://api.github.com/rate_limit").mock(
return_value=httpx.Response(401)
)
client = GitHubClient(token="bad_token")
with pytest.raises(AuthenticationError, match="401 Unauthorized"):
client.validate_token()
client.close()
def test_no_token_skips_validation(self):
client = GitHubClient(token=None)
client.validate_token()
client.close()