Skip to main content
Glama

MCP Atlassian

by ArconixForge
test_epics.py29.9 kB
"""Tests for the Jira Epics mixin.""" from unittest.mock import MagicMock, call import pytest from mcp_atlassian.jira import JiraFetcher from mcp_atlassian.jira.epics import EpicsMixin from mcp_atlassian.models.jira import JiraIssue class TestEpicsMixin: """Tests for the EpicsMixin class.""" @pytest.fixture def epics_mixin(self, jira_fetcher: JiraFetcher) -> EpicsMixin: """Create an EpicsMixin instance with mocked dependencies.""" mixin = jira_fetcher # Add a mock for get_issue to use when returning models mixin.get_issue = MagicMock( return_value=JiraIssue( id="12345", key="TEST-123", summary="Test Issue", description="Issue content", ) ) # Add a mock for search_issues to use for get_epic_issues mixin.search_issues = MagicMock( return_value=[ JiraIssue(key="TEST-456", summary="Issue 1"), JiraIssue(key="TEST-789", summary="Issue 2"), ] ) return mixin def test_try_discover_fields_from_existing_epic(self, epics_mixin: EpicsMixin): """Test _try_discover_fields_from_existing_epic with a successful discovery.""" # Skip if we already have both required fields field_ids = {"epic_link": "customfield_10014"} # Missing epic_name # Mock Epic search response mock_epic = { "key": "EPIC-123", "fields": { "issuetype": {"name": "Epic"}, "summary": "Test Epic", "customfield_10011": "Epic Name Value", # This should be discovered as epic_name }, } mock_results = {"issues": [mock_epic]} epics_mixin.jira.jql.return_value = mock_results # Call the method epics_mixin._try_discover_fields_from_existing_epic(field_ids) # Verify the epic_name field was discovered assert "epic_name" in field_ids assert field_ids["epic_name"] == "customfield_10011" def test_try_discover_fields_from_existing_epic_no_epics( self, epics_mixin: EpicsMixin ): """Test _try_discover_fields_from_existing_epic when no epics exist.""" field_ids = {} # Mock empty search response mock_results = {"issues": []} epics_mixin.jira.jql.return_value = mock_results # Call the method epics_mixin._try_discover_fields_from_existing_epic(field_ids) # Verify no fields were discovered assert not field_ids def test_try_discover_fields_from_existing_epic_with_both_fields( self, epics_mixin: EpicsMixin ): """Test _try_discover_fields_from_existing_epic when both fields already exist.""" field_ids = {"epic_link": "customfield_10014", "epic_name": "customfield_10011"} # Call the method - no JQL should be executed epics_mixin._try_discover_fields_from_existing_epic(field_ids) # Verify jql was not called epics_mixin.jira.jql.assert_not_called() def test_prepare_epic_fields_basic(self, epics_mixin: EpicsMixin): """Test prepare_epic_fields with basic epic name and color.""" # Mock get_field_ids_to_epic epics_mixin.get_field_ids_to_epic = MagicMock( return_value={ "epic_name": "customfield_10011", "epic_color": "customfield_10010", } ) # Prepare test data fields = {} summary = "Test Epic" kwargs = {} # Call the method epics_mixin.prepare_epic_fields(fields, summary, kwargs) # Verify the epic fields are stored in kwargs with __epic_ prefix # instead of directly in fields (for two-step creation) assert kwargs["__epic_name_value"] == "Test Epic" assert kwargs["__epic_name_field"] == "customfield_10011" assert kwargs["__epic_color_value"] == "green" assert kwargs["__epic_color_field"] == "customfield_10010" # Verify fields dict remains empty assert fields == {} def test_prepare_epic_fields_with_user_values(self, epics_mixin: EpicsMixin): """Test prepare_epic_fields with user-provided values.""" # Mock get_field_ids_to_epic epics_mixin.get_field_ids_to_epic = MagicMock( return_value={ "epic_name": "customfield_10011", "epic_color": "customfield_10010", } ) # Prepare test data fields = {} summary = "Test Epic" kwargs = {"epic_name": "Custom Epic Name", "epic_color": "blue"} # Call the method epics_mixin.prepare_epic_fields(fields, summary, kwargs) # Verify the epic fields are stored in kwargs with __epic_ prefix assert kwargs["__epic_name_value"] == "Custom Epic Name" assert kwargs["__epic_name_field"] == "customfield_10011" assert kwargs["__epic_color_value"] == "blue" assert kwargs["__epic_color_field"] == "customfield_10010" # Original values should be removed from kwargs assert "epic_name" not in kwargs assert "epic_color" not in kwargs # Verify fields dict remains empty assert fields == {} def test_prepare_epic_fields_missing_epic_name(self, epics_mixin: EpicsMixin): """Test prepare_epic_fields with missing epic_name field.""" # Mock get_field_ids_to_epic epics_mixin.get_field_ids_to_epic = MagicMock( return_value={"epic_color": "customfield_10010"} ) # Prepare test data fields = {} summary = "Test Epic" kwargs = {} # Call the method epics_mixin.prepare_epic_fields(fields, summary, kwargs) # Verify only the color was stored in kwargs assert "__epic_name_value" not in kwargs assert "__epic_name_field" not in kwargs assert kwargs["__epic_color_value"] == "green" assert kwargs["__epic_color_field"] == "customfield_10010" # Verify fields dict remains empty assert fields == {} def test_prepare_epic_fields_with_error(self, epics_mixin: EpicsMixin): """Test prepare_epic_fields catches and logs errors.""" # Mock get_field_ids_to_epic to raise an exception epics_mixin.get_field_ids_to_epic = MagicMock( side_effect=Exception("Field error") ) # Create the fields dict and call the method fields = {} epics_mixin.prepare_epic_fields(fields, "Test Epic", {}) # Verify that fields didn't get updated assert fields == {} # Verify the error was logged epics_mixin.get_field_ids_to_epic.assert_called_once() def test_prepare_epic_fields_with_non_standard_ids(self, epics_mixin: EpicsMixin): """Test that prepare_epic_fields correctly handles non-standard field IDs.""" # Mock field IDs with non-standard custom field IDs mock_field_ids = { "epic_name": "customfield_54321", "epic_color": "customfield_98765", } # Mock the get_field_ids_to_epic method to return our custom field IDs epics_mixin.get_field_ids_to_epic = MagicMock(return_value=mock_field_ids) # Create the fields dict and call the method with basic values fields = {} kwargs = {} epics_mixin.prepare_epic_fields(fields, "Test Epic", kwargs) # Verify fields were stored in kwargs with the non-standard IDs assert kwargs["__epic_name_value"] == "Test Epic" assert kwargs["__epic_name_field"] == "customfield_54321" assert kwargs["__epic_color_value"] == "green" assert kwargs["__epic_color_field"] == "customfield_98765" # Verify fields dict remains empty assert fields == {} # Test with custom values fields = {} kwargs = {"epic_name": "Custom Name", "epic_color": "blue"} epics_mixin.prepare_epic_fields(fields, "Test Epic", kwargs) # Verify custom values were stored in kwargs assert kwargs["__epic_name_value"] == "Custom Name" assert kwargs["__epic_name_field"] == "customfield_54321" assert kwargs["__epic_color_value"] == "blue" assert kwargs["__epic_color_field"] == "customfield_98765" # Original values should be removed from kwargs assert "epic_name" not in kwargs assert "epic_color" not in kwargs # Verify fields dict remains empty assert fields == {} def test_prepare_epic_fields_with_required_epic_name(self, epics_mixin: EpicsMixin): """Test Epic field preparation when Epic Name is a required field.""" # Mock get_field_ids_to_epic to return field IDs epics_mixin.get_field_ids_to_epic = MagicMock( return_value={ "epic_name": "customfield_10011", "epic_color": "customfield_10012", } ) # Mock get_required_fields to return Epic Name as required epics_mixin.get_required_fields = MagicMock( return_value={ "customfield_10011": { "fieldId": "customfield_10011", "required": True, "name": "Epic Name", } } ) fields = {} kwargs = {"epic_name": "My Epic Name", "epic_color": "blue"} # Call prepare_epic_fields with project_key epics_mixin.prepare_epic_fields(fields, "Test Epic", kwargs, "TEST") # Assert Epic Name was added to fields for initial creation assert fields["customfield_10011"] == "My Epic Name" # Assert Epic Color was stored for post-creation update assert kwargs["__epic_color_field"] == "customfield_10012" assert kwargs["__epic_color_value"] == "blue" # Verify get_required_fields was called with correct parameters epics_mixin.get_required_fields.assert_called_once_with("Epic", "TEST") def test_prepare_epic_fields_with_optional_epic_name(self, epics_mixin: EpicsMixin): """Test Epic field preparation when Epic Name is not a required field.""" # Mock get_field_ids_to_epic to return field IDs epics_mixin.get_field_ids_to_epic = MagicMock( return_value={ "epic_name": "customfield_10011", "epic_color": "customfield_10012", } ) # Mock get_required_fields to return empty dict (no required fields) epics_mixin.get_required_fields = MagicMock(return_value={}) fields = {} kwargs = {"epic_name": "My Epic Name", "epic_color": "green"} # Call prepare_epic_fields with project_key epics_mixin.prepare_epic_fields(fields, "Test Epic", kwargs, "TEST") # Assert Epic Name was stored for post-creation update (not in fields) assert "customfield_10011" not in fields assert kwargs["__epic_name_field"] == "customfield_10011" assert kwargs["__epic_name_value"] == "My Epic Name" # Assert Epic Color was also stored for post-creation update assert kwargs["__epic_color_field"] == "customfield_10012" assert kwargs["__epic_color_value"] == "green" def test_prepare_epic_fields_mixed_required_optional(self, epics_mixin: EpicsMixin): """Test Epic field preparation with mixed required and optional fields.""" # Mock get_field_ids_to_epic to return field IDs epics_mixin.get_field_ids_to_epic = MagicMock( return_value={ "epic_name": "customfield_10011", "epic_color": "customfield_10012", "epic_start_date": "customfield_10013", } ) # Mock get_required_fields to return Epic Name and Start Date as required epics_mixin.get_required_fields = MagicMock( return_value={ "customfield_10011": {"fieldId": "customfield_10011", "required": True}, "customfield_10013": {"fieldId": "customfield_10013", "required": True}, } ) fields = {} kwargs = { "epic_name": "My Epic Name", "epic_color": "purple", "epic_start_date": "2024-01-01", } # Call prepare_epic_fields with project_key epics_mixin.prepare_epic_fields(fields, "Test Epic", kwargs, "TEST") # Assert required fields were added to fields assert fields["customfield_10011"] == "My Epic Name" assert fields["customfield_10013"] == "2024-01-01" # Assert optional field was stored for post-creation update assert "customfield_10012" not in fields assert kwargs["__epic_color_field"] == "customfield_10012" assert kwargs["__epic_color_value"] == "purple" def test_prepare_epic_fields_no_project_key(self, epics_mixin: EpicsMixin): """Test Epic field preparation when no project_key is provided.""" # Mock get_field_ids_to_epic to return field IDs epics_mixin.get_field_ids_to_epic = MagicMock( return_value={ "epic_name": "customfield_10011", "epic_color": "customfield_10012", } ) # Mock get_required_fields should not be called epics_mixin.get_required_fields = MagicMock() fields = {} kwargs = {"epic_name": "My Epic Name", "epic_color": "red"} # Call prepare_epic_fields without project_key (None) epics_mixin.prepare_epic_fields(fields, "Test Epic", kwargs, None) # Assert all fields were stored for post-creation update (fallback behavior) assert "customfield_10011" not in fields assert "customfield_10012" not in fields assert kwargs["__epic_name_field"] == "customfield_10011" assert kwargs["__epic_name_value"] == "My Epic Name" assert kwargs["__epic_color_field"] == "customfield_10012" assert kwargs["__epic_color_value"] == "red" # Verify get_required_fields was not called epics_mixin.get_required_fields.assert_not_called() def test_prepare_epic_fields_get_required_fields_error( self, epics_mixin: EpicsMixin ): """Test Epic field preparation when get_required_fields raises an error.""" # Mock get_field_ids_to_epic to return field IDs epics_mixin.get_field_ids_to_epic = MagicMock( return_value={ "epic_name": "customfield_10011", "epic_color": "customfield_10012", } ) # Mock get_required_fields to raise an exception epics_mixin.get_required_fields = MagicMock(side_effect=Exception("API error")) fields = {} kwargs = {"epic_name": "My Epic Name", "epic_color": "yellow"} # Call prepare_epic_fields with project_key epics_mixin.prepare_epic_fields(fields, "Test Epic", kwargs, "TEST") # Assert it falls back to storing all fields for post-creation update assert "customfield_10011" not in fields assert "customfield_10012" not in fields assert kwargs["__epic_name_field"] == "customfield_10011" assert kwargs["__epic_name_value"] == "My Epic Name" assert kwargs["__epic_color_field"] == "customfield_10012" assert kwargs["__epic_color_value"] == "yellow" def test_prepare_epic_fields_no_get_required_fields_method( self, epics_mixin: EpicsMixin ): """Test Epic field preparation when get_required_fields method doesn't exist.""" # Mock get_field_ids_to_epic to return field IDs epics_mixin.get_field_ids_to_epic = MagicMock( return_value={ "epic_name": "customfield_10011", "epic_color": "customfield_10012", } ) # Mock hasattr to return False for get_required_fields original_hasattr = hasattr def mock_hasattr(obj, attr): if attr == "get_required_fields": return False return original_hasattr(obj, attr) import builtins builtins.hasattr = mock_hasattr fields = {} kwargs = {"epic_name": "My Epic Name", "epic_color": "orange"} # Call prepare_epic_fields with project_key epics_mixin.prepare_epic_fields(fields, "Test Epic", kwargs, "TEST") # Restore original hasattr builtins.hasattr = original_hasattr # Assert it falls back to storing all fields for post-creation update assert "customfield_10011" not in fields assert "customfield_10012" not in fields assert kwargs["__epic_name_field"] == "customfield_10011" assert kwargs["__epic_name_value"] == "My Epic Name" assert kwargs["__epic_color_field"] == "customfield_10012" assert kwargs["__epic_color_value"] == "orange" def test_dynamic_epic_field_discovery(self, epics_mixin: EpicsMixin): """Test the dynamic discovery of Epic fields with pattern matching.""" # Mock get_field_ids_to_epic with no epic-related fields epics_mixin.get_field_ids_to_epic = MagicMock( return_value={ "random_field": "customfield_12345", "some_other_field": "customfield_67890", "Epic-FieldName": "customfield_11111", # Should be found by pattern matching "epic_colour_field": "customfield_22222", # Should be found by pattern matching } ) # Create a fields dict and call prepare_epic_fields fields = {} kwargs = {} # The _get_epic_name_field_id and _get_epic_color_field_id methods should discover # the fields by pattern matching, even though they're not in the standard format # We need to patch these methods to return the expected values original_get_name = epics_mixin._get_epic_name_field_id original_get_color = epics_mixin._get_epic_color_field_id epics_mixin._get_epic_name_field_id = MagicMock( return_value="customfield_11111" ) epics_mixin._get_epic_color_field_id = MagicMock( return_value="customfield_22222" ) # Now call prepare_epic_fields epics_mixin.prepare_epic_fields(fields, "Test Epic Name", kwargs) # Verify the fields were stored in kwargs assert kwargs["__epic_name_value"] == "Test Epic Name" assert kwargs["__epic_name_field"] == "customfield_11111" assert kwargs["__epic_color_value"] == "green" assert kwargs["__epic_color_field"] == "customfield_22222" # Verify fields dict remains empty assert fields == {} # Restore the original methods epics_mixin._get_epic_name_field_id = original_get_name epics_mixin._get_epic_color_field_id = original_get_color def test_link_issue_to_epic_success(self, epics_mixin: EpicsMixin): """Test link_issue_to_epic with successful linking.""" # Setup mocks # - issue exists epics_mixin.jira.get_issue.side_effect = [ {"key": "TEST-123"}, # issue { # epic "key": "EPIC-456", "fields": {"issuetype": {"name": "Epic"}}, }, ] # Mock get_issue to return a valid JiraIssue epics_mixin.get_issue = MagicMock( return_value=JiraIssue(key="TEST-123", id="123456") ) # - epic link field discovered epics_mixin.get_field_ids_to_epic = MagicMock( return_value={"epic_link": "customfield_10014"} ) # - Parent field fails, then epic_link succeeds epics_mixin.jira.update_issue.side_effect = [ Exception("Parent field error"), # First attempt fails None, # Second attempt succeeds ] # Call the method result = epics_mixin.link_issue_to_epic("TEST-123", "EPIC-456") # Verify API calls - should have two calls, one for parent and one for epic_link assert epics_mixin.jira.update_issue.call_count == 2 # First call should be with parent assert epics_mixin.jira.update_issue.call_args_list[0] == call( issue_key="TEST-123", update={"fields": {"parent": {"key": "EPIC-456"}}} ) # Second call should be with epic_link field assert epics_mixin.jira.update_issue.call_args_list[1] == call( issue_key="TEST-123", update={"fields": {"customfield_10014": "EPIC-456"}} ) # Verify get_issue was called to return the result epics_mixin.get_issue.assert_called_once_with("TEST-123") # Verify result assert isinstance(result, JiraIssue) assert result.key == "TEST-123" def test_link_issue_to_epic_parent_field_success(self, epics_mixin: EpicsMixin): """Test link_issue_to_epic succeeding with parent field.""" # Setup mocks epics_mixin.jira.get_issue.side_effect = [ {"key": "TEST-123"}, # issue { # epic "key": "EPIC-456", "fields": {"issuetype": {"name": "Epic"}}, }, ] # Mock get_issue to return a valid JiraIssue epics_mixin.get_issue = MagicMock( return_value=JiraIssue(key="TEST-123", id="123456") ) # - No epic_link field (forces parent usage) epics_mixin.get_field_ids_to_epic = MagicMock(return_value={}) # Parent field update succeeds epics_mixin.jira.update_issue.return_value = None # Call the method result = epics_mixin.link_issue_to_epic("TEST-123", "EPIC-456") # Verify only one API call with parent field epics_mixin.jira.update_issue.assert_called_once_with( issue_key="TEST-123", update={"fields": {"parent": {"key": "EPIC-456"}}} ) # Verify result assert isinstance(result, JiraIssue) assert result.key == "TEST-123" def test_link_issue_to_epic_not_epic(self, epics_mixin: EpicsMixin): """Test link_issue_to_epic when the target is not an epic.""" # Setup mocks epics_mixin.jira.get_issue.side_effect = [ {"key": "TEST-123"}, # issue { # not an epic "key": "TEST-456", "fields": {"issuetype": {"name": "Task"}}, }, ] # Call the method and expect an error with pytest.raises( ValueError, match="Error linking issue to epic: TEST-456 is not an Epic" ): epics_mixin.link_issue_to_epic("TEST-123", "TEST-456") def test_link_issue_to_epic_all_methods_fail(self, epics_mixin: EpicsMixin): """Test link_issue_to_epic when all linking methods fail.""" # Setup mocks epics_mixin.jira.get_issue.side_effect = [ {"key": "TEST-123"}, # issue { # epic "key": "EPIC-456", "fields": {"issuetype": {"name": "Epic"}}, }, ] # No epic link fields found epics_mixin.get_field_ids_to_epic = MagicMock(return_value={}) # All update attempts fail epics_mixin.jira.update_issue.side_effect = Exception("Update failed") epics_mixin.jira.create_issue_link.side_effect = Exception("Link failed") # Call the method and expect a ValueError with pytest.raises( ValueError, match="Could not link issue TEST-123 to epic EPIC-456.", ): epics_mixin.link_issue_to_epic("TEST-123", "EPIC-456") def test_link_issue_to_epic_api_error(self, epics_mixin: EpicsMixin): """Test link_issue_to_epic with API error in the epic retrieval.""" # Setup mocks to fail at epic retrieval epics_mixin.jira.get_issue.side_effect = [ {"key": "TEST-123"}, # issue Exception("API error"), # epic retrieval fails ] # Call the method and expect the API error to be propagated with pytest.raises(Exception, match="Error linking issue to epic: API error"): epics_mixin.link_issue_to_epic("TEST-123", "EPIC-456") def test_get_epic_issues_success(self, epics_mixin): """Test get_epic_issues with successful retrieval.""" # Setup mocks epics_mixin.jira.get_issue.return_value = { "key": "EPIC-123", "fields": {"issuetype": {"name": "Epic"}}, } epics_mixin.get_field_ids_to_epic = MagicMock( return_value={"epic_link": "customfield_10014"} ) # Create a mock search result object with issues attribute mock_issues = [ JiraIssue(key="TEST-456", summary="Issue 1"), JiraIssue(key="TEST-789", summary="Issue 2"), ] # Create a mock object with an issues attribute class MockSearchResult: def __init__(self, issues): self.issues = issues mock_search_result = MockSearchResult(mock_issues) # Mock search_issues to return our mock search result epics_mixin.search_issues = MagicMock(return_value=mock_search_result) # Call the method with start parameter result = epics_mixin.get_epic_issues("EPIC-123", start=5, limit=10) # Verify search_issues was called with the right JQL epics_mixin.search_issues.assert_called_once() call_args = epics_mixin.search_issues.call_args[0] assert 'issueFunction in issuesScopedToEpic("EPIC-123")' in call_args[0] # Verify keyword arguments for start and limit call_kwargs = epics_mixin.search_issues.call_args[1] assert call_kwargs.get("start") == 5 assert call_kwargs.get("limit") == 10 # Verify result assert len(result) == 2 assert result[0].key == "TEST-456" assert result[1].key == "TEST-789" def test_get_epic_issues_not_epic(self, epics_mixin): """Test get_epic_issues when the issue is not an epic.""" # Setup mocks - issue is not an epic epics_mixin.jira.get_issue.return_value = { "key": "TEST-123", "fields": {"issuetype": {"name": "Task"}}, } # Call the method and expect an error with pytest.raises( ValueError, match="Issue TEST-123 is not an Epic, it is a Task" ): epics_mixin.get_epic_issues("TEST-123") def test_get_epic_issues_no_results(self, epics_mixin): """Test get_epic_issues when no results are found.""" # Setup mocks epics_mixin.jira.get_issue.return_value = { "key": "EPIC-123", "fields": {"issuetype": {"name": "Epic"}}, } epics_mixin.get_field_ids_to_epic = MagicMock( return_value={"epic_link": "customfield_10014"} ) # Make search_issues return empty results epics_mixin.search_issues = MagicMock(return_value=[]) # Call the method result = epics_mixin.get_epic_issues("EPIC-123") # Verify the result is an empty list assert isinstance(result, list) assert not result def test_get_epic_issues_fallback_jql(self, epics_mixin): """Test get_epic_issues with fallback JQL queries.""" # Setup mocks epics_mixin.jira.get_issue.return_value = { "key": "EPIC-123", "fields": {"issuetype": {"name": "Epic"}}, } epics_mixin.get_field_ids_to_epic = MagicMock( return_value={"epic_link": "customfield_10014", "parent": "parent"} ) # Create a mock class for SearchResult class MockSearchResult: def __init__(self, issues: list[JiraIssue]): self.issues = issues def __bool__(self): return bool(self.issues) def __len__(self): return len(self.issues) # Mock search_issues to return empty results for issueFunction but results for epic_link def search_side_effect(jql, **kwargs): if "issueFunction" in jql: return MockSearchResult([]) # No results for issueFunction if "customfield_10014" in jql: # Return results for customfield query return MockSearchResult( [ JiraIssue(key="CHILD-1", summary="Child 1"), JiraIssue(key="CHILD-2", summary="Child 2"), ] ) msg = f"Unexpected JQL query as {jql}" raise KeyError(msg) epics_mixin.search_issues = MagicMock(side_effect=search_side_effect) # Call the method with start parameter result = epics_mixin.get_epic_issues("EPIC-123", start=3, limit=10) # Verify we got results from the second query assert len(result) == 2 assert result[0].key == "CHILD-1" assert result[1].key == "CHILD-2" # Verify the start parameter was passed to search_issues assert epics_mixin.search_issues.call_count >= 2 # Check last call (which should be the one that returned results) last_call_kwargs = epics_mixin.search_issues.call_args[1] assert last_call_kwargs.get("start") == 3 assert last_call_kwargs.get("limit") == 10 def test_get_epic_issues_api_error(self, epics_mixin: EpicsMixin): """Test get_epic_issues with API error.""" # Setup mocks - simulate API error epics_mixin.jira.get_issue.side_effect = Exception("API error") # Call the method and expect an error with pytest.raises( Exception, match="Error getting epic issues: API error", ): epics_mixin.get_epic_issues("EPIC-123")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ArconixForge/mcp-atlassian'

If you have feedback or need assistance with the MCP directory API, please join our Discord server