feat: harden gateway with policy engine, secure tools, and governance docs

This commit is contained in:
2026-02-14 16:05:56 +01:00
parent e17d34e6d7
commit 5969892af3
55 changed files with 4711 additions and 1587 deletions

View File

@@ -0,0 +1,177 @@
"""Tests for expanded read/write MCP tool handlers."""
import pytest
from aegis_gitea_mcp.config import reset_settings
from aegis_gitea_mcp.gitea_client import GiteaError
from aegis_gitea_mcp.tools.read_tools import (
compare_refs_tool,
get_commit_diff_tool,
get_issue_tool,
get_pull_request_tool,
list_commits_tool,
list_issues_tool,
list_labels_tool,
list_pull_requests_tool,
list_releases_tool,
list_tags_tool,
search_code_tool,
)
from aegis_gitea_mcp.tools.write_tools import (
add_labels_tool,
assign_issue_tool,
create_issue_comment_tool,
create_issue_tool,
create_pr_comment_tool,
update_issue_tool,
)
@pytest.fixture(autouse=True)
def tool_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Provide minimal settings environment for response limit helpers."""
reset_settings()
monkeypatch.setenv("GITEA_URL", "https://gitea.example.com")
monkeypatch.setenv("GITEA_TOKEN", "test-token")
monkeypatch.setenv("MCP_API_KEYS", "a" * 64)
monkeypatch.setenv("ENVIRONMENT", "test")
class StubGitea:
"""Stubbed Gitea client for tool unit tests."""
async def search_code(self, owner, repo, query, *, ref, page, limit):
return {"hits": [{"path": "src/main.py", "snippet": "match text", "score": 1.0}]}
async def list_commits(self, owner, repo, *, ref, page, limit):
return [{"sha": "abc1234", "commit": {"message": "Fix bug", "author": {"date": "now"}}}]
async def get_commit_diff(self, owner, repo, sha):
return {
"commit": {"message": "Fix bug"},
"files": [{"filename": "a.py", "status": "modified"}],
}
async def compare_refs(self, owner, repo, base, head):
return {
"commits": [{"sha": "abc", "commit": {"message": "Msg"}}],
"files": [{"filename": "a.py", "status": "modified"}],
}
async def list_issues(self, owner, repo, *, state, page, limit, labels=None):
return [{"number": 1, "title": "Issue", "state": "open", "labels": []}]
async def get_issue(self, owner, repo, index):
return {"number": index, "title": "Issue", "body": "Body", "state": "open", "labels": []}
async def list_pull_requests(self, owner, repo, *, state, page, limit):
return [{"number": 1, "title": "PR", "state": "open"}]
async def get_pull_request(self, owner, repo, index):
return {"number": index, "title": "PR", "body": "Body", "state": "open"}
async def list_labels(self, owner, repo, *, page, limit):
return [{"id": 1, "name": "bug", "color": "ff0000", "description": "desc"}]
async def list_tags(self, owner, repo, *, page, limit):
return [{"name": "v1.0.0", "commit": {"sha": "abc"}}]
async def list_releases(self, owner, repo, *, page, limit):
return [{"id": 1, "tag_name": "v1.0.0", "name": "release"}]
async def create_issue(self, owner, repo, *, title, body, labels=None, assignees=None):
return {"number": 1, "title": title, "state": "open"}
async def update_issue(self, owner, repo, index, *, title=None, body=None, state=None):
return {"number": index, "title": title or "Issue", "state": state or "open"}
async def create_issue_comment(self, owner, repo, index, body):
return {"id": 1, "body": body}
async def create_pr_comment(self, owner, repo, index, body):
return {"id": 2, "body": body}
async def add_labels(self, owner, repo, index, labels):
return {"labels": [{"name": label} for label in labels]}
async def assign_issue(self, owner, repo, index, assignees):
return {"assignees": [{"login": user} for user in assignees]}
class ErrorGitea(StubGitea):
"""Stub that raises backend errors for failure-mode coverage."""
async def list_commits(self, owner, repo, *, ref, page, limit):
raise GiteaError("backend failure")
@pytest.mark.asyncio
@pytest.mark.parametrize(
"tool,args,expected_key",
[
(search_code_tool, {"owner": "acme", "repo": "app", "query": "foo"}, "results"),
(list_commits_tool, {"owner": "acme", "repo": "app"}, "commits"),
(get_commit_diff_tool, {"owner": "acme", "repo": "app", "sha": "abc1234"}, "files"),
(
compare_refs_tool,
{"owner": "acme", "repo": "app", "base": "main", "head": "feature"},
"commits",
),
(list_issues_tool, {"owner": "acme", "repo": "app"}, "issues"),
(get_issue_tool, {"owner": "acme", "repo": "app", "issue_number": 1}, "title"),
(list_pull_requests_tool, {"owner": "acme", "repo": "app"}, "pull_requests"),
(get_pull_request_tool, {"owner": "acme", "repo": "app", "pull_number": 1}, "title"),
(list_labels_tool, {"owner": "acme", "repo": "app"}, "labels"),
(list_tags_tool, {"owner": "acme", "repo": "app"}, "tags"),
(list_releases_tool, {"owner": "acme", "repo": "app"}, "releases"),
],
)
async def test_extended_read_tools_success(tool, args, expected_key):
"""Each expanded read tool should return expected top-level keys."""
result = await tool(StubGitea(), args)
assert expected_key in result
@pytest.mark.asyncio
async def test_extended_read_tools_failure_mode() -> None:
"""Expanded read tools should wrap backend failures."""
with pytest.raises(RuntimeError):
await list_commits_tool(ErrorGitea(), {"owner": "acme", "repo": "app"})
@pytest.mark.asyncio
@pytest.mark.parametrize(
"tool,args,expected_key",
[
(create_issue_tool, {"owner": "acme", "repo": "app", "title": "Issue"}, "number"),
(
update_issue_tool,
{"owner": "acme", "repo": "app", "issue_number": 1, "title": "Updated"},
"number",
),
(
create_issue_comment_tool,
{"owner": "acme", "repo": "app", "issue_number": 1, "body": "comment"},
"id",
),
(
create_pr_comment_tool,
{"owner": "acme", "repo": "app", "pull_number": 1, "body": "comment"},
"id",
),
(
add_labels_tool,
{"owner": "acme", "repo": "app", "issue_number": 1, "labels": ["bug"]},
"labels",
),
(
assign_issue_tool,
{"owner": "acme", "repo": "app", "issue_number": 1, "assignees": ["alice"]},
"assignees",
),
],
)
async def test_write_tools_success(tool, args, expected_key):
"""Write tools should normalize successful backend responses."""
result = await tool(StubGitea(), args)
assert expected_key in result