"""Tests for pull request merge tools.""" import json import pytest from unittest.mock import MagicMock, patch, AsyncMock from gitea_mcp.gitea_client import GiteaClient from gitea_mcp.tools.pull_requests import PullRequestTools @pytest.fixture def mock_client(): """Create a mock GiteaClient.""" client = MagicMock(spec=GiteaClient) client.config = {"repo": "owner/test-repo", "mode": "project"} return client @pytest.fixture def pr_tools(mock_client): """Create PullRequestTools with mock client.""" tools = PullRequestTools(mock_client) return tools class TestMergePullRequest: """Tests for merge_pull_request.""" def test_merge_default_style(self, mock_client): """Default merge style is 'merge'.""" mock_client.merge_pull_request.return_value = { "status": "merged", "pr_number": 1, "style": "merge" } result = mock_client.merge_pull_request(1) mock_client.merge_pull_request.assert_called_once_with(1) assert result["status"] == "merged" assert result["style"] == "merge" def test_merge_squash(self, mock_client): """Squash merge passes correct Do value.""" mock_client.merge_pull_request.return_value = { "status": "merged", "pr_number": 1, "style": "squash" } result = mock_client.merge_pull_request(1, merge_style="squash") assert result["style"] == "squash" def test_merge_rebase(self, mock_client): """Rebase merge passes correct Do value.""" mock_client.merge_pull_request.return_value = { "status": "merged", "pr_number": 1, "style": "rebase" } result = mock_client.merge_pull_request(1, merge_style="rebase") assert result["style"] == "rebase" def test_merge_rebase_merge(self, mock_client): """Rebase-merge strategy.""" mock_client.merge_pull_request.return_value = { "status": "merged", "pr_number": 1, "style": "rebase-merge" } result = mock_client.merge_pull_request(1, merge_style="rebase-merge") assert result["style"] == "rebase-merge" def test_merge_fast_forward_only(self, mock_client): """Fast-forward-only strategy.""" mock_client.merge_pull_request.return_value = { "status": "merged", "pr_number": 1, "style": "fast-forward-only" } result = mock_client.merge_pull_request(1, merge_style="fast-forward-only") assert result["style"] == "fast-forward-only" def test_merge_with_custom_message(self, mock_client): """Custom title and message are passed.""" mock_client.merge_pull_request.return_value = { "status": "merged", "pr_number": 5, "style": "merge" } mock_client.merge_pull_request( 5, title="Release v2.0", message="Merge feature branch" ) mock_client.merge_pull_request.assert_called_once_with( 5, title="Release v2.0", message="Merge feature branch" ) def test_merge_delete_branch(self, mock_client): """delete_branch flag is passed.""" mock_client.merge_pull_request.return_value = { "status": "merged", "pr_number": 3, "style": "merge" } mock_client.merge_pull_request(3, delete_branch=True) mock_client.merge_pull_request.assert_called_once_with(3, delete_branch=True) def test_merge_force(self, mock_client): """force_merge flag is passed.""" mock_client.merge_pull_request.return_value = { "status": "merged", "pr_number": 3, "style": "merge" } mock_client.merge_pull_request(3, force_merge=True) mock_client.merge_pull_request.assert_called_once_with(3, force_merge=True) def test_merge_invalid_style(self): """Invalid merge style raises ValueError.""" valid_styles = {"merge", "rebase", "rebase-merge", "squash", "fast-forward-only"} style = "invalid" with pytest.raises(ValueError, match="Invalid merge_style"): if style not in valid_styles: raise ValueError( f"Invalid merge_style '{style}'. Must be one of: {', '.join(sorted(valid_styles))}" ) class TestGetPrMergeStatus: """Tests for get_pr_merge_status.""" def test_mergeable_pr(self, mock_client): """Returns mergeable status for open PR.""" mock_client.get_pull_request.return_value = { "mergeable": True, "state": "open", "merged": False, "merge_base": "abc123" } mock_client.get_pr_merge_status.return_value = { "pr_number": 1, "mergeable": True, "state": "open", "merged": False, "merge_base": "abc123" } result = mock_client.get_pr_merge_status(1) assert result["mergeable"] is True assert result["merged"] is False def test_already_merged_pr(self, mock_client): """Returns merged status for already-merged PR.""" mock_client.get_pr_merge_status.return_value = { "pr_number": 2, "mergeable": False, "state": "closed", "merged": True, "merge_base": "def456" } result = mock_client.get_pr_merge_status(2) assert result["merged"] is True def test_unmergeable_pr(self, mock_client): """Returns mergeable=False for PR with conflicts.""" mock_client.get_pr_merge_status.return_value = { "pr_number": 3, "mergeable": False, "state": "open", "merged": False, "merge_base": "ghi789" } result = mock_client.get_pr_merge_status(3) assert result["mergeable"] is False class TestCancelAutoMerge: """Tests for cancel_auto_merge.""" def test_cancel_success(self, mock_client): """Successful cancellation returns status.""" mock_client.cancel_auto_merge.return_value = { "status": "cancelled", "pr_number": 1 } result = mock_client.cancel_auto_merge(1) assert result["status"] == "cancelled" def test_cancel_multiple_prs(self, mock_client): """Can cancel multiple PRs.""" results = [] for pr_num in [1, 2, 3]: mock_client.cancel_auto_merge.return_value = { "status": "cancelled", "pr_number": pr_num } result = mock_client.cancel_auto_merge(pr_num) results.append(result) assert len(results) == 3 assert all(r["status"] == "cancelled" for r in results) class TestTypeCoercion: """Tests for boolean coercion in _coerce_types.""" def test_bool_string_true_variations(self): """Boolean string variations for true.""" from gitea_mcp.tool_registry import _coerce_types for val in ["true", "True", "TRUE"]: result = _coerce_types({"force_merge": val}) assert result["force_merge"] is True for val in ["yes", "1"]: result = _coerce_types({"force_merge": val}) assert result["force_merge"] is True def test_bool_string_false_variations(self): """Boolean string variations for false.""" from gitea_mcp.tool_registry import _coerce_types for val in ["false", "False", "FALSE"]: result = _coerce_types({"delete_branch": val}) assert result["delete_branch"] is False for val in ["no", "0"]: result = _coerce_types({"delete_branch": val}) assert result["delete_branch"] is False def test_bool_actual_values(self): """Actual booleans pass through unchanged.""" from gitea_mcp.tool_registry import _coerce_types result = _coerce_types({"force_merge": True, "delete_branch": False}) assert result["force_merge"] is True assert result["delete_branch"] is False def test_mixed_types(self): """Mixed types are coerced correctly.""" from gitea_mcp.tool_registry import _coerce_types result = _coerce_types({ "pr_number": "42", "force_merge": "true", "delete_branch": False, "merge_style": "squash" }) assert result["pr_number"] == 42 assert result["force_merge"] is True assert result["delete_branch"] is False assert result["merge_style"] == "squash"