test: add 106 unit + smoke tests, 93% coverage
- pytest + respx + pytest-cov added as dev dependencies - tests/smoke/test_registration.py: verify all 66 tools registered and unique - tests/unit/: 12 files covering every tool — HTTP method, URL, params/body, errors - tests/config.test.json + conftest.py: zero-network test setup via GITEA_CONFIG - CLAUDE.md: developer guide with testing philosophy, pyramid, and commands - server/gitea_mcp.py: get_commit, list_pr_files tools added - README.md: tool count corrected to 66
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import json
|
||||
import pytest
|
||||
import httpx
|
||||
import respx
|
||||
from server.gitea_mcp import (
|
||||
list_pull_requests, get_pull_request, create_pull_request, update_pull_request,
|
||||
merge_pull_request, get_pr_diff, list_pr_files, list_pr_reviews, create_pr_review,
|
||||
)
|
||||
|
||||
BASE = "https://gitea.test/api/v1"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_list_pull_requests():
|
||||
route = respx.get(f"{BASE}/repos/owner/repo/pulls").mock(
|
||||
return_value=httpx.Response(200, json=[])
|
||||
)
|
||||
list_pull_requests("owner", "repo")
|
||||
params = dict(route.calls[0].request.url.params)
|
||||
assert params["state"] == "open"
|
||||
assert params["limit"] == "20"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_list_pull_requests_closed():
|
||||
route = respx.get(f"{BASE}/repos/owner/repo/pulls").mock(
|
||||
return_value=httpx.Response(200, json=[])
|
||||
)
|
||||
list_pull_requests("owner", "repo", state="closed", limit=5)
|
||||
params = dict(route.calls[0].request.url.params)
|
||||
assert params["state"] == "closed"
|
||||
assert params["limit"] == "5"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_get_pull_request():
|
||||
route = respx.get(f"{BASE}/repos/owner/repo/pulls/7").mock(
|
||||
return_value=httpx.Response(200, json={"number": 7, "title": "Add feature"})
|
||||
)
|
||||
result = get_pull_request("owner", "repo", 7)
|
||||
assert route.called
|
||||
assert result["number"] == 7
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_create_pull_request():
|
||||
route = respx.post(f"{BASE}/repos/owner/repo/pulls").mock(
|
||||
return_value=httpx.Response(201, json={"number": 10})
|
||||
)
|
||||
result = create_pull_request("owner", "repo", "Add feature", "feature/x", "main")
|
||||
assert route.called
|
||||
body = json.loads(route.calls[0].request.content)
|
||||
assert body["title"] == "Add feature"
|
||||
assert body["head"] == "feature/x"
|
||||
assert body["base"] == "main"
|
||||
assert "body" not in body
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_create_pull_request_with_body_and_labels():
|
||||
route = respx.post(f"{BASE}/repos/owner/repo/pulls").mock(
|
||||
return_value=httpx.Response(201, json={"number": 11})
|
||||
)
|
||||
create_pull_request(
|
||||
"owner", "repo", "Fix bug", "fix/crash", "main",
|
||||
body="Fixes crash on startup", labels=[3],
|
||||
)
|
||||
body = json.loads(route.calls[0].request.content)
|
||||
assert body["body"] == "Fixes crash on startup"
|
||||
assert body["labels"] == [3]
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_update_pull_request():
|
||||
route = respx.patch(f"{BASE}/repos/owner/repo/pulls/5").mock(
|
||||
return_value=httpx.Response(200, json={"number": 5})
|
||||
)
|
||||
update_pull_request("owner", "repo", 5, title="New title")
|
||||
body = json.loads(route.calls[0].request.content)
|
||||
assert body["title"] == "New title"
|
||||
assert "body" not in body
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_merge_pull_request_defaults():
|
||||
route = respx.post(f"{BASE}/repos/owner/repo/pulls/3/merge").mock(
|
||||
return_value=httpx.Response(200, json={"success": True})
|
||||
)
|
||||
merge_pull_request("owner", "repo", 3)
|
||||
assert route.called
|
||||
body = json.loads(route.calls[0].request.content)
|
||||
assert body["Do"] == "merge"
|
||||
assert body["delete_branch_after_merge"] is False
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_merge_pull_request_squash_with_delete():
|
||||
route = respx.post(f"{BASE}/repos/owner/repo/pulls/4/merge").mock(
|
||||
return_value=httpx.Response(200, json={"success": True})
|
||||
)
|
||||
merge_pull_request("owner", "repo", 4, merge_style="squash", delete_branch_after_merge=True)
|
||||
body = json.loads(route.calls[0].request.content)
|
||||
assert body["Do"] == "squash"
|
||||
assert body["delete_branch_after_merge"] is True
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_get_pr_diff():
|
||||
diff_text = "diff --git a/file.py b/file.py\n+added line\n"
|
||||
route = respx.get(f"{BASE}/repos/owner/repo/pulls/2.diff").mock(
|
||||
return_value=httpx.Response(200, text=diff_text)
|
||||
)
|
||||
result = get_pr_diff("owner", "repo", 2)
|
||||
assert route.called
|
||||
assert result["diff"] == diff_text
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_get_pr_diff_error():
|
||||
respx.get(f"{BASE}/repos/owner/repo/pulls/99.diff").mock(
|
||||
return_value=httpx.Response(404, text="not found")
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="404"):
|
||||
get_pr_diff("owner", "repo", 99)
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_list_pr_files():
|
||||
route = respx.get(f"{BASE}/repos/owner/repo/pulls/5/files").mock(
|
||||
return_value=httpx.Response(200, json=[{"filename": "server/gitea_mcp.py", "status": "modified"}])
|
||||
)
|
||||
result = list_pr_files("owner", "repo", 5)
|
||||
assert route.called
|
||||
assert result[0]["filename"] == "server/gitea_mcp.py"
|
||||
params = dict(route.calls[0].request.url.params)
|
||||
assert params["limit"] == "50"
|
||||
assert params["page"] == "1"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_list_pr_files_with_whitespace():
|
||||
route = respx.get(f"{BASE}/repos/owner/repo/pulls/6/files").mock(
|
||||
return_value=httpx.Response(200, json=[])
|
||||
)
|
||||
list_pr_files("owner", "repo", 6, whitespace="ignore-all")
|
||||
params = dict(route.calls[0].request.url.params)
|
||||
assert params["whitespace"] == "ignore-all"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_list_pr_reviews():
|
||||
route = respx.get(f"{BASE}/repos/owner/repo/pulls/8/reviews").mock(
|
||||
return_value=httpx.Response(200, json=[{"id": 1, "state": "APPROVED"}])
|
||||
)
|
||||
result = list_pr_reviews("owner", "repo", 8)
|
||||
assert route.called
|
||||
assert result[0]["state"] == "APPROVED"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_create_pr_review_approved():
|
||||
route = respx.post(f"{BASE}/repos/owner/repo/pulls/9/reviews").mock(
|
||||
return_value=httpx.Response(200, json={"id": 5, "state": "APPROVED"})
|
||||
)
|
||||
result = create_pr_review("owner", "repo", 9, "APPROVED", body="Looks good!")
|
||||
assert route.called
|
||||
body = json.loads(route.calls[0].request.content)
|
||||
assert body["event"] == "APPROVED"
|
||||
assert body["body"] == "Looks good!"
|
||||
|
||||
|
||||
@respx.mock
|
||||
def test_create_pr_review_request_changes():
|
||||
route = respx.post(f"{BASE}/repos/owner/repo/pulls/10/reviews").mock(
|
||||
return_value=httpx.Response(200, json={"id": 6, "state": "REQUEST_CHANGES"})
|
||||
)
|
||||
create_pr_review("owner", "repo", 10, "REQUEST_CHANGES")
|
||||
body = json.loads(route.calls[0].request.content)
|
||||
assert body["event"] == "REQUEST_CHANGES"
|
||||
assert "body" not in body
|
||||
Reference in New Issue
Block a user