#167 discussions: ignore closed tickets
Merged 3 years ago by kparal. Opened 3 years ago by kparal.

@@ -133,17 +133,23 @@ 

          app.logger.debug(msg)

          return JsonResponse({'msg': msg})

  

-     issue_id = data.get('msg', {}).get('issue', {}).get('id', {})

-     if issue_id:

-         pagure_bot.webhook_handler(issue_id)

-         msg = 'Message successfully parsed'

+     issue_id = data.get('msg', {}).get('issue', {}).get('id', None)

+     status = data.get('msg', {}).get('issue', {}).get('status', '')

+     if not issue_id or not status:

+         msg = 'Unable to parse received message (isssue id, status)'

          app.logger.debug(msg)

          return JsonResponse({'msg': msg})

-     else:

-         msg = 'Unable to parse issue id from received message'

+ 

+     if status == 'Closed':

+         msg = 'Ignoring a closed issue'

          app.logger.debug(msg)

          return JsonResponse({'msg': msg})

  

+     pagure_bot.webhook_handler(issue_id)

+     msg = 'Message successfully parsed'

+     app.logger.debug(msg)

+     return JsonResponse({'msg': msg})

+ 

  

  def _svg_response_text(info_all):

      svg_template = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="{total_height}" width="300">'\

@@ -115,8 +115,13 @@ 

                          app.logger.debug(f"[DRY RUN] Closing issue #{issue_id}")

                      else:

                          app.logger.debug(f"Closing issue #{issue_id}")

-                         pagure_interface.post_comment(issue_id, comment)

+                         # Close first, then submit a comment. While it doesn't look that pretty in

+                         # this order, closing old tickets occurs when a release is over, and a new

+                         # version of BBA might have been deployed by then. We don't want to process

+                         # the old tickets with some new logic. By closing them first, we make sure

+                         # to not process them anymore.  #truestory #realworld

                          pagure_interface.close_issue(issue_id)

+                         pagure_interface.post_comment(issue_id, comment)

                  except pagure_interface.PagureAPIException as e:

                      app.logger.error(f'Unable to close Pagure discussion #{issue_id} for bug '

                                       f'{bug.bugid}. Pagure error: {e}')

file modified
+120 -2
@@ -1,12 +1,13 @@ 

  from datetime import datetime

  import json

+ import copy

  # First, try to import modern http.client and support also old one on EPEL 7/Python 2

  try:

      import http.client as httplib

  except ImportError:

      import httplib

  

- from mock import patch

+ import mock

  

  from blockerbugs import db

  from blockerbugs import app
@@ -15,8 +16,9 @@ 

  from blockerbugs.models.userinfo import UserInfo

  from testing.test_controllers import add_release, add_milestone, \

      add_bug, add_update

- from blockerbugs.controllers.api import errors

+ from blockerbugs.controllers.api import api, errors

  from blockerbugs.controllers.api.api import _get_bugtypes, _UNKNOWN_BUG_SVG_TEXT, _BUG_CLOSED

+ from blockerbugs.util import pagure_bot

  

  

  class TestRestAPI(object):
@@ -57,6 +59,17 @@ 

          build = Build()

          build.koji_id = 33

          build.nvr = 'libofx-0.9.9-1.fc20'

+ 

+         cls.webhook_data = {

+             'msg': {

+                 'issue': {

+                     'id': 6666,

+                     'status': 'Open',

+                 },

+             },

+             'topic': 'issue.comment.added',

+         }

+ 

          cls.api_user = UserInfo('api_user')

          passwd = cls.api_user.generate_api_key()

          cls.headers = {'X-Auth-User': 'api_user', 'X-Auth-Key': passwd}
@@ -68,6 +81,8 @@ 

          db.session.rollback()

          db.drop_all()

  

+     # === /api/v0/milestones ===

+ 

      def test_unknown_release(self):

          url = '/api/v0/milestones/9999/unknown/bugs'

          resp = self.client.get(url)
@@ -151,6 +166,8 @@ 

          data = json.loads(resp.data)

          assert data['name'] == self.milestone.name

  

+     # === /api/v0/bugimg ===

+ 

      def test_get_bugimg(self):

          url = '/api/v0/bugimg/9002'

          resp = self.client.get(url)
@@ -197,3 +214,104 @@ 

          assert '99-beta' in data

          assert '99-final' in data

          assert _BUG_CLOSED not in data

+ 

+     # === /api/v0/webhook ===

+ 

+     def test_webhook_bot_disabled(self, monkeypatch):

+         url = '/api/v0/webhook'

+         mock_webhook_handler = mock.MagicMock()

+         monkeypatch.setattr(pagure_bot, 'webhook_handler', mock_webhook_handler)

+         monkeypatch.setattr(api, 'check_signature', mock.MagicMock(return_value=True))

+         monkeypatch.setitem(app.config, 'PAGURE_BOT_ENABLED', False)

+ 

+         resp = self.client.post(url, json=self.webhook_data)

+         assert resp.status_code == httplib.OK

