Introduce a GiteaOAuthValidator for JWT and userinfo validation and fallbacks, add /oauth/token proxy, and thread per-user tokens through the request context and automation paths. Update config and .env.example for OAuth-first mode, add OpenAPI, extensive unit/integration tests, GitHub/Gitea CI workflows, docs, and lint/test enforcement (>=80% cov).
129 lines
4.3 KiB
Python
129 lines
4.3 KiB
Python
"""Tests for repository-focused tool handlers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from aegis_gitea_mcp.config import reset_settings
|
|
from aegis_gitea_mcp.gitea_client import GiteaError
|
|
from aegis_gitea_mcp.tools.repository import (
|
|
get_file_contents_tool,
|
|
get_file_tree_tool,
|
|
get_repository_info_tool,
|
|
list_repositories_tool,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def repository_tool_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Provide minimal settings needed by response limit helpers."""
|
|
reset_settings()
|
|
monkeypatch.setenv("GITEA_URL", "https://gitea.example.com")
|
|
monkeypatch.setenv("GITEA_TOKEN", "legacy-token")
|
|
monkeypatch.setenv("MCP_API_KEYS", "a" * 64)
|
|
monkeypatch.setenv("ENVIRONMENT", "test")
|
|
yield
|
|
reset_settings()
|
|
|
|
|
|
class RepoStub:
|
|
"""Stub Gitea client for repository tools."""
|
|
|
|
async def list_repositories(self):
|
|
return [{"name": "demo", "owner": {"login": "acme"}, "full_name": "acme/demo"}]
|
|
|
|
async def get_repository(self, owner, repo):
|
|
return {"name": repo, "owner": {"login": owner}, "full_name": f"{owner}/{repo}"}
|
|
|
|
async def get_tree(self, owner, repo, ref, recursive):
|
|
return {"tree": [{"path": "README.md", "type": "blob", "size": 11, "sha": "abc"}]}
|
|
|
|
async def get_file_contents(self, owner, repo, filepath, ref):
|
|
return {
|
|
"content": "SGVsbG8gV29ybGQ=",
|
|
"encoding": "base64",
|
|
"size": 11,
|
|
"sha": "abc",
|
|
"html_url": f"https://example/{owner}/{repo}/{filepath}",
|
|
}
|
|
|
|
|
|
class RepoErrorStub(RepoStub):
|
|
"""Stub that raises backend errors."""
|
|
|
|
async def list_repositories(self):
|
|
raise GiteaError("backend down")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_repositories_tool_success() -> None:
|
|
"""Repository listing tool normalizes output shape."""
|
|
result = await list_repositories_tool(RepoStub(), {})
|
|
assert result["count"] == 1
|
|
assert result["repositories"][0]["owner"] == "acme"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_repositories_tool_failure_mode() -> None:
|
|
"""Repository listing tool wraps backend errors."""
|
|
with pytest.raises(RuntimeError, match="Failed to list repositories"):
|
|
await list_repositories_tool(RepoErrorStub(), {})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_repository_info_tool_success() -> None:
|
|
"""Repository info tool returns normalized metadata."""
|
|
result = await get_repository_info_tool(RepoStub(), {"owner": "acme", "repo": "demo"})
|
|
assert result["full_name"] == "acme/demo"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_file_tree_tool_success() -> None:
|
|
"""File tree tool returns bounded tree entries."""
|
|
result = await get_file_tree_tool(
|
|
RepoStub(),
|
|
{"owner": "acme", "repo": "demo", "ref": "main", "recursive": False},
|
|
)
|
|
assert result["count"] == 1
|
|
assert result["tree"][0]["path"] == "README.md"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_file_contents_tool_decodes_base64() -> None:
|
|
"""File contents tool decodes UTF-8 base64 payloads."""
|
|
result = await get_file_contents_tool(
|
|
RepoStub(),
|
|
{"owner": "acme", "repo": "demo", "filepath": "README.md", "ref": "main"},
|
|
)
|
|
assert result["content"] == "Hello World"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_file_contents_tool_handles_invalid_base64() -> None:
|
|
"""Invalid base64 payloads are returned safely without crashing."""
|
|
|
|
class InvalidBase64Stub(RepoStub):
|
|
async def get_file_contents(self, owner, repo, filepath, ref):
|
|
return {"content": "%%%not-base64%%%", "encoding": "base64", "size": 4, "sha": "abc"}
|
|
|
|
result = await get_file_contents_tool(
|
|
InvalidBase64Stub(),
|
|
{"owner": "acme", "repo": "demo", "filepath": "README.md", "ref": "main"},
|
|
)
|
|
assert result["content"] == "%%%not-base64%%%"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_file_contents_tool_failure_mode() -> None:
|
|
"""File contents tool wraps backend failures."""
|
|
|
|
class ErrorFileStub(RepoStub):
|
|
async def get_file_contents(self, owner, repo, filepath, ref):
|
|
raise GiteaError("boom")
|
|
|
|
with pytest.raises(RuntimeError, match="Failed to get file contents"):
|
|
await get_file_contents_tool(
|
|
ErrorFileStub(),
|
|
{"owner": "acme", "repo": "demo", "filepath": "README.md", "ref": "main"},
|
|
)
|