forked from cardosofelipe/fast-next-template
Implements the Git Operations MCP server (Issue #58) providing: Core features: - GitPython wrapper for local repository operations (clone, commit, push, pull, diff, log) - Branch management (create, delete, list, checkout) - Workspace isolation per project with file-based locking - Gitea provider for remote PR operations MCP Tools (17 registered): - clone_repository, git_status, create_branch, list_branches - checkout, commit, push, pull, diff, log - create_pull_request, get_pull_request, list_pull_requests - merge_pull_request, get_workspace, lock_workspace, unlock_workspace Technical details: - FastMCP + FastAPI with JSON-RPC 2.0 protocol - pydantic-settings for configuration (env prefix: GIT_OPS_) - Comprehensive error hierarchy with structured codes - 131 tests passing with 67% coverage - Async operations via ThreadPoolExecutor Closes: #105, #106, #107, #108, #109 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
300 lines
7.7 KiB
Python
300 lines
7.7 KiB
Python
"""
|
|
Test configuration and fixtures for Git Operations MCP Server.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from collections.abc import AsyncIterator, Iterator
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from git import Repo as GitRepo
|
|
|
|
# Set test environment
|
|
os.environ["IS_TEST"] = "true"
|
|
os.environ["GIT_OPS_WORKSPACE_BASE_PATH"] = "/tmp/test-workspaces"
|
|
os.environ["GIT_OPS_GITEA_BASE_URL"] = "https://gitea.test.com"
|
|
os.environ["GIT_OPS_GITEA_TOKEN"] = "test-token"
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def reset_settings_session():
|
|
"""Reset settings at start and end of test session."""
|
|
from config import reset_settings
|
|
|
|
reset_settings()
|
|
yield
|
|
reset_settings()
|
|
|
|
|
|
@pytest.fixture
|
|
def reset_settings():
|
|
"""Reset settings before each test that needs it."""
|
|
from config import reset_settings
|
|
|
|
reset_settings()
|
|
yield
|
|
reset_settings()
|
|
|
|
|
|
@pytest.fixture
|
|
def test_settings():
|
|
"""Get test settings."""
|
|
from config import Settings
|
|
|
|
return Settings(
|
|
workspace_base_path=Path("/tmp/test-workspaces"),
|
|
gitea_base_url="https://gitea.test.com",
|
|
gitea_token="test-token",
|
|
github_token="github-test-token",
|
|
git_author_name="Test Agent",
|
|
git_author_email="test@syndarix.ai",
|
|
enable_force_push=False,
|
|
debug=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir() -> Iterator[Path]:
|
|
"""Create a temporary directory for tests."""
|
|
temp_path = Path(tempfile.mkdtemp())
|
|
yield temp_path
|
|
if temp_path.exists():
|
|
shutil.rmtree(temp_path)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_workspace(temp_dir: Path) -> Path:
|
|
"""Create a temporary workspace directory."""
|
|
workspace = temp_dir / "workspace"
|
|
workspace.mkdir(parents=True, exist_ok=True)
|
|
return workspace
|
|
|
|
|
|
@pytest.fixture
|
|
def git_repo(temp_workspace: Path) -> GitRepo:
|
|
"""Create a git repository in the temp workspace."""
|
|
# Initialize with main branch (Git 2.28+)
|
|
repo = GitRepo.init(temp_workspace, initial_branch="main")
|
|
|
|
# Configure git
|
|
with repo.config_writer() as cw:
|
|
cw.set_value("user", "name", "Test User")
|
|
cw.set_value("user", "email", "test@example.com")
|
|
|
|
# Create initial commit
|
|
test_file = temp_workspace / "README.md"
|
|
test_file.write_text("# Test Repository\n")
|
|
repo.index.add(["README.md"])
|
|
repo.index.commit("Initial commit")
|
|
|
|
return repo
|
|
|
|
|
|
@pytest.fixture
|
|
def git_repo_with_remote(git_repo: GitRepo, temp_dir: Path) -> tuple[GitRepo, GitRepo]:
|
|
"""Create a git repository with a 'remote' (bare repo)."""
|
|
# Create bare repo as remote
|
|
remote_path = temp_dir / "remote.git"
|
|
remote_repo = GitRepo.init(remote_path, bare=True)
|
|
|
|
# Add remote to main repo
|
|
git_repo.create_remote("origin", str(remote_path))
|
|
|
|
# Push initial commit
|
|
git_repo.remotes.origin.push("main:main")
|
|
|
|
# Set up tracking
|
|
git_repo.heads.main.set_tracking_branch(git_repo.remotes.origin.refs.main)
|
|
|
|
return git_repo, remote_repo
|
|
|
|
|
|
@pytest.fixture
|
|
def workspace_manager(temp_dir: Path, test_settings):
|
|
"""Create a WorkspaceManager with test settings."""
|
|
from workspace import WorkspaceManager
|
|
|
|
test_settings.workspace_base_path = temp_dir / "workspaces"
|
|
return WorkspaceManager(test_settings)
|
|
|
|
|
|
@pytest.fixture
|
|
def git_wrapper(temp_workspace: Path, test_settings):
|
|
"""Create a GitWrapper for the temp workspace."""
|
|
from git_wrapper import GitWrapper
|
|
|
|
return GitWrapper(temp_workspace, test_settings)
|
|
|
|
|
|
@pytest.fixture
|
|
def git_wrapper_with_repo(git_repo: GitRepo, test_settings):
|
|
"""Create a GitWrapper for a repo that's already initialized."""
|
|
from git_wrapper import GitWrapper
|
|
|
|
return GitWrapper(Path(git_repo.working_dir), test_settings)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_gitea_provider():
|
|
"""Create a mock Gitea provider."""
|
|
provider = AsyncMock()
|
|
provider.name = "gitea"
|
|
provider.is_connected = AsyncMock(return_value=True)
|
|
provider.get_authenticated_user = AsyncMock(return_value="test-user")
|
|
provider.parse_repo_url = MagicMock(return_value=("owner", "repo"))
|
|
return provider
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_httpx_client():
|
|
"""Create a mock httpx client for provider tests."""
|
|
from unittest.mock import AsyncMock
|
|
|
|
mock_response = AsyncMock()
|
|
mock_response.status_code = 200
|
|
mock_response.json = MagicMock(return_value={})
|
|
mock_response.text = ""
|
|
|
|
mock_client = AsyncMock()
|
|
mock_client.request = AsyncMock(return_value=mock_response)
|
|
mock_client.get = AsyncMock(return_value=mock_response)
|
|
mock_client.post = AsyncMock(return_value=mock_response)
|
|
mock_client.patch = AsyncMock(return_value=mock_response)
|
|
mock_client.delete = AsyncMock(return_value=mock_response)
|
|
|
|
return mock_client
|
|
|
|
|
|
@pytest.fixture
|
|
async def gitea_provider(test_settings, mock_httpx_client):
|
|
"""Create a GiteaProvider with mocked HTTP client."""
|
|
from providers.gitea import GiteaProvider
|
|
|
|
provider = GiteaProvider(
|
|
base_url=test_settings.gitea_base_url,
|
|
token=test_settings.gitea_token,
|
|
settings=test_settings,
|
|
)
|
|
provider._client = mock_httpx_client
|
|
|
|
yield provider
|
|
|
|
await provider.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_pr_data():
|
|
"""Sample PR data from Gitea API."""
|
|
return {
|
|
"number": 42,
|
|
"title": "Test PR",
|
|
"body": "This is a test pull request",
|
|
"state": "open",
|
|
"head": {"ref": "feature-branch"},
|
|
"base": {"ref": "main"},
|
|
"user": {"login": "test-user"},
|
|
"created_at": "2024-01-15T10:00:00Z",
|
|
"updated_at": "2024-01-15T12:00:00Z",
|
|
"merged_at": None,
|
|
"closed_at": None,
|
|
"html_url": "https://gitea.test.com/owner/repo/pull/42",
|
|
"labels": [{"name": "enhancement"}],
|
|
"assignees": [{"login": "assignee1"}],
|
|
"requested_reviewers": [{"login": "reviewer1"}],
|
|
"mergeable": True,
|
|
"draft": False,
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_commit_data():
|
|
"""Sample commit data."""
|
|
return {
|
|
"sha": "abc123def456",
|
|
"short_sha": "abc123d",
|
|
"message": "Test commit message",
|
|
"author": {
|
|
"name": "Test Author",
|
|
"email": "author@test.com",
|
|
"date": "2024-01-15T10:00:00Z",
|
|
},
|
|
"committer": {
|
|
"name": "Test Committer",
|
|
"email": "committer@test.com",
|
|
"date": "2024-01-15T10:00:00Z",
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_fastapi_app():
|
|
"""Create a test FastAPI app."""
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
app = FastAPI()
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return {"status": "healthy"}
|
|
|
|
return TestClient(app)
|
|
|
|
|
|
# Async fixtures
|
|
|
|
|
|
@pytest.fixture
|
|
async def async_workspace_manager(
|
|
temp_dir: Path, test_settings
|
|
) -> AsyncIterator:
|
|
"""Async fixture for workspace manager."""
|
|
from workspace import WorkspaceManager
|
|
|
|
test_settings.workspace_base_path = temp_dir / "workspaces"
|
|
manager = WorkspaceManager(test_settings)
|
|
yield manager
|
|
|
|
|
|
# Test data fixtures
|
|
|
|
|
|
@pytest.fixture
|
|
def valid_project_id() -> str:
|
|
"""Valid project ID for tests."""
|
|
return "test-project-123"
|
|
|
|
|
|
@pytest.fixture
|
|
def valid_agent_id() -> str:
|
|
"""Valid agent ID for tests."""
|
|
return "agent-456"
|
|
|
|
|
|
@pytest.fixture
|
|
def invalid_ids() -> list[str]:
|
|
"""Invalid IDs for validation tests."""
|
|
return [
|
|
"",
|
|
" ",
|
|
"a" * 200, # Too long
|
|
"test@invalid", # Invalid character
|
|
"test!invalid",
|
|
"../path/traversal",
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_repo_url() -> str:
|
|
"""Sample repository URL."""
|
|
return "https://gitea.test.com/owner/repo.git"
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_ssh_repo_url() -> str:
|
|
"""Sample SSH repository URL."""
|
|
return "git@gitea.test.com:owner/repo.git"
|