from unittest.mock import MagicMock, patch
import pytest
from mcp_atlassian.jira.issues import IssuesMixin
from mcp_atlassian.jira.protocols import (
AttachmentsOperationsProto,
EpicOperationsProto,
FieldsOperationsProto,
IssueOperationsProto,
ProjectsOperationsProto,
UsersOperationsProto,
)
class ConcreteIssuesMixin(
IssuesMixin,
AttachmentsOperationsProto,
EpicOperationsProto,
FieldsOperationsProto,
IssueOperationsProto,
ProjectsOperationsProto,
UsersOperationsProto,
):
def _generate_field_map(self):
pass
def _get_account_id(self, user_identifier):
pass
def _try_discover_fields_from_existing_epic(self):
pass
def get_field_by_id(self, field_id):
pass
def get_field_ids_to_epic(self):
pass
def get_project_issue_types(self, project_key):
pass
def get_required_fields(self, project_key, issue_type_name):
pass
def get_issue_forms(self, issue_key):
pass
def get_form_details(self, issue_key, form_id):
pass
def prepare_epic_fields(self, fields, summary, kwargs, project_key):
pass
def update_epic_fields(self, issue_key, epic_fields):
pass
def upload_attachments(self, issue_key, attachment_paths):
pass
@pytest.fixture
def issues_mixin():
"""Fixture to create an instance of IssuesMixin with a mocked jira client."""
with patch("mcp_atlassian.jira.config.JiraConfig.from_env") as mock_from_env:
mock_config = MagicMock()
mock_config.is_cloud = True
mock_from_env.return_value = mock_config
mixin = ConcreteIssuesMixin()
mixin.jira = MagicMock()
mixin.config = mock_config
# Mock methods that are not part of the mixin but are called by it
mixin.get_project_issue_types = MagicMock()
mixin._markdown_to_jira = MagicMock(side_effect=lambda x: x)
mixin._get_account_id = MagicMock(return_value="account_id_123")
mixin._add_assignee_to_fields = MagicMock()
mixin._prepare_epic_fields = MagicMock()
mixin._prepare_parent_fields = MagicMock()
mixin._process_additional_fields = MagicMock()
mixin._handle_create_issue_error = MagicMock()
# Mock the final call to get the created issue
created_issue_response = {
"key": "PROJ-123",
"fields": {"summary": "Test Summary"},
}
mixin.jira.get_issue.return_value = created_issue_response
return mixin
def test_create_issue_uses_epic_id_when_found(issues_mixin):
"""Verify create_issue uses the issue type ID for 'Epic' when found."""
issues_mixin.get_project_issue_types.return_value = [
{"id": "10000", "name": "Story", "subtask": False},
{"id": "10001", "name": "Epic", "subtask": False},
]
issues_mixin.jira.create_issue.return_value = {"key": "PROJ-1"}
issues_mixin.create_issue(
project_key="PROJ",
summary="New Epic",
issue_type="Epic",
)
call_args = issues_mixin.jira.create_issue.call_args
assert call_args is not None
fields = call_args[1]["fields"]
assert "id" in fields["issuetype"].keys()
assert "name" in fields["issuetype"].keys()
assert fields["issuetype"]["id"] == "10001"
def test_create_issue_uses_subtask_id_when_found(issues_mixin):
"""Verify create_issue uses the issue type ID for 'Subtask' when found."""
issues_mixin.get_project_issue_types.return_value = [
{"id": "10002", "name": "Sub-task", "subtask": True},
]
issues_mixin.jira.create_issue.return_value = {"key": "PROJ-2"}
issues_mixin.create_issue(
project_key="PROJ",
summary="New Subtask",
issue_type="Subtask",
parent="PROJ-1",
)
call_args = issues_mixin.jira.create_issue.call_args
assert call_args is not None
fields = call_args[1]["fields"]
assert "id" in fields["issuetype"].keys()
assert "name" in fields["issuetype"].keys()
assert fields["issuetype"]["id"] == "10002"
def test_create_issue_falls_back_to_name_if_id_not_found(issues_mixin):
"""Verify create_issue falls back to name if a type ID is not found."""
issues_mixin.get_project_issue_types.return_value = [
{"id": "10000", "name": "Story", "subtask": False},
]
issues_mixin.jira.create_issue.return_value = {"key": "PROJ-3"}
issues_mixin.create_issue(
project_key="PROJ",
summary="New Epic with no ID",
issue_type="Epic",
)
call_args = issues_mixin.jira.create_issue.call_args
assert call_args is not None
fields = call_args[1]["fields"]
assert fields["issuetype"] == {"name": "Epic"}
def test_find_epic_issue_type_id_returns_id(issues_mixin):
"""Verify _find_epic_issue_type_id returns the correct ID."""
issues_mixin.get_project_issue_types.return_value = [
{"id": "10001", "name": "Epic", "subtask": False},
]
epic_id = issues_mixin._find_epic_issue_type_id("PROJ")
assert epic_id == "10001"
def test_find_subtask_issue_type_id_returns_id(issues_mixin):
"""Verify _find_subtask_issue_type_id returns the correct ID."""
issues_mixin.get_project_issue_types.return_value = [
{"id": "10002", "name": "Sub-task", "subtask": True},
]
subtask_id = issues_mixin._find_subtask_issue_type_id("PROJ")
assert subtask_id == "10002"
def test_find_epic_issue_type_id_returns_none_if_not_found(issues_mixin):
"""Verify _find_epic_issue_type_id returns None when no Epic type exists."""
issues_mixin.get_project_issue_types.return_value = [
{"id": "10000", "name": "Story", "subtask": False},
]
epic_id = issues_mixin._find_epic_issue_type_id("PROJ")
assert epic_id is None
def test_find_subtask_issue_type_id_returns_none_if_not_found(issues_mixin):
"""Verify _find_subtask_issue_type_id returns None when no Sub-task type exists."""
issues_mixin.get_project_issue_types.return_value = [
{"id": "10000", "name": "Story", "subtask": False},
]
subtask_id = issues_mixin._find_subtask_issue_type_id("PROJ")
assert subtask_id is None