diff --git a/jbi/bugzilla/models.py b/jbi/bugzilla/models.py index 845a0535..a474c255 100644 --- a/jbi/bugzilla/models.py +++ b/jbi/bugzilla/models.py @@ -173,11 +173,15 @@ def is_assigned(self) -> bool: return self.assigned_to != "nobody@mozilla.org" def extract_from_see_also(self, project_key): - """Extract Jira Issue Key from see_also if jira url present""" + """Extract Jira Issue Key from see_also if jira url present for the specified project. + + Returns the Jira issue key only if it matches the specified project_key. + If a see_also link points to a different Jira project, it will not be returned, + allowing a new ticket to be created for the specified project. + """ if not self.see_also or len(self.see_also) == 0: return None - candidates = [] for url in self.see_also: try: parsed_url: ParseResult = urlparse(url=url) @@ -199,12 +203,11 @@ def extract_from_see_also(self, project_key): parsed_jira_key = parsed_url.path.rstrip("/").split("/")[-1] if parsed_jira_key: # URL ending with / # Issue keys are like `{project_key}-{number}` + # Only return if the key matches the specified project if parsed_jira_key.startswith(f"{project_key}-"): return parsed_jira_key - # If not obvious, then keep this link as candidate. - candidates.append(parsed_jira_key) - return candidates[0] if candidates else None + return None class WebhookRequest(BaseModel, frozen=True): diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 3958c9d5..d7977e30 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -64,27 +64,36 @@ def test_override_step_configuration_for_single_action_type(): ([], None), (["foo"], None), (["fail:/format"], None), - (["foo", "http://jira.net/123"], "123"), + # Non-matching project keys should return None + (["foo", "http://jira.net/123"], None), (["http://org/123"], None), (["http://jira.com"], None), (["http://mozilla.jira.com/"], None), - (["http://mozilla.jira.com/123"], "123"), - (["http://mozilla.jira.com/123/"], "123"), - (["http://mozilla.jira.com/ticket/123"], "123"), - (["http://atlassian.com/ticket/123"], "123"), - (["http://mozilla.jira.com/123", "http://mozilla.jira.com/456"], "123"), + (["http://mozilla.jira.com/123"], None), + (["http://mozilla.jira.com/123/"], None), + (["http://mozilla.jira.com/ticket/123"], None), + (["http://atlassian.com/ticket/123"], None), + (["http://mozilla.jira.com/123", "http://mozilla.jira.com/456"], None), + # Multiple Jira issues from different projects should return None if none match ( ["http://mozilla.jira.com/FOO-123", "http://mozilla.jira.com/BAR-456"], - "FOO-123", + None, ), + # Issue keys that don't match the project format should return None ( ["http://mozilla.jira.com/FOO-123", "http://mozilla.jira.com/JBI456"], - "FOO-123", + None, ), + # Only return issue key if it matches the specified project ( ["http://mozilla.jira.com/FOO-123", "http://mozilla.jira.com/JBI-456"], "JBI-456", ), + # Test the specific scenario: BZFFX issue shouldn't prevent GENAI creation + ( + ["http://mozilla.jira.com/BZFFX-123"], + None, + ), ], ) def test_extract_see_also(see_also, expected, bug_factory): @@ -92,6 +101,30 @@ def test_extract_see_also(see_also, expected, bug_factory): assert bug.extract_from_see_also("JBI") == expected +def test_extract_see_also_different_projects(bug_factory): + """Test that a bug with a BZFFX issue can still match GENAI project.""" + bug = bug_factory(see_also=["http://mozilla.jira.com/browse/BZFFX-123"]) + # When looking for GENAI project, should return None (allowing new ticket creation) + assert bug.extract_from_see_also("GENAI") is None + # When looking for BZFFX project, should return the issue key + assert bug.extract_from_see_also("BZFFX") == "BZFFX-123" + + +def test_extract_see_also_multiple_projects(bug_factory): + """Test that extract_from_see_also correctly handles bugs linked to multiple projects.""" + bug = bug_factory( + see_also=[ + "http://mozilla.jira.com/browse/BZFFX-123", + "http://mozilla.jira.com/browse/GENAI-456", + ] + ) + # Each project should only match its own issue + assert bug.extract_from_see_also("BZFFX") == "BZFFX-123" + assert bug.extract_from_see_also("GENAI") == "GENAI-456" + # Non-matching project should return None + assert bug.extract_from_see_also("FOOBAR") is None + + @pytest.mark.parametrize( "product,component,expected", [ diff --git a/tests/unit/test_runner.py b/tests/unit/test_runner.py index cc867ee6..3cb18c84 100644 --- a/tests/unit/test_runner.py +++ b/tests/unit/test_runner.py @@ -276,7 +276,9 @@ def test_runner_ignores_request_if_jira_is_linked_but_without_whiteboard( ) mocked_bugzilla.get_bug.return_value = webhook.bug - assert webhook.bug.extract_from_see_also(project_key="foo") is not None + # Verify that the bug has a JBI link (matching project), but not for other projects + assert webhook.bug.extract_from_see_also(project_key="JBI") == "JBI-234" + assert webhook.bug.extract_from_see_also(project_key="foo") is None with pytest.raises(IgnoreInvalidRequestError) as exc_info: execute_action(request=webhook, actions=actions) diff --git a/tests/unit/test_steps.py b/tests/unit/test_steps.py index 36fba724..73968aef 100644 --- a/tests/unit/test_steps.py +++ b/tests/unit/test_steps.py @@ -1378,8 +1378,7 @@ def test_maybe_update_components_create_components_normal_component( ) mocked_jira.create_component.assert_called_once_with( - project=action_context.jira.project, - name="NewComponent", + {"project": action_context.jira.project, "name": "NewComponent"} ) mocked_jira.update_issue_field.assert_called_with( key="JBI-234", @@ -1419,8 +1418,7 @@ def test_maybe_update_components_create_components_prefix_component( ) mocked_jira.create_component.assert_called_once_with( - project=action_context.jira.project, - name="Firefox::NewComponent", + {"project": action_context.jira.project, "name": "Firefox::NewComponent"} ) mocked_jira.update_issue_field.assert_called_with( key="JBI-234",