| |
@@ -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