diff --git a/docs/content/issue_tracking/jira/OS__jira_guide.md b/docs/content/issue_tracking/jira/OS__jira_guide.md index a9dca184057..59d9101f60f 100644 --- a/docs/content/issue_tracking/jira/OS__jira_guide.md +++ b/docs/content/issue_tracking/jira/OS__jira_guide.md @@ -159,7 +159,7 @@ Here is an example of a **jira\_full** Issue: #### Component -If you manage your Jira Space using Components, you can assign the appropriate Component for DefectDojo here. +If you manage your Jira Space using Components, you can assign the appropriate Component for DefectDojo here. To assign more than one Component, enter a comma-separated list (for example, `Security, DevSecOps`); each value is sent to Jira as a separate component. **Custom fields** diff --git a/docs/content/issue_tracking/jira/PRO__jira_guide.md b/docs/content/issue_tracking/jira/PRO__jira_guide.md index 6fe271f367a..7e7623a8f77 100644 --- a/docs/content/issue_tracking/jira/PRO__jira_guide.md +++ b/docs/content/issue_tracking/jira/PRO__jira_guide.md @@ -126,7 +126,7 @@ Here is an example of a **jira\_full** Issue: #### Component -If you manage your Jira Space using Components, you can assign the appropriate Component for DefectDojo here. +If you manage your Jira Space using Components, you can assign the appropriate Component for DefectDojo here. To assign more than one Component, enter a comma-separated list (for example, `Security, DevSecOps`); each value is sent to Jira as a separate component. #### Custom fields diff --git a/dojo/jira/forms.py b/dojo/jira/forms.py index 3ec7005d0fc..c014c92778a 100644 --- a/dojo/jira/forms.py +++ b/dojo/jira/forms.py @@ -135,6 +135,11 @@ def __init__(self, *args, **kwargs): self.engagement = kwargs.pop("engagement", None) super().__init__(*args, **kwargs) + self.fields["component"].help_text = ( + "Comma-separate multiple components to assign more than one to the JIRA issue, " + "e.g. 'Security, DevSecOps'." + ) + logger.debug("self.target: %s, self.product: %s, self.instance: %s", self.target, self.product, self.instance) logger.debug("data: %s", self.data) if self.target == "engagement": diff --git a/dojo/jira/helper.py b/dojo/jira/helper.py index 5b83a0596ab..9ce434801ea 100644 --- a/dojo/jira/helper.py +++ b/dojo/jira/helper.py @@ -867,7 +867,12 @@ def prepare_jira_issue_fields( } if component_name: - fields["components"] = [{"name": component_name}] + # The component field holds a comma-separated list of component names, so split it + # into the list of components Jira expects ([{"name": "A"}, {"name": "B"}]). A single + # value without commas yields a single component. (SC-13173) + components = [{"name": name.strip()} for name in component_name.split(",") if name.strip()] + if components: + fields["components"] = components if custom_fields: fields.update(custom_fields) diff --git a/unittests/test_jira_helper.py b/unittests/test_jira_helper.py index b90a304f7a4..ba2019f0765 100644 --- a/unittests/test_jira_helper.py +++ b/unittests/test_jira_helper.py @@ -29,3 +29,49 @@ def test_issue_from_jira_is_active_with_unknown_status(self): def test_issue_from_jira_is_active_defaults_to_active_on_missing_attribute(self): """AttributeError anywhere in the fields.status.statusCategory.key chain defaults to active.""" self.assertTrue(jira_helper.issue_from_jira_is_active(Mock(spec=[]))) + + +class JIRAComponentFieldTest(TestCase): + + """ + SC-13173: the JIRA project `component` field holds a comma-separated list of + component names. prepare_jira_issue_fields must split it into multiple Jira + components so Jira receives [{"name": "A"}, {"name": "B"}] instead of a single + component named "A,B". + """ + + def _fields(self, component_name): + return jira_helper.prepare_jira_issue_fields( + project_key="PROJ", + issuetype_name="Bug", + summary="summary", + description="description", + component_name=component_name, + ) + + def test_single_component(self): + fields = self._fields("Security") + self.assertEqual([{"name": "Security"}], fields["components"]) + + def test_multiple_components_split_on_comma(self): + fields = self._fields("Security,DevSecOps") + self.assertEqual([{"name": "Security"}, {"name": "DevSecOps"}], fields["components"]) + + def test_multiple_components_whitespace_trimmed(self): + fields = self._fields("Security, DevSecOps , Platform") + self.assertEqual( + [{"name": "Security"}, {"name": "DevSecOps"}, {"name": "Platform"}], + fields["components"], + ) + + def test_empty_entries_dropped(self): + fields = self._fields("Security,,DevSecOps,") + self.assertEqual([{"name": "Security"}, {"name": "DevSecOps"}], fields["components"]) + + def test_no_component_omits_field(self): + fields = self._fields("") + self.assertNotIn("components", fields) + + def test_only_separators_omits_field(self): + fields = self._fields(" , , ") + self.assertNotIn("components", fields)