"""Unit tests for the Gitea HTTP client. Uses respx to mock httpx.""" from __future__ import annotations import base64 import httpx import pytest import respx from homelab_mcp_gitea.client import GiteaClient, GiteaError BASE = "https://gitea.test" TOKEN = "test-token-xyz" def _client() -> GiteaClient: return GiteaClient(base_url=BASE, token=TOKEN) @pytest.mark.asyncio @respx.mock async def test_list_repos_returns_parsed_json() -> None: respx.get(f"{BASE}/api/v1/user/repos").mock( return_value=httpx.Response( 200, json=[ {"full_name": "root/alpha", "private": False}, {"full_name": "root/beta", "private": True}, ], ) ) async with _client() as c: repos = await c.list_repos(limit=10) assert len(repos) == 2 assert repos[0]["full_name"] == "root/alpha" @pytest.mark.asyncio @respx.mock async def test_get_repo_returns_metadata() -> None: respx.get(f"{BASE}/api/v1/repos/root/alpha").mock( return_value=httpx.Response( 200, json={ "full_name": "root/alpha", "description": "test repo", "private": False, "default_branch": "main", "stars_count": 7, "open_issues_count": 2, }, ) ) async with _client() as c: data = await c.get_repo("root", "alpha") assert data["stars_count"] == 7 @pytest.mark.asyncio @respx.mock async def test_get_file_decodes_base64_content() -> None: raw = "hello world\n" encoded = base64.b64encode(raw.encode()).decode() respx.get(f"{BASE}/api/v1/repos/root/alpha/contents/README.md").mock( return_value=httpx.Response(200, json={"content": encoded}) ) async with _client() as c: text = await c.get_file("root", "alpha", "README.md") assert text == raw @pytest.mark.asyncio @respx.mock async def test_create_issue_posts_payload_and_returns_issue() -> None: respx.post(f"{BASE}/api/v1/repos/root/alpha/issues").mock( return_value=httpx.Response( 201, json={ "number": 42, "title": "bug", "html_url": "https://gitea.test/root/alpha/issues/42", }, ) ) async with _client() as c: issue = await c.create_issue("root", "alpha", "bug", "details") assert issue["number"] == 42 sent = respx.calls.last.request import json payload = json.loads(sent.content) assert payload == {"title": "bug", "body": "details"} @pytest.mark.asyncio @respx.mock async def test_list_open_prs_filters_by_state() -> None: respx.get(f"{BASE}/api/v1/repos/root/alpha/pulls").mock( return_value=httpx.Response( 200, json=[ { "number": 5, "title": "feat: x", "user": {"login": "alice"}, "base": {"ref": "main"}, } ], ) ) async with _client() as c: prs = await c.list_open_prs("root", "alpha") assert prs[0]["number"] == 5 sent = respx.calls.last.request assert sent.url.params["state"] == "open" @pytest.mark.asyncio @respx.mock async def test_merge_pr_uses_lowercase_do_field() -> None: """Trap regression: Gitea needs the JSON key `do`, not `Do` or `Merge_Method`.""" respx.post(f"{BASE}/api/v1/repos/root/alpha/pulls/5/merge").mock( return_value=httpx.Response(200, content=b"") ) async with _client() as c: ok = await c.merge_pr("root", "alpha", 5) assert ok is True import json sent_payload = json.loads(respx.calls.last.request.content) assert sent_payload == {"do": "merge"} @pytest.mark.asyncio @respx.mock async def test_merge_pr_raises_on_nonempty_body_even_at_200() -> None: """The other half of the trap: status 200 with a body means failure.""" respx.post(f"{BASE}/api/v1/repos/root/alpha/pulls/5/merge").mock( return_value=httpx.Response( 200, json={"message": "[Do]: Required"}, ) ) async with _client() as c: with pytest.raises(GiteaError) as exc_info: await c.merge_pr("root", "alpha", 5) assert "do" in str(exc_info.value).lower() @pytest.mark.asyncio @respx.mock async def test_http_error_raises_gitea_error() -> None: respx.get(f"{BASE}/api/v1/repos/root/missing").mock( return_value=httpx.Response(404, json={"message": "not found"}) ) async with _client() as c: with pytest.raises(GiteaError) as exc_info: await c.get_repo("root", "missing") assert exc_info.value.status == 404 def test_constructor_requires_url_and_token() -> None: import os os.environ.pop("GITEA_URL", None) os.environ.pop("GITEA_TOKEN", None) with pytest.raises(ValueError, match="GITEA_URL"): GiteaClient(token="t") with pytest.raises(ValueError, match="GITEA_TOKEN"): GiteaClient(base_url=BASE)