diff --git a/jbi/actions/default.py b/jbi/actions/default.py index 45d07d65..431521f2 100644 --- a/jbi/actions/default.py +++ b/jbi/actions/default.py @@ -74,7 +74,7 @@ def __call__( # pylint: disable=inconsistent-return-statements def comment_create_or_noop(self, payload: BugzillaWebhookRequest) -> ActionResult: """Confirm issue is already linked, then apply comments; otherwise noop""" - bug_obj = payload.bugzilla_object + bug_obj = payload.bug linked_issue_key = bug_obj.extract_from_see_also() log_context = ActionLogContext( @@ -94,17 +94,17 @@ def comment_create_or_noop(self, payload: BugzillaWebhookRequest) -> ActionResul ) return False, {} - comment = payload.map_as_jira_comment() - if comment is None: + if bug_obj.comment is None: logger.debug( "No matching comment found in payload", extra=log_context.dict(), ) return False, {} + formatted_comment = payload.map_as_jira_comment() jira_response = self.jira_client.issue_add_comment( issue_key=linked_issue_key, - comment=payload.map_as_jira_comment(), + comment=formatted_comment, ) logger.debug( "Comment added to Jira issue %s", @@ -144,7 +144,7 @@ def bug_create_or_update( self, payload: BugzillaWebhookRequest ) -> ActionResult: # pylint: disable=too-many-locals """Create and link jira issue with bug, or update; rollback if multiple events fire""" - bug_obj = payload.bugzilla_object + bug_obj = payload.bug linked_issue_key = bug_obj.extract_from_see_also() # type: ignore if not linked_issue_key: return self.create_and_link_issue(payload, bug_obj) @@ -205,10 +205,8 @@ def create_and_link_issue( # pylint: disable=too-many-locals bug_obj.id, extra=log_context.dict(), ) - comment_list = self.bugzilla_client.get_comments(idlist=[bug_obj.id]) - description = comment_list["bugs"][str(bug_obj.id)]["comments"][0]["text"][ - :JIRA_DESCRIPTION_CHAR_LIMIT - ] + comment_list = self.bugzilla_client.get_comments(bug_obj.id) + description = comment_list[0].text[:JIRA_DESCRIPTION_CHAR_LIMIT] fields = { **self.jira_fields(bug_obj), # type: ignore @@ -238,7 +236,8 @@ def create_and_link_issue( # pylint: disable=too-many-locals # In the time taken to create the Jira issue the bug may have been updated so # re-retrieve it to ensure we have the latest data. - bug_obj = payload.getbug_as_bugzilla_object() + bug_obj = self.bugzilla_client.get_bug(bug_obj.id) + jira_key_in_bugzilla = bug_obj.extract_from_see_also() _duplicate_creation_event = ( jira_key_in_bugzilla is not None @@ -263,8 +262,9 @@ def create_and_link_issue( # pylint: disable=too-many-locals bug_obj.id, extra=log_context.update(operation=Operation.LINK).dict(), ) - update = self.bugzilla_client.build_update(see_also_add=jira_url) - bugzilla_response = self.bugzilla_client.update_bugs([bug_obj.id], update) + bugzilla_response = self.bugzilla_client.update_bug( + bug_obj, see_also_add=jira_url + ) bugzilla_url = f"{settings.bugzilla_base_url}/show_bug.cgi?id={bug_obj.id}" logger.debug( diff --git a/jbi/models.py b/jbi/models.py index 3a920bc9..e0899c4f 100644 --- a/jbi/models.py +++ b/jbi/models.py @@ -25,7 +25,6 @@ from jbi import Operation from jbi.errors import ActionNotFoundError -from jbi.services import get_bugzilla logger = logging.getLogger(__name__) @@ -316,13 +315,6 @@ def lookup_action(self, actions: Actions) -> Action: class BugzillaWebhookRequest(BaseModel): """Bugzilla Webhook Request Object""" - class Config: - """pydantic model config""" - - keep_untouched = ( - functools.cached_property, - ) # https://github.com/samuelcolvin/pydantic/issues/1241 - webhook_id: int webhook_name: str event: BugzillaWebhookEvent @@ -330,27 +322,9 @@ class Config: def map_as_jira_comment(self): """Extract comment from Webhook Event""" - comment: BugzillaWebhookComment = self.bug.comment commenter: BugzillaWebhookUser = self.event.user - comment_body: str = comment.body - - if comment.is_private: - bug_comments = get_bugzilla().get_comments([self.bug.id]) - comment_list = bug_comments["bugs"][str(self.bug.id)]["comments"] - matching_comments = [c for c in comment_list if c["id"] == comment.id] - if len(matching_comments) != 1: - return None - comment_body = matching_comments[0]["text"] - - body = f"*({commenter.login})* commented: \n{{quote}}{comment_body}{{quote}}" - return body - - def map_as_jira_description(self): - """Extract description as comment from Webhook Event""" comment: BugzillaWebhookComment = self.bug.comment - comment_body: str = comment.body - body = f"*(description)*: \n{{quote}}{comment_body}{{quote}}" - return body + return f"*({commenter.login})* commented: \n{{quote}}{comment.body}{{quote}}" def map_as_comments( self, @@ -380,17 +354,14 @@ def map_as_comments( return [json.dumps(comment, indent=4) for comment in comments] - def getbug_as_bugzilla_object(self) -> BugzillaBug: - """Helper method to get up to date bug data from Request.bug.id in BugzillaBug format""" - current_bug_info = get_bugzilla().getbug(self.bug.id) # type: ignore - return BugzillaBug.parse_obj(current_bug_info.__dict__) - @functools.cached_property - def bugzilla_object(self) -> BugzillaBug: - """Returns the bugzilla bug object, querying the API as needed for private bugs""" - if not self.bug.is_private: - return self.bug - return self.getbug_as_bugzilla_object() +class BugzillaComment(BaseModel): + """Bugzilla Comment""" + + id: int + text: str + is_private: bool + creator: str class BugzillaApiResponse(BaseModel): diff --git a/jbi/runner.py b/jbi/runner.py index 0b2bad88..9ef4a41b 100644 --- a/jbi/runner.py +++ b/jbi/runner.py @@ -8,7 +8,8 @@ from jbi import Operation from jbi.environment import Settings from jbi.errors import ActionNotFoundError, IgnoreInvalidRequestError -from jbi.models import Actions, BugzillaBug, BugzillaWebhookRequest, RunnerLogContext +from jbi.models import Actions, BugzillaWebhookRequest, RunnerLogContext +from jbi.services import get_bugzilla logger = logging.getLogger(__name__) @@ -38,23 +39,26 @@ def execute_action( ) try: - bug_obj: BugzillaBug = request.bugzilla_object + if request.bug.is_private: + request = request.copy( + update={"bug": get_bugzilla().get_bug(request.bug.id)} + ) except Exception as err: logger.exception("Failed to get bug: %s", err, extra=log_context.dict()) raise IgnoreInvalidRequestError( "bug not accessible or bugzilla down" ) from err - log_context = log_context.update(bug=bug_obj) + log_context = log_context.update(bug=request.bug) try: - action = bug_obj.lookup_action(actions) + action = request.bug.lookup_action(actions) except ActionNotFoundError as err: raise IgnoreInvalidRequestError( f"no action matching bug whiteboard tags: {err}" ) from err log_context = log_context.update(action=action) - if bug_obj.is_private and not action.allow_private: + if request.bug.is_private and not action.allow_private: raise IgnoreInvalidRequestError( f"private bugs are not valid for action {action.whiteboard_tag!r}" ) @@ -63,7 +67,7 @@ def execute_action( "Execute action '%s:%s' for Bug %s", action.whiteboard_tag, action.module, - bug_obj.id, + request.bug.id, extra=log_context.update(operation=Operation.EXECUTE).dict(), ) @@ -72,7 +76,7 @@ def execute_action( logger.info( "Action %r executed successfully for Bug %s", action.whiteboard_tag, - bug_obj.id, + request.bug.id, extra=log_context.update( operation=Operation.SUCCESS if handled else Operation.IGNORE ).dict(), diff --git a/jbi/services.py b/jbi/services.py index 3c00aadf..05b65d10 100644 --- a/jbi/services.py +++ b/jbi/services.py @@ -8,9 +8,11 @@ import backoff import bugzilla as rh_bugzilla from atlassian import Jira, errors +from pydantic import parse_obj_as from statsd.defaults.env import statsd from jbi import environment +from jbi.models import BugzillaBug, BugzillaComment if TYPE_CHECKING: from jbi.models import Actions @@ -84,15 +86,54 @@ def jira_visible_projects(jira=None) -> list[dict]: return projects +class BugzillaClient: + """ + Wrapper around the Bugzilla client to turn responses into our models instances. + """ + + def __init__(self, base_url: str, api_key: str): + """Constructor""" + self._client = rh_bugzilla.Bugzilla(base_url, api_key=api_key) + + @property + def logged_in(self): + """Return `true` if credentials are valid""" + return self._client.logged_in + + def get_bug(self, bugid) -> BugzillaBug: + """Return the Bugzilla object with all attributes""" + response = self._client.getbug(bugid).__dict__ + bug = BugzillaBug.parse_obj(response) + # If comment is private, then webhook does not have comment, fetch it from server + if bug.comment and bug.comment.is_private: + comment_list = self.get_comments(bugid) + matching_comments = [c for c in comment_list if c.id == bug.comment.id] + # If no matching entry is found, set `bug.comment` to `None`. + found = matching_comments[0] if matching_comments else None + bug = bug.copy(update={"comment": found}) + return bug + + def get_comments(self, bugid) -> list[BugzillaComment]: + """Return the list of comments for the specified bug ID""" + response = self._client.get_comments(idlist=[bugid]) + comments = response["bugs"][str(bugid)]["comments"] + return parse_obj_as(list[BugzillaComment], comments) + + def update_bug(self, bugid, **attrs): + """Update the specified bug with the specified attributes""" + update = self._client.build_update(**attrs) + return self._client.update_bugs([bugid], update) + + def get_bugzilla(): """Get bugzilla service""" - bugzilla_client = rh_bugzilla.Bugzilla( + bugzilla_client = BugzillaClient( settings.bugzilla_base_url, api_key=str(settings.bugzilla_api_key) ) instrumented_methods = ( - "getbug", + "get_bug", "get_comments", - "update_bugs", + "update_bug", ) return InstrumentedClient( wrapped=bugzilla_client, diff --git a/tests/conftest.py b/tests/conftest.py index 59f7c68c..4dbe653f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,9 +32,12 @@ def settings(): @pytest.fixture(autouse=True) -def mocked_bugzilla(): - with mock.patch("jbi.services.rh_bugzilla.Bugzilla") as mocked_bz: - yield mocked_bz() +def mocked_bugzilla(request): + if "no_mocked_bugzilla" in request.keywords: + yield None + else: + with mock.patch("jbi.services.BugzillaClient") as mocked_bz: + yield mocked_bz() @pytest.fixture(autouse=True) diff --git a/tests/fixtures/factories.py b/tests/fixtures/factories.py index 7d022018..6601baa8 100644 --- a/tests/fixtures/factories.py +++ b/tests/fixtures/factories.py @@ -1,6 +1,7 @@ from jbi.models import ( Action, BugzillaBug, + BugzillaComment, BugzillaWebhookEvent, BugzillaWebhookRequest, BugzillaWebhookUser, @@ -77,12 +78,14 @@ def webhook_factory(**overrides): def comment_factory(**overrides): - return { - "id": 343, - "text": "comment text", - "bug_id": 654321, - "count": 1, - "is_private": True, - "creator": "mathieu@mozilla.org", - **overrides, - } + return BugzillaComment.parse_obj( + { + "id": 343, + "text": "comment text", + "bug_id": 654321, + "count": 1, + "is_private": True, + "creator": "mathieu@mozilla.org", + **overrides, + } + ) diff --git a/tests/unit/actions/test_default.py b/tests/unit/actions/test_default.py index 4c1a56c4..29587ea3 100644 --- a/tests/unit/actions/test_default.py +++ b/tests/unit/actions/test_default.py @@ -31,7 +31,7 @@ def test_default_returns_callable_with_data( ): sentinel = mock.sentinel mocked_jira.create_or_update_issue_remote_links.return_value = sentinel - mocked_bugzilla.getbug.return_value = webhook_create_example.bug + mocked_bugzilla.get_bug.return_value = webhook_create_example.bug callable_object = default.init(jira_project_key="") handled, details = callable_object(payload=webhook_create_example) @@ -43,10 +43,10 @@ def test_default_returns_callable_with_data( def test_created_public( webhook_create_example: BugzillaWebhookRequest, mocked_jira, mocked_bugzilla ): - mocked_bugzilla.getbug.return_value = webhook_create_example.bug - mocked_bugzilla.get_comments.return_value = { - "bugs": {"654321": {"comments": [{"text": "Initial comment"}]}} - } + mocked_bugzilla.get_bug.return_value = webhook_create_example.bug + mocked_bugzilla.get_comments.return_value = [ + comment_factory(text="Initial comment") + ] callable_object = default.init(jira_project_key="JBI") callable_object(payload=webhook_create_example) @@ -62,32 +62,6 @@ def test_created_public( ) -def test_created_private( - webhook_create_private_example: BugzillaWebhookRequest, mocked_jira, mocked_bugzilla -): - fetched_private_bug = bug_factory( - id=webhook_create_private_example.bug.id, - is_private=webhook_create_private_example.bug.is_private, - ) - mocked_bugzilla.getbug.return_value = fetched_private_bug - mocked_bugzilla.get_comments.return_value = { - "bugs": {"654321": {"comments": [{"text": "Initial comment"}]}} - } - callable_object = default.init(jira_project_key="JBI") - - callable_object(payload=webhook_create_private_example) - - mocked_jira.create_issue.assert_called_once_with( - fields={ - "summary": "JBI Test", - "labels": ["bugzilla", "devtest", "[devtest]"], - "issuetype": {"name": "Bug"}, - "description": "Initial comment", - "project": {"key": "JBI"}, - }, - ) - - def test_modified_public(webhook_modify_example: BugzillaWebhookRequest, mocked_jira): assert webhook_modify_example.bug callable_object = default.init(jira_project_key="") @@ -102,25 +76,6 @@ def test_modified_public(webhook_modify_example: BugzillaWebhookRequest, mocked_ ) -def test_modified_private( - webhook_modify_private_example: BugzillaWebhookRequest, mocked_jira, mocked_bugzilla -): - fetched_private_bug = bug_factory( - id=webhook_modify_private_example.bug.id, - is_private=webhook_modify_private_example.bug.is_private, - see_also=["https://mozilla.atlassian.net/browse/JBI-234"], - ) - mocked_bugzilla.getbug.return_value = fetched_private_bug - callable_object = default.init(jira_project_key="") - - callable_object(payload=webhook_modify_private_example) - - mocked_jira.update_issue_field.assert_called_once_with( - key="JBI-234", - fields={"summary": "JBI Test", "labels": ["bugzilla", "devtest", "[devtest]"]}, - ) - - def test_added_comment(webhook_comment_example: BugzillaWebhookRequest, mocked_jira): callable_object = default.init(jira_project_key="") @@ -133,51 +88,6 @@ def test_added_comment(webhook_comment_example: BugzillaWebhookRequest, mocked_j ) -def test_added_private_comment( - webhook_private_comment_example, mocked_jira, mocked_bugzilla -): - # given - comments = [ - comment_factory(id=343, text="not this one", count=1), - comment_factory(id=344, text="hello", count=2), - comment_factory(id=345, text="not this one", count=3), - ] - - mocked_bugzilla.get_comments.return_value = { - "bugs": {"654321": {"comments": comments}}, - "comments": {}, - } - - callable_object = default.init(jira_project_key="") - - # when the default action receives a webhook with a private comment (number 2) - callable_object(payload=webhook_private_comment_example) - - # then - mocked_jira.issue_add_comment.assert_called_once_with( - issue_key="JBI-234", - comment="*(mathieu@mozilla.org)* commented: \n{quote}hello{quote}", - ) - - -def test_added_missing_private_comment( - webhook_private_comment_example: BugzillaWebhookRequest, - mocked_jira, - mocked_bugzilla, -): - - callable_object = default.init(jira_project_key="") - mocked_bugzilla.get_comments.return_value = { - "bugs": {str(webhook_private_comment_example.bug.id): {"comments": []}}, - "comments": {}, - } - - handled, _ = callable_object(payload=webhook_private_comment_example) - - mocked_jira.issue_add_comment.assert_not_called() - assert not handled - - def test_added_comment_without_linked_issue( webhook_comment_example: BugzillaWebhookRequest, mocked_jira ): @@ -207,10 +117,10 @@ def test_jira_returns_an_error( def test_disabled_label_field( webhook_create_example: BugzillaWebhookRequest, mocked_jira, mocked_bugzilla ): - mocked_bugzilla.getbug.return_value = webhook_create_example.bug - mocked_bugzilla.get_comments.return_value = { - "bugs": {"654321": {"comments": [{"text": "Initial comment"}]}} - } + mocked_bugzilla.get_bug.return_value = webhook_create_example.bug + mocked_bugzilla.get_comments.return_value = [ + comment_factory(text="Initial comment") + ] callable_object = default.init(jira_project_key="JBI", sync_whiteboard_labels=False) callable_object(payload=webhook_create_example) diff --git a/tests/unit/actions/test_default_with_assignee_and_status.py b/tests/unit/actions/test_default_with_assignee_and_status.py index a41b989f..52e09da0 100644 --- a/tests/unit/actions/test_default_with_assignee_and_status.py +++ b/tests/unit/actions/test_default_with_assignee_and_status.py @@ -1,11 +1,12 @@ from jbi.actions import default_with_assignee_and_status as action +from tests.fixtures.factories import comment_factory def test_create_with_no_assignee(webhook_create_example, mocked_jira, mocked_bugzilla): - mocked_bugzilla.getbug.return_value = webhook_create_example.bug - mocked_bugzilla.get_comments.return_value = { - "bugs": {"654321": {"comments": [{"text": "Initial comment"}]}} - } + mocked_bugzilla.get_bug.return_value = webhook_create_example.bug + mocked_bugzilla.get_comments.return_value = [ + comment_factory(text="Initial comment") + ] mocked_jira.create_issue.return_value = {"key": "new-id"} callable_object = action.init(jira_project_key="JBI") handled, _ = callable_object(payload=webhook_create_example) @@ -27,12 +28,13 @@ def test_create_with_no_assignee(webhook_create_example, mocked_jira, mocked_bug def test_create_with_assignee(webhook_create_example, mocked_jira, mocked_bugzilla): webhook_create_example.bug.assigned_to = "dtownsend@mozilla.com" - mocked_bugzilla.getbug.return_value = webhook_create_example.bug + # Make sure the bug fetched the second time in `create_and_link_issue()` also has the assignee. + mocked_bugzilla.get_bug.return_value = webhook_create_example.bug mocked_jira.create_issue.return_value = {"key": "JBI-534"} mocked_jira.user_find_by_user_string.return_value = [{"accountId": "6254"}] - mocked_bugzilla.get_comments.return_value = { - "bugs": {"654321": {"comments": [{"text": "Initial comment"}]}} - } + mocked_bugzilla.get_comments.return_value = [ + comment_factory(text="Initial comment") + ] callable_object = action.init(jira_project_key="JBI") callable_object(payload=webhook_create_example) @@ -118,10 +120,10 @@ def test_create_with_unknown_status( ): webhook_create_example.bug.status = "NEW" webhook_create_example.bug.resolution = "" - mocked_bugzilla.getbug.return_value = webhook_create_example.bug - mocked_bugzilla.get_comments.return_value = { - "bugs": {"654321": {"comments": [{"text": "Initial comment"}]}} - } + mocked_bugzilla.get_bug.return_value = webhook_create_example.bug + mocked_bugzilla.get_comments.return_value = [ + comment_factory(text="Initial comment") + ] mocked_jira.create_issue.return_value = {"key": "new-id"} callable_object = action.init( @@ -150,14 +152,13 @@ def test_create_with_unknown_status( def test_create_with_known_status(webhook_create_example, mocked_jira, mocked_bugzilla): webhook_create_example.bug.status = "ASSIGNED" webhook_create_example.bug.resolution = "" - + # Make sure the bug fetched the second time in `create_and_link_issue()` also has the status. + mocked_bugzilla.get_bug.return_value = webhook_create_example.bug + mocked_bugzilla.get_comments.return_value = [ + comment_factory(text="Initial comment") + ] mocked_jira.create_issue.return_value = {"key": "JBI-534"} - mocked_bugzilla.getbug.return_value = webhook_create_example.bug - mocked_bugzilla.get_comments.return_value = { - "bugs": {"654321": {"comments": [{"text": "Initial comment"}]}} - } - callable_object = action.init( jira_project_key="JBI", status_map={ diff --git a/tests/unit/test_bugzilla.py b/tests/unit/test_bugzilla.py index 53840b22..da7374ea 100644 --- a/tests/unit/test_bugzilla.py +++ b/tests/unit/test_bugzilla.py @@ -66,11 +66,6 @@ def test_lookup_action_missing(actions_example): assert str(exc_info.value) == "example devtest" -def test_map_jira_description(webhook_comment_example): - desc = webhook_comment_example.map_as_jira_description() - assert desc == "*(description)*: \n{quote}hello{quote}" - - def test_map_as_comments(webhook_change_status_assignee): mapped = webhook_change_status_assignee.map_as_comments( status_log_enabled=True, assignee_log_enabled=True @@ -101,35 +96,3 @@ def test_payload_changes_list_in_routing_key(webhook_change_status_assignee): "assigned_to", "status", ] - - -def test_payload_bugzilla_object_public(mocked_bugzilla, webhook_modify_example): - bug_obj = webhook_modify_example.bugzilla_object - mocked_bugzilla.getbug.assert_not_called() - assert bug_obj.product == "JBI" - assert bug_obj.status == "NEW" - assert webhook_modify_example.bug == bug_obj - - -def test_bugzilla_object_private(mocked_bugzilla, webhook_modify_private_example): - # given - fetched_private_bug = bug_factory( - id=webhook_modify_private_example.bug.id, - is_private=webhook_modify_private_example.bug.is_private, - see_also=["https://mozilla.atlassian.net/browse/JBI-234"], - ) - mocked_bugzilla.getbug.return_value = fetched_private_bug - - # when a private bug's bugzilla_object property is accessed - webhook_modify_private_example.bugzilla_object - # then - mocked_bugzilla.getbug.assert_called_once_with( - webhook_modify_private_example.bug.id - ) - assert fetched_private_bug.product == "JBI" - assert fetched_private_bug.status == "NEW" - - # when it is accessed again - fetched_private_bug = webhook_modify_private_example.bugzilla_object - # getbug was still only called once - mocked_bugzilla.getbug.assert_called_once() diff --git a/tests/unit/test_router.py b/tests/unit/test_router.py index 25d9b178..ef9557bc 100644 --- a/tests/unit/test_router.py +++ b/tests/unit/test_router.py @@ -66,8 +66,8 @@ def test_webhook_is_200_if_action_succeeds( mocked_jira, mocked_bugzilla, ): - mocked_bugzilla.getbug.return_value = webhook_create_example.bug - mocked_bugzilla.update_bugs.return_value = { + mocked_bugzilla.get_bug.return_value = webhook_create_example.bug + mocked_bugzilla.update_bug.return_value = { "bugs": [ { "changes": { diff --git a/tests/unit/test_runner.py b/tests/unit/test_runner.py index ef3b7c7e..9cc3910c 100644 --- a/tests/unit/test_runner.py +++ b/tests/unit/test_runner.py @@ -23,7 +23,7 @@ def test_request_is_ignored_because_private( mocked_bugzilla, ): bug = bug_factory(id=webhook_create_private_example.bug.id, is_private=True) - mocked_bugzilla.getbug.return_value = bug + mocked_bugzilla.get_bug.return_value = bug with pytest.raises(IgnoreInvalidRequestError) as exc_info: execute_action( request=webhook_create_private_example, @@ -41,7 +41,7 @@ def test_private_request_is_allowed( mocked_bugzilla, ): bug = bug_factory(id=webhook_create_private_example.bug.id, is_private=True) - mocked_bugzilla.getbug.return_value = bug + mocked_bugzilla.get_bug.return_value = bug actions_example["devtest"].allow_private = True @@ -215,3 +215,30 @@ def test_counter_is_incremented_on_processed_requests( settings=settings, ) mocked.incr.assert_called_with("jbi.bugzilla.processed.count") + + +def test_bugzilla_object_is_fetched_when_private( + mocked_bugzilla, + webhook_modify_private_example, + actions_example: Actions, + settings: Settings, +): + fetched_private_bug = bug_factory( + id=webhook_modify_private_example.bug.id, + is_private=webhook_modify_private_example.bug.is_private, + see_also=["https://mozilla.atlassian.net/browse/JBI-234"], + ) + mocked_bugzilla.get_bug.return_value = fetched_private_bug + actions_example["devtest"].allow_private = True + + # when the runner executes a private bug + execute_action( + request=webhook_modify_private_example, + actions=actions_example, + settings=settings, + ) + + # then + mocked_bugzilla.get_bug.assert_called_once_with( + webhook_modify_private_example.bug.id + ) diff --git a/tests/unit/test_services.py b/tests/unit/test_services.py index 53673c04..6515bc34 100644 --- a/tests/unit/test_services.py +++ b/tests/unit/test_services.py @@ -7,6 +7,7 @@ import pytest from jbi.services import get_bugzilla, get_jira +from tests.fixtures.factories import comment_factory def test_counter_is_incremented_on_jira_create_issue(): @@ -36,14 +37,73 @@ def test_timer_is_used_on_bugzilla_getcomments(): mocked.timer.assert_called_with("jbi.bugzilla.methods.get_comments.timer") +@pytest.mark.no_mocked_bugzilla def test_bugzilla_methods_are_retried_if_raising(): - with mock.patch( - "jbi.services.rh_bugzilla.Bugzilla.return_value.get_comments" - ) as mocked: - mocked.side_effect = (bugzilla.BugzillaError("boom"), [mock.sentinel]) + with mock.patch("jbi.services.rh_bugzilla.Bugzilla") as mocked_bugzilla: + mocked_bugzilla().get_comments.side_effect = [ + bugzilla.BugzillaError("boom"), + {"bugs": {"42": {"comments": []}}}, + ] bugzilla_client = get_bugzilla() # Not raising - bugzilla_client.get_comments([]) - - assert mocked.call_count == 2 + bugzilla_client.get_comments(42) + + assert mocked_bugzilla().get_comments.call_count == 2 + + +@pytest.mark.no_mocked_bugzilla +def test_bugzilla_get_bug_comment(webhook_private_comment_example): + # given + with mock.patch("jbi.services.rh_bugzilla.Bugzilla") as mocked_bugzilla: + mocked_bugzilla().getbug.return_value = webhook_private_comment_example.bug + + comments = [ + { + "id": 343, + "text": "not this one", + "is_private": False, + "creator": "mathieu@mozilla.org", + }, + { + "id": 344, + "text": "hello", + "is_private": False, + "creator": "mathieu@mozilla.org", + }, + { + "id": 345, + "text": "not this one", + "is_private": False, + "creator": "mathieu@mozilla.org", + }, + ] + mocked_bugzilla().get_comments.return_value = { + "bugs": { + str(webhook_private_comment_example.bug.id): {"comments": comments} + }, + "comments": {}, + } + + expanded = get_bugzilla().get_bug(webhook_private_comment_example.bug.id) + + # then + assert expanded.comment.creator == "mathieu@mozilla.org" + assert expanded.comment.text == "hello" + + +@pytest.mark.no_mocked_bugzilla +def test_bugzilla_missing_private_comment( + webhook_private_comment_example, +): + with mock.patch("jbi.services.rh_bugzilla.Bugzilla") as mocked_bugzilla: + mocked_bugzilla().getbug.return_value = webhook_private_comment_example.bug + + mocked_bugzilla().get_comments.return_value = { + "bugs": {str(webhook_private_comment_example.bug.id): {"comments": []}}, + "comments": {}, + } + + expanded = get_bugzilla().get_bug(webhook_private_comment_example.bug.id) + + assert not expanded.comment