- 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
159 lines
5.6 KiB
Python
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()
|