"""Tests for pull request tools."""
from unittest.mock import patch, MagicMock
import pytest
from bitbucket_mcp.tools import pull_requests as _mod
create_pull_request = _mod.create_pull_request.fn
list_pull_requests = _mod.list_pull_requests.fn
get_pull_request = _mod.get_pull_request.fn
update_pull_request = _mod.update_pull_request.fn
approve_pull_request = _mod.approve_pull_request.fn
unapprove_pull_request = _mod.unapprove_pull_request.fn
merge_pull_request = _mod.merge_pull_request.fn
decline_pull_request = _mod.decline_pull_request.fn
add_pull_request_comment = _mod.add_pull_request_comment.fn
get_pull_request_comments = _mod.get_pull_request_comments.fn
get_pull_request_diff = _mod.get_pull_request_diff.fn
get_pull_request_diffstat = _mod.get_pull_request_diffstat.fn
reply_to_comment = _mod.reply_to_comment.fn
add_inline_comment = _mod.add_inline_comment.fn
delete_comment = _mod.delete_comment.fn
add_reviewer = _mod.add_reviewer.fn
remove_reviewer = _mod.remove_reviewer.fn
get_pull_request_merge_status = _mod.get_pull_request_merge_status.fn
from tests.conftest import make_mock_response, make_paginated_response
class TestListPullRequests:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.get.return_value = make_paginated_response([
{
"id": 1, "title": "Test PR", "state": "OPEN",
"author": {"display_name": "Dev"},
"source": {"branch": {"name": "feature"}},
"destination": {"branch": {"name": "main"}},
"links": {"html": {"href": "https://bb.org/pr/1"}},
"created_on": "2025-01-01", "updated_on": "2025-01-02"
}
])
result = list_pull_requests("my-repo")
assert result["success"] is True
assert len(result["pull_requests"]) == 1
assert result["pull_requests"][0]["title"] == "Test PR"
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_auth_failure(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.get.return_value = make_mock_response(401)
result = list_pull_requests("my-repo")
assert result["success"] is False
assert "Authentication failed" in result["error"]
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_repo_not_found(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.get.return_value = make_mock_response(404)
result = list_pull_requests("nonexistent")
assert result["success"] is False
assert "not found" in result["error"]
def test_no_credentials(self):
result = list_pull_requests("my-repo")
assert result["success"] is False
assert "No" in result["error"]
class TestCreatePullRequest:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
# Mock default reviewers + create PR
client.get.return_value = make_paginated_response([])
client.post.return_value = make_mock_response(201, {
"id": 42, "title": "New PR", "state": "OPEN",
"links": {"html": {"href": "https://bb.org/pr/42"}},
"reviewers": []
})
result = create_pull_request("repo", "New PR", "feature", "main")
assert result["success"] is True
assert result["id"] == 42
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_bad_request(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.get.return_value = make_paginated_response([])
client.post.return_value = make_mock_response(400, {
"error": {"message": "Branch not found"}
})
result = create_pull_request("repo", "PR", "nonexistent", "main")
assert result["success"] is False
assert "Branch not found" in result["error"]
class TestGetPullRequest:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.get.return_value = make_mock_response(200, {
"id": 1, "title": "Test", "description": "desc",
"state": "OPEN",
"author": {"display_name": "Dev"},
"source": {"branch": {"name": "feature"}},
"destination": {"branch": {"name": "main"}},
"links": {"html": {"href": "https://bb.org/pr/1"}},
"created_on": "2025-01-01", "updated_on": "2025-01-02",
"close_source_branch": False,
"comment_count": 3, "task_count": 1,
"reviewers": [], "participants": [],
"merge_commit": None
})
result = get_pull_request("repo", 1)
assert result["success"] is True
assert result["title"] == "Test"
assert result["comment_count"] == 3
class TestApprovePullRequest:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.post.return_value = make_mock_response(200, {
"approved": True,
"user": {"display_name": "Reviewer"}
})
result = approve_pull_request("repo", 1)
assert result["success"] is True
assert "approved" in result["message"]
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_already_approved(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.post.return_value = make_mock_response(409)
result = approve_pull_request("repo", 1)
assert result["success"] is False
assert "already approved" in result["error"]
class TestMergePullRequest:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.post.return_value = make_mock_response(200, {
"merge_commit": {"hash": "abc123"},
"state": "MERGED"
})
result = merge_pull_request("repo", 1, merge_strategy="squash")
assert result["success"] is True
assert result["merge_commit"] == "abc123"
def test_invalid_strategy(self, saved_config):
result = merge_pull_request("repo", 1, merge_strategy="invalid")
assert result["success"] is False
assert "Invalid merge strategy" in result["error"]
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_merge_conflict(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.post.return_value = make_mock_response(409, {
"error": {"message": "Merge conflict"}
})
result = merge_pull_request("repo", 1)
assert result["success"] is False
assert "Merge conflict" in result["error"]
class TestGetPullRequestDiffstat:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.get.return_value = make_mock_response(200, {
"values": [
{
"old": {"path": "file.py"},
"new": {"path": "file.py"},
"status": "modified",
"lines_added": 10,
"lines_removed": 5,
}
],
"next": None,
})
result = get_pull_request_diffstat("repo", 1)
assert result["success"] is True
assert result["total_files"] == 1
assert result["total_lines_added"] == 10
assert result["total_lines_removed"] == 5
class TestGetPullRequestDiff:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
resp = make_mock_response(200)
resp.text = "--- a/file.py\n+++ b/file.py\n@@ -1 +1 @@\n-old\n+new"
client.get.return_value = resp
result = get_pull_request_diff("repo", 1)
assert result["success"] is True
assert "---" in result["diff"]
assert result["truncated"] is False
class TestAddPullRequestComment:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.post.return_value = make_mock_response(201, {
"id": 10,
"content": {"raw": "LGTM"},
"user": {"display_name": "Dev"},
"created_on": "2025-01-01"
})
result = add_pull_request_comment("repo", 1, "LGTM")
assert result["success"] is True
assert result["id"] == 10
class TestReplyToComment:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.post.return_value = make_mock_response(201, {
"id": 11,
"content": {"raw": "Thanks!"},
"user": {"display_name": "Dev"},
"created_on": "2025-01-01"
})
result = reply_to_comment("repo", 1, 10, "Thanks!")
assert result["success"] is True
assert result["parent_id"] == 10
class TestDeleteComment:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.delete.return_value = make_mock_response(204)
result = delete_comment("repo", 1, 10)
assert result["success"] is True
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_permission_denied(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.delete.return_value = make_mock_response(403)
result = delete_comment("repo", 1, 10)
assert result["success"] is False
assert "Permission denied" in result["error"]
class TestUpdatePullRequest:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_success(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.put.return_value = make_mock_response(200, {
"id": 1, "title": "Updated",
"description": "", "state": "OPEN",
"source": {"branch": {"name": "feature"}},
"destination": {"branch": {"name": "main"}},
"links": {"html": {"href": "https://bb.org/pr/1"}},
"close_source_branch": False,
"reviewers": [], "updated_on": "2025-01-02"
})
result = update_pull_request("repo", 1, title="Updated")
assert result["success"] is True
assert result["title"] == "Updated"
def test_no_fields(self, saved_config):
result = update_pull_request("repo", 1)
assert result["success"] is False
assert "No update fields" in result["error"]
class TestGetPullRequestMergeStatus:
@patch("bitbucket_mcp.tools.pull_requests.httpx.Client")
def test_can_merge(self, MockClient, saved_config):
client = MagicMock()
MockClient.return_value.__enter__ = MagicMock(return_value=client)
MockClient.return_value.__exit__ = MagicMock(return_value=False)
client.get.side_effect = [
# PR data
make_mock_response(200, {
"state": "OPEN",
"participants": [
{"user": {"display_name": "Rev"}, "approved": True, "state": "approved"}
],
"source": {"branch": {"name": "feature"}},
"destination": {"branch": {"name": "main"}},
}),
# Diffstat
make_mock_response(200, {"values": []}),
]
result = get_pull_request_merge_status("repo", 1)
assert result["success"] is True
assert result["can_merge"] is True
assert result["approval_count"] == 1