Skip to content

Commit c6fea35

Browse files
authored
fix: handle empty JQL and ORDER BY clauses with project filters (sooperset#578)
When using JIRA_PROJECTS_FILTER environment variable or the projects_filter parameter, the search functionality would create invalid JQL syntax when: - The JQL query was empty or None - The JQL query started with an ORDER BY clause This fix detects these cases and properly prepends the project filter instead of attempting to use AND, which was causing Jira API errors. Examples of fixed behavior: - Empty JQL: `project IN ("ACQ", "LMAN")` - ORDER BY: `project IN ("ACQ", "LMAN") ORDER BY created DESC` Reported-by: adampaulsen Github-Issue: sooperset#573
1 parent 7714850 commit c6fea35

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed

src/mcp_atlassian/jira/search.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,15 @@ def search_issues(
6464
project_query = f"project IN ({projects_list})"
6565

6666
# Add the project filter to existing query
67-
if jql and project_query:
68-
if "project = " not in jql and "project IN" not in jql:
69-
# Only add if not already filtering by project
70-
jql = f"({jql}) AND {project_query}"
71-
else:
67+
if not jql:
68+
# Empty JQL - just use project filter
7269
jql = project_query
70+
elif jql.strip().upper().startswith("ORDER BY"):
71+
# JQL starts with ORDER BY - prepend project filter
72+
jql = f"{project_query} {jql}"
73+
elif "project = " not in jql and "project IN" not in jql:
74+
# Only add if not already filtering by project
75+
jql = f"({jql}) AND {project_query}"
7376

7477
logger.info(f"Applied projects filter to query: {jql}")
7578

tests/unit/jira/test_search.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,3 +672,119 @@ def test_search_issues_with_config_projects_filter_jql_construction(
672672
api_method_mock.assert_called_with(
673673
"(text ~ 'test') AND project = \"OVERRIDE\"", **expected_kwargs
674674
)
675+
676+
@pytest.mark.parametrize("is_cloud", [True, False])
677+
def test_search_issues_with_empty_jql_and_projects_filter(
678+
self, search_mixin: SearchMixin, mock_issues_response, is_cloud
679+
):
680+
"""Test that empty JQL correctly prepends project filter without AND."""
681+
# Setup
682+
search_mixin.config.is_cloud = is_cloud
683+
search_mixin.config.projects_filter = None
684+
search_mixin.config.url = "https://test.example.com"
685+
686+
# Setup mock response for both API methods
687+
search_mixin.jira.enhanced_jql_get_list_of_tickets = MagicMock(
688+
return_value=mock_issues_response["issues"]
689+
)
690+
search_mixin.jira.jql = MagicMock(return_value=mock_issues_response)
691+
api_method_mock = getattr(
692+
search_mixin.jira, "enhanced_jql_get_list_of_tickets" if is_cloud else "jql"
693+
)
694+
695+
# Define expected kwargs based on is_cloud
696+
expected_kwargs = {
697+
"fields": ANY,
698+
"limit": ANY,
699+
"expand": ANY,
700+
}
701+
# Add start parameter only for Server/DC
702+
if not is_cloud:
703+
expected_kwargs["start"] = ANY
704+
705+
# Test 1: Empty string JQL with single project
706+
search_mixin.search_issues("", projects_filter="PROJ1")
707+
api_method_mock.assert_called_with('project = "PROJ1"', **expected_kwargs)
708+
709+
# Reset mock
710+
api_method_mock.reset_mock()
711+
712+
# Test 2: Empty string JQL with multiple projects
713+
search_mixin.search_issues("", projects_filter="PROJ1,PROJ2")
714+
api_method_mock.assert_called_with(
715+
'project IN ("PROJ1", "PROJ2")', **expected_kwargs
716+
)
717+
718+
# Reset mock
719+
api_method_mock.reset_mock()
720+
721+
# Test 3: None JQL with projects filter
722+
result = search_mixin.search_issues(None, projects_filter="PROJ1")
723+
api_method_mock.assert_called_with('project = "PROJ1"', **expected_kwargs)
724+
assert isinstance(result, JiraSearchResult)
725+
726+
@pytest.mark.parametrize("is_cloud", [True, False])
727+
def test_search_issues_with_order_by_and_projects_filter(
728+
self, search_mixin: SearchMixin, mock_issues_response, is_cloud
729+
):
730+
"""Test that JQL starting with ORDER BY correctly prepends project filter."""
731+
# Setup
732+
search_mixin.config.is_cloud = is_cloud
733+
search_mixin.config.projects_filter = None
734+
search_mixin.config.url = "https://test.example.com"
735+
736+
# Setup mock response for both API methods
737+
search_mixin.jira.enhanced_jql_get_list_of_tickets = MagicMock(
738+
return_value=mock_issues_response["issues"]
739+
)
740+
search_mixin.jira.jql = MagicMock(return_value=mock_issues_response)
741+
api_method_mock = getattr(
742+
search_mixin.jira, "enhanced_jql_get_list_of_tickets" if is_cloud else "jql"
743+
)
744+
745+
# Define expected kwargs based on is_cloud
746+
expected_kwargs = {
747+
"fields": ANY,
748+
"limit": ANY,
749+
"expand": ANY,
750+
}
751+
# Add start parameter only for Server/DC
752+
if not is_cloud:
753+
expected_kwargs["start"] = ANY
754+
755+
# Test 1: ORDER BY with single project
756+
search_mixin.search_issues("ORDER BY created DESC", projects_filter="PROJ1")
757+
api_method_mock.assert_called_with(
758+
'project = "PROJ1" ORDER BY created DESC', **expected_kwargs
759+
)
760+
761+
# Reset mock
762+
api_method_mock.reset_mock()
763+
764+
# Test 2: ORDER BY with multiple projects
765+
search_mixin.search_issues(
766+
"ORDER BY created DESC", projects_filter="PROJ1,PROJ2"
767+
)
768+
api_method_mock.assert_called_with(
769+
'project IN ("PROJ1", "PROJ2") ORDER BY created DESC', **expected_kwargs
770+
)
771+
772+
# Reset mock
773+
api_method_mock.reset_mock()
774+
775+
# Test 3: Case insensitive ORDER BY
776+
search_mixin.search_issues("order by updated ASC", projects_filter="PROJ1")
777+
api_method_mock.assert_called_with(
778+
'project = "PROJ1" order by updated ASC', **expected_kwargs
779+
)
780+
781+
# Reset mock
782+
api_method_mock.reset_mock()
783+
784+
# Test 4: ORDER BY with extra spaces
785+
search_mixin.search_issues(
786+
" ORDER BY priority DESC ", projects_filter="PROJ1"
787+
)
788+
api_method_mock.assert_called_with(
789+
'project = "PROJ1" ORDER BY priority DESC ', **expected_kwargs
790+
)

0 commit comments

Comments
 (0)