From 0615260a368e924608c4cac76a27f3622d16023c Mon Sep 17 00:00:00 2001 From: Bernhard Willert Date: Thu, 19 Feb 2026 11:21:57 +0100 Subject: [PATCH 1/3] 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") From 4a2fa85ab5a092579106479a410e4679c0a87865 Mon Sep 17 00:00:00 2001 From: Bernhard Willert Date: Fri, 17 Apr 2026 08:58:34 +0200 Subject: [PATCH 2/3] removed trailing whitespace --- dojo/jira_link/helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index 5d10bad3ac8..90eaa34d821 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -1236,12 +1236,12 @@ def issue_from_jira_is_active(issue_from_jira): 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 None return False From 72deeed3d3ee6d3bccd248011d95401473132fd7 Mon Sep 17 00:00:00 2001 From: Bernhard Willert Date: Mon, 20 Apr 2026 11:17:32 +0200 Subject: [PATCH 3/3] Fix JIRA helper tests to comply with JIRA API specification - Remove test_issue_from_jira_is_active_with_unknown_status_and_none_resolution - Remove test_issue_from_jira_is_active_without_status_category_with_none_string_resolution These tests checked for resolution field as string 'None', which violates JIRA API spec. According to JIRA API, resolution is either an object with properties (id, name, etc) or null, never a string value. Remaining 12 tests verify correct behavior per the API spec. --- unittests/test_jira_helper.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/unittests/test_jira_helper.py b/unittests/test_jira_helper.py index 51ded30bacf..bc03dd874a3 100644 --- a/unittests/test_jira_helper.py +++ b/unittests/test_jira_helper.py @@ -66,12 +66,6 @@ def test_issue_from_jira_is_active_with_unknown_status_and_resolution(self): 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() @@ -88,14 +82,6 @@ def test_issue_from_jira_is_active_without_status_category_with_resolution(self) 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