"""Tests for Jira development information operations."""
from unittest.mock import MagicMock, Mock
import pytest
from requests.exceptions import HTTPError
from mcp_atlassian.jira.development import DevelopmentMixin
class TestDevelopmentMixin:
@pytest.fixture
def development_mixin(self, mock_config, mock_atlassian_jira):
mixin = DevelopmentMixin(config=mock_config)
mixin.jira = mock_atlassian_jira
return mixin
@pytest.fixture
def mock_dev_status_response(self):
"""Mock response from dev-status API with PRs."""
return {
"detail": [
{
"_instance": {
"name": "Bitbucket Server",
"baseUrl": "https://stash.example.com",
},
"pullRequests": [
{
"id": "123",
"name": "[TEST-123] Fix bug in login",
"status": "MERGED",
"url": "https://stash.example.com/projects/PROJ/repos/app/pull-requests/123",
"source": {
"branch": "bugfix/TEST-123-login-fix",
"repository": {
"name": "app",
"url": "https://stash.example.com/projects/PROJ/repos/app",
},
},
"destination": {"branch": "develop"},
"author": {"name": "John Doe"},
"reviewers": [{"name": "Jane Smith"}],
"lastUpdate": "2024-01-15T10:30:00.000Z",
}
],
"branches": [
{
"name": "bugfix/TEST-123-login-fix",
"url": "https://stash.example.com/projects/PROJ/repos/app/browse?at=bugfix/TEST-123-login-fix",
"createPullRequestUrl": "https://stash.example.com/projects/PROJ/repos/app/pull-requests?create&sourceBranch=bugfix/TEST-123-login-fix",
}
],
"repositories": [],
}
]
}
@pytest.fixture
def mock_dev_status_with_commits(self):
"""Mock response with commits in repositories."""
return {
"detail": [
{
"_instance": {
"name": "Bitbucket Server",
"baseUrl": "https://stash.example.com",
},
"pullRequests": [],
"branches": [],
"repositories": [
{
"name": "app",
"url": "https://stash.example.com/projects/PROJ/repos/app",
"avatar": "https://stash.example.com/avatar.png",
"commits": [
{
"id": "abc123def456",
"displayId": "abc123d",
"message": "TEST-123: Fix login bug",
"author": {"name": "John Doe"},
"authorTimestamp": "2024-01-15T09:00:00.000Z",
"url": "https://stash.example.com/projects/PROJ/repos/app/commits/abc123def456",
}
],
}
],
}
]
}
def test_get_issue_development_info_success(
self, development_mixin, mock_dev_status_response
):
"""Test successful retrieval of development info."""
# Mock get_issue to return issue with ID
development_mixin.jira.get_issue.return_value = {
"id": "12345",
"key": "TEST-123",
}
# Mock the session.get call
mock_response = MagicMock()
mock_response.json.return_value = mock_dev_status_response
mock_response.raise_for_status = MagicMock()
development_mixin.jira._session.get.return_value = mock_response
result = development_mixin.get_issue_development_info(
"TEST-123", application_type="stash", data_type="pullrequest"
)
assert result["issue_key"] == "TEST-123"
assert len(result["pullRequests"]) == 1
assert result["pullRequests"][0]["name"] == "[TEST-123] Fix bug in login"
assert result["pullRequests"][0]["status"] == "MERGED"
assert result["pullRequests"][0]["author"] == "John Doe"
assert len(result["branches"]) == 1
assert result["branches"][0]["name"] == "bugfix/TEST-123-login-fix"
def test_get_issue_development_info_with_commits(
self, development_mixin, mock_dev_status_with_commits
):
"""Test retrieval of development info with commits."""
development_mixin.jira.get_issue.return_value = {
"id": "12345",
"key": "TEST-123",
}
mock_response = MagicMock()
mock_response.json.return_value = mock_dev_status_with_commits
mock_response.raise_for_status = MagicMock()
development_mixin.jira._session.get.return_value = mock_response
result = development_mixin.get_issue_development_info(
"TEST-123", application_type="stash", data_type="repository"
)
assert result["issue_key"] == "TEST-123"
assert len(result["commits"]) == 1
assert result["commits"][0]["message"] == "TEST-123: Fix login bug"
assert result["commits"][0]["author"] == "John Doe"
assert len(result["repositories"]) == 1
assert result["repositories"][0]["name"] == "app"
def test_get_issue_development_info_empty_response(self, development_mixin):
"""Test handling of empty development info response."""
development_mixin.jira.get_issue.return_value = {
"id": "12345",
"key": "TEST-123",
}
mock_response = MagicMock()
mock_response.json.return_value = {"detail": []}
mock_response.raise_for_status = MagicMock()
development_mixin.jira._session.get.return_value = mock_response
result = development_mixin.get_issue_development_info(
"TEST-123", application_type="stash", data_type="pullrequest"
)
assert result["issue_key"] == "TEST-123"
assert result["pullRequests"] == []
assert result["branches"] == []
assert result["commits"] == []
def test_get_issue_development_info_issue_not_found(self, development_mixin):
"""Test error when issue is not found."""
development_mixin.jira.get_issue.return_value = {"key": "TEST-123"}
with pytest.raises(Exception, match="Could not get issue ID"):
development_mixin.get_issue_development_info(
"TEST-123", application_type="stash", data_type="pullrequest"
)
def test_get_issue_development_info_api_error(self, development_mixin):
"""Test handling of API errors."""
development_mixin.jira.get_issue.return_value = {
"id": "12345",
"key": "TEST-123",
}
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = HTTPError(
response=Mock(status_code=500)
)
development_mixin.jira._session.get.return_value = mock_response
with pytest.raises(Exception, match="Error retrieving development info"):
development_mixin.get_issue_development_info(
"TEST-123", application_type="stash", data_type="pullrequest"
)
def test_get_issue_development_info_auto_discovery(self, development_mixin):
"""Test auto-discovery of application types when not specified."""
development_mixin.jira.get_issue.return_value = {
"id": "12345",
"key": "TEST-123",
}
# Return empty for all combinations
mock_response = MagicMock()
mock_response.json.return_value = {"detail": []}
mock_response.raise_for_status = MagicMock()
development_mixin.jira._session.get.return_value = mock_response
result = development_mixin.get_issue_development_info("TEST-123")
# Should have tried multiple app types
assert development_mixin.jira._session.get.call_count > 1
assert result["issue_key"] == "TEST-123"
def test_get_issues_development_info_success(
self, development_mixin, mock_dev_status_response
):
"""Test batch retrieval of development info for multiple issues."""
development_mixin.jira.get_issue.return_value = {
"id": "12345",
"key": "TEST-123",
}
mock_response = MagicMock()
mock_response.json.return_value = mock_dev_status_response
mock_response.raise_for_status = MagicMock()
development_mixin.jira._session.get.return_value = mock_response
results = development_mixin.get_issues_development_info(
["TEST-123", "TEST-456"], application_type="stash", data_type="pullrequest"
)
assert len(results) == 2
# Each result has its own issue_key from the input list
assert results[0]["issue_key"] == "TEST-123"
assert results[1]["issue_key"] == "TEST-456"
# Both should have PRs from the mock response
assert len(results[0]["pullRequests"]) == 1
assert len(results[1]["pullRequests"]) == 1
def test_get_issues_development_info_partial_failure(self, development_mixin):
"""Test batch retrieval with some failures."""
# First call succeeds, second fails
development_mixin.jira.get_issue.side_effect = [
{"id": "12345", "key": "TEST-123"},
Exception("Issue not found"),
]
mock_response = MagicMock()
mock_response.json.return_value = {"detail": []}
mock_response.raise_for_status = MagicMock()
development_mixin.jira._session.get.return_value = mock_response
results = development_mixin.get_issues_development_info(
["TEST-123", "TEST-456"], application_type="stash", data_type="pullrequest"
)
assert len(results) == 2
assert results[0]["issue_key"] == "TEST-123"
assert "error" in results[1]
assert results[1]["pullRequests"] == []
def test_get_issue_development_info_plugin_not_found(self, development_mixin):
"""Test 404 response returns descriptive error message without raising."""
development_mixin.jira.get_issue.return_value = {
"id": "12345",
"key": "TEST-123",
}
mock_response = MagicMock()
mock_response.status_code = 404
development_mixin.jira._session.get.return_value = mock_response
result = development_mixin.get_issue_development_info(
"TEST-123", application_type="stash", data_type="pullrequest"
)
assert "error" in result
assert "dev-status plugin" in result["error"]
assert result["pullRequests"] == []
assert result["branches"] == []
assert result["commits"] == []
def test_get_issue_development_info_access_denied(self, development_mixin):
"""Test 403 response returns descriptive error message without raising."""
development_mixin.jira.get_issue.return_value = {
"id": "12345",
"key": "TEST-123",
}
mock_response = MagicMock()
mock_response.status_code = 403
development_mixin.jira._session.get.return_value = mock_response
result = development_mixin.get_issue_development_info(
"TEST-123", application_type="stash", data_type="pullrequest"
)
assert "error" in result
assert "Access denied" in result["error"]
assert result["pullRequests"] == []
assert result["branches"] == []
assert result["commits"] == []
def test_get_issue_development_info_auto_discovery_404(self, development_mixin):
"""Test auto-discovery stops early on 404 and propagates error."""
development_mixin.jira.get_issue.return_value = {
"id": "12345",
"key": "TEST-123",
}
mock_response = MagicMock()
mock_response.status_code = 404
development_mixin.jira._session.get.return_value = mock_response
result = development_mixin.get_issue_development_info("TEST-123")
assert "error" in result
assert "dev-status plugin" in result["error"]
# Should stop after first 404, not exhaust all 12 combinations
assert development_mixin.jira._session.get.call_count == 1
def test_parse_development_info_with_reviewers(self, development_mixin):
"""Test parsing of PR reviewers."""
response = {
"detail": [
{
"_instance": {"name": "Bitbucket", "baseUrl": "https://bb.com"},
"pullRequests": [
{
"id": "1",
"name": "Test PR",
"status": "OPEN",
"url": "https://bb.com/pr/1",
"source": {"branch": "feature", "repository": {}},
"destination": {"branch": "main"},
"author": {"name": "Author"},
"reviewers": [
{"name": "Reviewer1"},
{"name": "Reviewer2"},
],
"lastUpdate": "2024-01-01",
}
],
"branches": [],
"repositories": [],
}
]
}
result = development_mixin._parse_development_info(response, "TEST-1")
assert len(result["pullRequests"]) == 1
assert result["pullRequests"][0]["reviewers"] == ["Reviewer1", "Reviewer2"]
def test_parse_development_info_missing_fields(self, development_mixin):
"""Test parsing handles missing optional fields gracefully."""
response = {
"detail": [
{
"_instance": {},
"pullRequests": [
{
"id": "1",
"name": "Minimal PR",
"status": "OPEN",
"url": "https://example.com/pr/1",
}
],
"branches": [],
"repositories": [],
}
]
}
result = development_mixin._parse_development_info(response, "TEST-1")
assert len(result["pullRequests"]) == 1
pr = result["pullRequests"][0]
assert pr["name"] == "Minimal PR"
assert pr["source"] == ""
assert pr["destination"] == ""
assert pr["author"] == ""
assert pr["reviewers"] == []