+         respdata = json.loads(resp.data)

+ 

+         assert not mock_webhook_handler.called

+         assert respdata['msg'] == 'Pagure bot disabled, ignoring request'

+ 

+     def test_webhook_bad_signature(self, monkeypatch):

+         # If we don't mock the signature, it doesn't match

+         url = '/api/v0/webhook'

+         mock_webhook_handler = mock.MagicMock()

+         monkeypatch.setattr(pagure_bot, 'webhook_handler', mock_webhook_handler)

+ 

+         resp = self.client.post(url, json=self.webhook_data)

+         assert resp.status_code == httplib.OK

+         respdata = json.loads(resp.data)

+         print(respdata['msg'])

+ 

+         assert not mock_webhook_handler.called

+         assert respdata['msg'] == 'Wrong signature, ignoring.'

+ 

+     def test_webhook_wrong_topic(self, monkeypatch):

+         url = '/api/v0/webhook'

+         mock_webhook_handler = mock.MagicMock()

+         monkeypatch.setattr(pagure_bot, 'webhook_handler', mock_webhook_handler)

+         monkeypatch.setattr(api, 'check_signature', mock.MagicMock(return_value=True))

+         monkeypatch.setitem(self.webhook_data, 'topic', 'test.invalid.topic')

+ 

+         resp = self.client.post(url, json=self.webhook_data)

+         assert resp.status_code == httplib.OK

+         respdata = json.loads(resp.data)

+ 

+         assert not mock_webhook_handler.called

+         assert respdata['msg'].startswith('Ignoring message with topic')

+ 

+     def test_webhook_missing_fields(self, monkeypatch):

+         url = '/api/v0/webhook'

+         mock_webhook_handler = mock.MagicMock()

+         monkeypatch.setattr(pagure_bot, 'webhook_handler', mock_webhook_handler)

+         monkeypatch.setattr(api, 'check_signature', mock.MagicMock(return_value=True))

+ 

+         for field in ['id', 'status']:

+             webhook_data = copy.deepcopy(self.webhook_data)

+             del webhook_data['msg']['issue'][field]

+ 

+             resp = self.client.post(url, json=webhook_data)

+             assert resp.status_code == httplib.OK

+             respdata = json.loads(resp.data)

+ 

+             assert not mock_webhook_handler.called

+             assert respdata['msg'].startswith('Unable to parse received message')

+ 

+         for field in ['id', 'status']:

+             for value in ['', None]:

+                 webhook_data = copy.deepcopy(self.webhook_data)

+                 webhook_data['msg']['issue'][field] = value

+ 

+                 resp = self.client.post(url, json=webhook_data)

+                 assert resp.status_code == httplib.OK

+                 respdata = json.loads(resp.data)

+ 

+                 assert not mock_webhook_handler.called

+                 assert respdata['msg'].startswith('Unable to parse received message')

+ 

+     def test_webhook_closed_issue(self, monkeypatch):

+         url = '/api/v0/webhook'

+         mock_webhook_handler = mock.MagicMock()

+         monkeypatch.setattr(pagure_bot, 'webhook_handler', mock_webhook_handler)

+         monkeypatch.setattr(api, 'check_signature', mock.MagicMock(return_value=True))

+         monkeypatch.setitem(self.webhook_data['msg']['issue'], 'status', 'Closed')

+ 

+         resp = self.client.post(url, json=self.webhook_data)

+         assert resp.status_code == httplib.OK

+         respdata = json.loads(resp.data)

+ 

+         assert not mock_webhook_handler.called

+         assert respdata['msg'] == 'Ignoring a closed issue'

+ 

+     def test_webhook_executed(self, monkeypatch):

+         url = '/api/v0/webhook'

+         mock_webhook_handler = mock.MagicMock()

+         monkeypatch.setattr(pagure_bot, 'webhook_handler', mock_webhook_handler)

+         monkeypatch.setattr(api, 'check_signature', mock.MagicMock(return_value=True))

+ 

+         resp = self.client.post(url, json=self.webhook_data)

+         assert resp.status_code == httplib.OK

+         respdata = json.loads(resp.data)

+ 

+         assert mock_webhook_handler.call_count == 1

+         assert mock_webhook_handler.call_args.args == (self.webhook_data['msg']['issue']['id'],)

+         assert respdata['msg'] == 'Message successfully parsed'

I believed we ignored closed tickets already, but we probably forgot about it.
It is implemented now.

At the same time, when we close old tickets, make sure we close it first and
post a comment later, otherwise we still trigger the webhook and we often don't
want to do that.

As a bonus, cover the whole webhook API with tests.


Yesterday I wanted to close old F33 tickets in production, and found out that in some cases it recalculates the votes (see here) and probably always it recreates the ticket description. This patch should avoid that, and simply close the old tickets without further processing.

@lbrabec, please review, thanks

Build succeeded.

Seems okay, nice testing coverage ❤️👏

Commit 578aab6 fixes this pull-request

Pull-Request has been merged by kparal

3 years ago