db5a43b19a
Relocate the script into the skill (skills/gitea/scripts/gitea_mcp.py) and add a PEP 723 dependency block, so `uv run scripts/gitea_mcp.py` provisions deps with no install step and the skill folder is portable (drop into ~/.cline/skills/). Drop the non-standard `metadata` frontmatter — Cline only recognizes name + description. - .mcp.json points at the relocated script and runs with --no-project (PEP 723) - SKILL.md uses relative scripts/ paths; metadata block removed - find_config walks up the tree to locate config.json (skill root or plugin root) - tests import gitea_mcp via pytest pythonpath; README/CLAUDE paths updated Tests: 119 passed, 92% coverage. Standalone list-tools verified via uv --no-project.
152 lines
4.5 KiB
Python
152 lines
4.5 KiB
Python
import json
|
|
import pytest
|
|
import httpx
|
|
import respx
|
|
from gitea_mcp import (
|
|
list_my_repos, search_repos, get_repo, create_repo, create_org_repo,
|
|
update_repo, delete_repo, fork_repo, set_repo_topics,
|
|
)
|
|
|
|
BASE = "https://gitea.test/api/v1"
|
|
|
|
|
|
@respx.mock
|
|
def test_list_my_repos_defaults():
|
|
route = respx.get(f"{BASE}/user/repos").mock(return_value=httpx.Response(200, json=[]))
|
|
list_my_repos()
|
|
assert route.called
|
|
params = dict(route.calls[0].request.url.params)
|
|
assert params["limit"] == "50"
|
|
assert params["page"] == "1"
|
|
|
|
|
|
@respx.mock
|
|
def test_list_my_repos_custom_page():
|
|
route = respx.get(f"{BASE}/user/repos").mock(return_value=httpx.Response(200, json=[]))
|
|
list_my_repos(limit=10, page=3)
|
|
assert route.called
|
|
params = dict(route.calls[0].request.url.params)
|
|
assert params["limit"] == "10"
|
|
assert params["page"] == "3"
|
|
|
|
|
|
@respx.mock
|
|
def test_search_repos():
|
|
route = respx.get(f"{BASE}/repos/search").mock(
|
|
return_value=httpx.Response(200, json={"data": []})
|
|
)
|
|
result = search_repos("myquery")
|
|
assert route.called
|
|
params = dict(route.calls[0].request.url.params)
|
|
assert params["q"] == "myquery"
|
|
assert params["limit"] == "20"
|
|
|
|
|
|
@respx.mock
|
|
def test_get_repo():
|
|
route = respx.get(f"{BASE}/repos/owner/myrepo").mock(
|
|
return_value=httpx.Response(200, json={"name": "myrepo", "full_name": "owner/myrepo"})
|
|
)
|
|
result = get_repo("owner", "myrepo")
|
|
assert route.called
|
|
assert result["name"] == "myrepo"
|
|
|
|
|
|
@respx.mock
|
|
def test_get_repo_not_found():
|
|
respx.get(f"{BASE}/repos/owner/missing").mock(
|
|
return_value=httpx.Response(404, json={"message": "not found"})
|
|
)
|
|
with pytest.raises(RuntimeError, match="404"):
|
|
get_repo("owner", "missing")
|
|
|
|
|
|
@respx.mock
|
|
def test_create_repo_defaults():
|
|
route = respx.post(f"{BASE}/user/repos").mock(
|
|
return_value=httpx.Response(201, json={"name": "newrepo"})
|
|
)
|
|
result = create_repo("newrepo")
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["name"] == "newrepo"
|
|
assert body["private"] is True
|
|
assert body["auto_init"] is True
|
|
assert "description" not in body
|
|
|
|
|
|
@respx.mock
|
|
def test_create_repo_with_options():
|
|
route = respx.post(f"{BASE}/user/repos").mock(
|
|
return_value=httpx.Response(201, json={"name": "pub"})
|
|
)
|
|
create_repo("pub", description="A public repo", private=False, gitignores="Python")
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["description"] == "A public repo"
|
|
assert body["private"] is False
|
|
assert body["gitignores"] == "Python"
|
|
|
|
|
|
@respx.mock
|
|
def test_create_org_repo():
|
|
route = respx.post(f"{BASE}/orgs/myorg/repos").mock(
|
|
return_value=httpx.Response(201, json={"name": "orgrepo"})
|
|
)
|
|
result = create_org_repo("myorg", "orgrepo")
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["name"] == "orgrepo"
|
|
|
|
|
|
@respx.mock
|
|
def test_update_repo():
|
|
route = respx.patch(f"{BASE}/repos/owner/repo").mock(
|
|
return_value=httpx.Response(200, json={"name": "repo"})
|
|
)
|
|
update_repo("owner", "repo", description="updated")
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["description"] == "updated"
|
|
|
|
|
|
@respx.mock
|
|
def test_delete_repo():
|
|
route = respx.delete(f"{BASE}/repos/owner/repo").mock(
|
|
return_value=httpx.Response(204)
|
|
)
|
|
result = delete_repo("owner", "repo")
|
|
assert route.called
|
|
assert result == {"success": True}
|
|
|
|
|
|
@respx.mock
|
|
def test_fork_repo():
|
|
route = respx.post(f"{BASE}/repos/upstream/repo/forks").mock(
|
|
return_value=httpx.Response(202, json={"name": "repo"})
|
|
)
|
|
fork_repo("upstream", "repo")
|
|
assert route.called
|
|
|
|
|
|
@respx.mock
|
|
def test_fork_repo_to_org():
|
|
route = respx.post(f"{BASE}/repos/upstream/repo/forks").mock(
|
|
return_value=httpx.Response(202, json={"name": "myfork"})
|
|
)
|
|
fork_repo("upstream", "repo", organization="myorg", new_repo_name="myfork")
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["organization"] == "myorg"
|
|
assert body["name"] == "myfork"
|
|
|
|
|
|
@respx.mock
|
|
def test_set_repo_topics():
|
|
route = respx.put(f"{BASE}/repos/owner/repo/topics").mock(
|
|
return_value=httpx.Response(204)
|
|
)
|
|
result = set_repo_topics("owner", "repo", ["python", "mcp"])
|
|
assert route.called
|
|
body = json.loads(route.calls[0].request.content)
|
|
assert body["topics"] == ["python", "mcp"]
|
|
assert result == {"success": True}
|