"""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()