From b921ec2b42d8bfb19ea8f24d51ef91887fd006c1 Mon Sep 17 00:00:00 2001 From: Bernhard Willert Date: Thu, 19 Feb 2026 11:21:57 +0100 Subject: [PATCH] Check statusCategory for Jira issue status * status category is mainly used to decide if a jira issue is active or not * if the category is undefined or an unknown status, fall back to resolution checking * the resolution object was compared to a string "None", this always returned False * provide unit tests for new functionality --- dojo/jira_link/helper.py | 41 +++++----- unittests/test_jira_helper.py | 143 ++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 unittests/test_jira_helper.py diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index feff72003ef..5d10bad3ac8 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -1219,28 +1219,31 @@ def get_jira_issue_from_jira(find): def issue_from_jira_is_active(issue_from_jira): - # "resolution":{ - # "self":"http://www.testjira.com/rest/api/2/resolution/11", - # "id":"11", - # "description":"Cancelled by the customer.", - # "name":"Cancelled" - # }, - - # or - # "resolution": null - - # or - # "resolution": "None" - - if not hasattr(issue_from_jira.fields, "resolution"): - logger.debug(vars(issue_from_jira)) + if not hasattr(issue_from_jira, "fields"): + logger.debug("No jira data fields found, treating as active") return True - if not issue_from_jira.fields.resolution: + key = getattr(getattr(getattr(issue_from_jira.fields, "status", None), "statusCategory", None), "key", None) + if key: + match key: + case "new" | "indeterminate": + logger.debug("Jira issue status category is '%s', treating as active", key) + return True + case "done": + logger.debug("Jira issue status category is 'done', treating as inactive") + return False + case "undefined": + logger.debug("Jira issue status category is 'undefined', no decision possible") + case _: + logger.warning("Unknown Jira status category key '%s', falling back to resolution check", key) + + # the statusCategory is not specified or "undefined", fallback: checking if a resolution is set and evaluate it + if not hasattr(issue_from_jira.fields, "resolution") or not issue_from_jira.fields.resolution: + logger.debug("No resolution found, treating as active") return True - - # some kind of resolution is present that is not null or None - return issue_from_jira.fields.resolution == "None" + + # some kind of resolution is present that is not None + return False def push_status_to_jira(obj, jira_instance, jira, issue, *, save=False): diff --git a/unittests/test_jira_helper.py b/unittests/test_jira_helper.py new file mode 100644 index 00000000000..51ded30bacf --- /dev/null +++ b/unittests/test_jira_helper.py @@ -0,0 +1,143 @@ +import logging +from unittest.mock import Mock + +from dojo.jira_link import helper as jira_helper +from unittests.dojo_test_case import DojoTestCase + +logger = logging.getLogger(__name__) + + +class JIRAHelperTest(DojoTestCase): + + """Unit tests for JIRA helper functions""" + + def create_mock_jira_issue(self, status_category_key=None, resolution=None): + """ + Helper to create a mock JIRA issue with configurable status category and resolution. + + Args: + status_category_key: The key for statusCategory (e.g., "new", "indeterminate", "done") + resolution: Resolution value (None, "None", or a dict with resolution details) + + """ + issue = Mock() + issue.fields = Mock() + + if status_category_key is not None: + issue.fields.status = Mock() + issue.fields.status.statusCategory = Mock() + issue.fields.status.statusCategory.key = status_category_key + else: + # Simulate missing status or statusCategory + del issue.fields.status + + issue.fields.resolution = resolution + + return issue + + def test_issue_from_jira_is_active_with_new_status(self): + """Test that issues with 'new' status category are treated as active""" + issue = self.create_mock_jira_issue(status_category_key="new") + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue with 'new' status category should be active") + + def test_issue_from_jira_is_active_with_indeterminate_status(self): + """Test that issues with 'indeterminate' status category are treated as active""" + issue = self.create_mock_jira_issue(status_category_key="indeterminate") + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue with 'indeterminate' status category should be active") + + def test_issue_from_jira_is_active_with_done_status(self): + """Test that issues with 'done' status category are treated as inactive""" + issue = self.create_mock_jira_issue(status_category_key="done") + result = jira_helper.issue_from_jira_is_active(issue) + self.assertFalse(result, "Issue with 'done' status category should be inactive") + + def test_issue_from_jira_is_active_with_unknown_status_and_no_resolution(self): + """Test that issues with unknown status category fall back to resolution check""" + issue = self.create_mock_jira_issue(status_category_key="custom_status", resolution=None) + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue with unknown status and no resolution should be active") + + def test_issue_from_jira_is_active_with_unknown_status_and_resolution(self): + """Test that issues with unknown status category and resolution are treated as inactive""" + resolution = {"id": "11", "name": "Fixed"} + issue = self.create_mock_jira_issue(status_category_key="custom_status", resolution=resolution) + result = jira_helper.issue_from_jira_is_active(issue) + self.assertFalse(result, "Issue with unknown status and resolution should be inactive") + + def test_issue_from_jira_is_active_with_unknown_status_and_none_resolution(self): + """Test that issues with unknown status category and 'None' resolution are treated as active""" + issue = self.create_mock_jira_issue(status_category_key="custom_status", resolution="None") + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue with unknown status and 'None' resolution should be active") + + def test_issue_from_jira_is_active_without_status_category_and_no_resolution(self): + """Test fallback to resolution check when status category is not available""" + issue = Mock() + issue.fields = Mock() + issue.fields.resolution = None + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue without status category and no resolution should be active") + + def test_issue_from_jira_is_active_without_status_category_with_resolution(self): + """Test fallback to resolution check when status category is not available""" + issue = Mock() + issue.fields = Mock() + issue.fields.resolution = {"id": "11", "name": "Fixed"} + result = jira_helper.issue_from_jira_is_active(issue) + self.assertFalse(result, "Issue without status category but with resolution should be inactive") + + def test_issue_from_jira_is_active_without_status_category_with_none_string_resolution(self): + """Test that 'None' string resolution is treated as active""" + issue = Mock() + issue.fields = Mock() + issue.fields.resolution = "None" + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue with 'None' string resolution should be active") + + def test_issue_from_jira_is_active_without_fields(self): + """Test that issues without fields attribute fall back gracefully""" + issue = Mock(spec=[]) # Mock with no attributes + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue without fields should default to active") + + def test_issue_from_jira_is_active_with_missing_status_attribute(self): + """Test AttributeError handling when status is missing""" + issue = Mock() + issue.fields = Mock(spec=["resolution"]) # Has fields but no status + issue.fields.resolution = None + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue with missing status attribute should fall back to resolution check") + + def test_issue_from_jira_is_active_with_missing_status_category(self): + """Test AttributeError handling when statusCategory is missing""" + issue = Mock() + issue.fields = Mock() + issue.fields.status = Mock(spec=[]) # Has status but no statusCategory + issue.fields.resolution = None + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue with missing statusCategory should fall back to resolution check") + + def test_issue_from_jira_is_active_with_missing_status_category_key(self): + """Test AttributeError handling when statusCategory.key is missing""" + issue = Mock() + issue.fields = Mock() + issue.fields.status = Mock() + issue.fields.status.statusCategory = Mock(spec=[]) # Has statusCategory but no key + issue.fields.resolution = None + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Issue with missing statusCategory.key should fall back to resolution check") + + def test_issue_from_jira_is_active_status_category_takes_precedence(self): + """Test that status category takes precedence over resolution""" + # Create an issue with "done" status but no resolution + issue = self.create_mock_jira_issue(status_category_key="done", resolution=None) + result = jira_helper.issue_from_jira_is_active(issue) + self.assertFalse(result, "Status category should take precedence over resolution") + + # Create an issue with "new" status but has a resolution + resolution = {"id": "11", "name": "Fixed"} + issue = self.create_mock_jira_issue(status_category_key="new", resolution=resolution) + result = jira_helper.issue_from_jira_is_active(issue) + self.assertTrue(result, "Status category should take precedence over resolution")