Skip to content

Commit b921ec2

Browse files
committed
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
1 parent 19f7044 commit b921ec2

2 files changed

Lines changed: 165 additions & 19 deletions

File tree

dojo/jira_link/helper.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,28 +1219,31 @@ def get_jira_issue_from_jira(find):
12191219

12201220

12211221
def issue_from_jira_is_active(issue_from_jira):
1222-
# "resolution":{
1223-
# "self":"http://www.testjira.com/rest/api/2/resolution/11",
1224-
# "id":"11",
1225-
# "description":"Cancelled by the customer.",
1226-
# "name":"Cancelled"
1227-
# },
1228-
1229-
# or
1230-
# "resolution": null
1231-
1232-
# or
1233-
# "resolution": "None"
1234-
1235-
if not hasattr(issue_from_jira.fields, "resolution"):
1236-
logger.debug(vars(issue_from_jira))
1222+
if not hasattr(issue_from_jira, "fields"):
1223+
logger.debug("No jira data fields found, treating as active")
12371224
return True
12381225

1239-
if not issue_from_jira.fields.resolution:
1226+
key = getattr(getattr(getattr(issue_from_jira.fields, "status", None), "statusCategory", None), "key", None)
1227+
if key:
1228+
match key:
1229+
case "new" | "indeterminate":
1230+
logger.debug("Jira issue status category is '%s', treating as active", key)
1231+
return True
1232+
case "done":
1233+
logger.debug("Jira issue status category is 'done', treating as inactive")
1234+
return False
1235+
case "undefined":
1236+
logger.debug("Jira issue status category is 'undefined', no decision possible")
1237+
case _:
1238+
logger.warning("Unknown Jira status category key '%s', falling back to resolution check", key)
1239+
1240+
# the statusCategory is not specified or "undefined", fallback: checking if a resolution is set and evaluate it
1241+
if not hasattr(issue_from_jira.fields, "resolution") or not issue_from_jira.fields.resolution:
1242+
logger.debug("No resolution found, treating as active")
12401243
return True
1241-
1242-
# some kind of resolution is present that is not null or None
1243-
return issue_from_jira.fields.resolution == "None"
1244+
1245+
# some kind of resolution is present that is not None
1246+
return False
12441247

12451248

12461249
def push_status_to_jira(obj, jira_instance, jira, issue, *, save=False):

unittests/test_jira_helper.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import logging
2+
from unittest.mock import Mock
3+
4+
from dojo.jira_link import helper as jira_helper
5+
from unittests.dojo_test_case import DojoTestCase
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class JIRAHelperTest(DojoTestCase):
11+
12+
"""Unit tests for JIRA helper functions"""
13+
14+
def create_mock_jira_issue(self, status_category_key=None, resolution=None):
15+
"""
16+
Helper to create a mock JIRA issue with configurable status category and resolution.
17+
18+
Args:
19+
status_category_key: The key for statusCategory (e.g., "new", "indeterminate", "done")
20+
resolution: Resolution value (None, "None", or a dict with resolution details)
21+
22+
"""
23+
issue = Mock()
24+
issue.fields = Mock()
25+
26+
if status_category_key is not None:
27+
issue.fields.status = Mock()
28+
issue.fields.status.statusCategory = Mock()
29+
issue.fields.status.statusCategory.key = status_category_key
30+
else:
31+
# Simulate missing status or statusCategory
32+
del issue.fields.status
33+
34+
issue.fields.resolution = resolution
35+
36+
return issue
37+
38+
def test_issue_from_jira_is_active_with_new_status(self):
39+
"""Test that issues with 'new' status category are treated as active"""
40+
issue = self.create_mock_jira_issue(status_category_key="new")
41+
result = jira_helper.issue_from_jira_is_active(issue)
42+
self.assertTrue(result, "Issue with 'new' status category should be active")
43+
44+
def test_issue_from_jira_is_active_with_indeterminate_status(self):
45+
"""Test that issues with 'indeterminate' status category are treated as active"""
46+
issue = self.create_mock_jira_issue(status_category_key="indeterminate")
47+
result = jira_helper.issue_from_jira_is_active(issue)
48+
self.assertTrue(result, "Issue with 'indeterminate' status category should be active")
49+
50+
def test_issue_from_jira_is_active_with_done_status(self):
51+
"""Test that issues with 'done' status category are treated as inactive"""
52+
issue = self.create_mock_jira_issue(status_category_key="done")
53+
result = jira_helper.issue_from_jira_is_active(issue)
54+
self.assertFalse(result, "Issue with 'done' status category should be inactive")
55+
56+
def test_issue_from_jira_is_active_with_unknown_status_and_no_resolution(self):
57+
"""Test that issues with unknown status category fall back to resolution check"""
58+
issue = self.create_mock_jira_issue(status_category_key="custom_status", resolution=None)
59+
result = jira_helper.issue_from_jira_is_active(issue)
60+
self.assertTrue(result, "Issue with unknown status and no resolution should be active")
61+
62+
def test_issue_from_jira_is_active_with_unknown_status_and_resolution(self):
63+
"""Test that issues with unknown status category and resolution are treated as inactive"""
64+
resolution = {"id": "11", "name": "Fixed"}
65+
issue = self.create_mock_jira_issue(status_category_key="custom_status", resolution=resolution)
66+
result = jira_helper.issue_from_jira_is_active(issue)
67+
self.assertFalse(result, "Issue with unknown status and resolution should be inactive")
68+
69+
def test_issue_from_jira_is_active_with_unknown_status_and_none_resolution(self):
70+
"""Test that issues with unknown status category and 'None' resolution are treated as active"""
71+
issue = self.create_mock_jira_issue(status_category_key="custom_status", resolution="None")
72+
result = jira_helper.issue_from_jira_is_active(issue)
73+
self.assertTrue(result, "Issue with unknown status and 'None' resolution should be active")
74+
75+
def test_issue_from_jira_is_active_without_status_category_and_no_resolution(self):
76+
"""Test fallback to resolution check when status category is not available"""
77+
issue = Mock()
78+
issue.fields = Mock()
79+
issue.fields.resolution = None
80+
result = jira_helper.issue_from_jira_is_active(issue)
81+
self.assertTrue(result, "Issue without status category and no resolution should be active")
82+
83+
def test_issue_from_jira_is_active_without_status_category_with_resolution(self):
84+
"""Test fallback to resolution check when status category is not available"""
85+
issue = Mock()
86+
issue.fields = Mock()
87+
issue.fields.resolution = {"id": "11", "name": "Fixed"}
88+
result = jira_helper.issue_from_jira_is_active(issue)
89+
self.assertFalse(result, "Issue without status category but with resolution should be inactive")
90+
91+
def test_issue_from_jira_is_active_without_status_category_with_none_string_resolution(self):
92+
"""Test that 'None' string resolution is treated as active"""
93+
issue = Mock()
94+
issue.fields = Mock()
95+
issue.fields.resolution = "None"
96+
result = jira_helper.issue_from_jira_is_active(issue)
97+
self.assertTrue(result, "Issue with 'None' string resolution should be active")
98+
99+
def test_issue_from_jira_is_active_without_fields(self):
100+
"""Test that issues without fields attribute fall back gracefully"""
101+
issue = Mock(spec=[]) # Mock with no attributes
102+
result = jira_helper.issue_from_jira_is_active(issue)
103+
self.assertTrue(result, "Issue without fields should default to active")
104+
105+
def test_issue_from_jira_is_active_with_missing_status_attribute(self):
106+
"""Test AttributeError handling when status is missing"""
107+
issue = Mock()
108+
issue.fields = Mock(spec=["resolution"]) # Has fields but no status
109+
issue.fields.resolution = None
110+
result = jira_helper.issue_from_jira_is_active(issue)
111+
self.assertTrue(result, "Issue with missing status attribute should fall back to resolution check")
112+
113+
def test_issue_from_jira_is_active_with_missing_status_category(self):
114+
"""Test AttributeError handling when statusCategory is missing"""
115+
issue = Mock()
116+
issue.fields = Mock()
117+
issue.fields.status = Mock(spec=[]) # Has status but no statusCategory
118+
issue.fields.resolution = None
119+
result = jira_helper.issue_from_jira_is_active(issue)
120+
self.assertTrue(result, "Issue with missing statusCategory should fall back to resolution check")
121+
122+
def test_issue_from_jira_is_active_with_missing_status_category_key(self):
123+
"""Test AttributeError handling when statusCategory.key is missing"""
124+
issue = Mock()
125+
issue.fields = Mock()
126+
issue.fields.status = Mock()
127+
issue.fields.status.statusCategory = Mock(spec=[]) # Has statusCategory but no key
128+
issue.fields.resolution = None
129+
result = jira_helper.issue_from_jira_is_active(issue)
130+
self.assertTrue(result, "Issue with missing statusCategory.key should fall back to resolution check")
131+
132+
def test_issue_from_jira_is_active_status_category_takes_precedence(self):
133+
"""Test that status category takes precedence over resolution"""
134+
# Create an issue with "done" status but no resolution
135+
issue = self.create_mock_jira_issue(status_category_key="done", resolution=None)
136+
result = jira_helper.issue_from_jira_is_active(issue)
137+
self.assertFalse(result, "Status category should take precedence over resolution")
138+
139+
# Create an issue with "new" status but has a resolution
140+
resolution = {"id": "11", "name": "Fixed"}
141+
issue = self.create_mock_jira_issue(status_category_key="new", resolution=resolution)
142+
result = jira_helper.issue_from_jira_is_active(issue)
143+
self.assertTrue(result, "Status category should take precedence over resolution")

0 commit comments

Comments
 (0)