Skip to content

Commit bfaa463

Browse files
committed
Merge remote-tracking branch 'origin/master'
2 parents 2c0f7c1 + 1a4590b commit bfaa463

File tree

6 files changed

+230
-58
lines changed

6 files changed

+230
-58
lines changed

.travis.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
dist: xenial
2+
language: python
3+
python:
4+
- "2.7"
5+
- "3.6"
6+
- "3.7"
7+
- "pypy"
8+
- "pypy3"
9+
install:
10+
- pip install tox-travis
11+
script:
12+
- tox
13+
matrix:
14+
include:
15+
- python: "3.7"
16+
script: black --line-length 120 --check github_webhook tests setup.py
17+
install: pip install black

github_webhook/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
:license: Apache License, Version 2.0
99
"""
1010

11+
<<<<<<< HEAD
1112
from textwrap import dedent
1213

1314
import sys
@@ -18,6 +19,9 @@
1819
version=sys.version_info[0])))
1920

2021
from github_webhook.webhook import Webhook
22+
=======
23+
from github_webhook.webhook import Webhook # noqa
24+
>>>>>>> origin/master
2125

2226
# -----------------------------------------------------------------------------
2327
# Copyright 2015 Bloomberg Finance L.P.

github_webhook/webhook.py

Lines changed: 46 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,16 @@ class Webhook(object):
1515
:param secret: Optional secret, used to authenticate the hook comes from Github
1616
"""
1717

18-
def __init__(self, app, endpoint='/postreceive', secret=None):
19-
app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive,
20-
methods=['POST'])
18+
def __init__(self, app, endpoint="/postreceive", secret=None):
19+
app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"])
2120

2221
self._hooks = collections.defaultdict(list)
2322
self._logger = logging.getLogger('webhook')
2423
if secret is not None and not isinstance(secret, bytes):
2524
secret = secret.encode('utf-8')
2625
self._secret = secret
2726

28-
def hook(self, event_type='push'):
27+
def hook(self, event_type="push"):
2928
"""
3029
Registers a function as a hook. Multiple hooks can be registered for a given type, but the
3130
order in which they are invoke is unspecified.
@@ -42,8 +41,7 @@ def decorator(func):
4241
def _get_digest(self):
4342
"""Return message digest if a secret key was provided"""
4443

45-
return hmac.new(
46-
self._secret, request.data, hashlib.sha1).hexdigest() if self._secret else None
44+
return hmac.new(self._secret, request.data, hashlib.sha1).hexdigest() if self._secret else None
4745

4846
def _postreceive(self):
4947
"""Callback from Flask"""
@@ -55,86 +53,76 @@ def _postreceive(self):
5553
if not isinstance(digest, str):
5654
digest = str(digest)
5755

58-
if (len(sig_parts) < 2 or sig_parts[0] != 'sha1'
59-
or not hmac.compare_digest(sig_parts[1], digest)):
60-
abort(400, 'Invalid signature')
56+
if len(sig_parts) < 2 or sig_parts[0] != "sha1" or not hmac.compare_digest(sig_parts[1], digest):
57+
abort(400, "Invalid signature")
6158

62-
event_type = _get_header('X-Github-Event')
59+
event_type = _get_header("X-Github-Event")
6360
data = request.get_json()
6461

6562
if data is None:
66-
abort(400, 'Request body must contain json')
63+
abort(400, "Request body must contain json")
6764

68-
self._logger.info(
69-
'%s (%s)', _format_event(event_type, data), _get_header('X-Github-Delivery'))
65+
self._logger.info("%s (%s)", _format_event(event_type, data), _get_header("X-Github-Delivery"))
7066

7167
for hook in self._hooks.get(event_type, []):
7268
resp = hook(data)
7369
if resp: # Allow hook to respond if necessary
7470
return resp
7571

76-
return '', 204
72+
return "", 204
73+
7774

7875
def _get_header(key):
7976
"""Return message header"""
8077

8178
try:
8279
return request.headers[key]
8380
except KeyError:
84-
abort(400, 'Missing header: ' + key)
81+
abort(400, "Missing header: " + key)
82+
8583

8684
EVENT_DESCRIPTIONS = {
87-
'commit_comment': '{comment[user][login]} commented on '
88-
'{comment[commit_id]} in {repository[full_name]}',
89-
'create': '{sender[login]} created {ref_type} ({ref}) in '
90-
'{repository[full_name]}',
91-
'delete': '{sender[login]} deleted {ref_type} ({ref}) in '
92-
'{repository[full_name]}',
93-
'deployment': '{sender[login]} deployed {deployment[ref]} to '
94-
'{deployment[environment]} in {repository[full_name]}',
95-
'deployment_status': 'deployment of {deployement[ref]} to '
96-
'{deployment[environment]} '
97-
'{deployment_status[state]} in '
98-
'{repository[full_name]}',
99-
'fork': '{forkee[owner][login]} forked {forkee[name]}',
100-
'gollum': '{sender[login]} edited wiki pages in {repository[full_name]}',
101-
'issue_comment': '{sender[login]} commented on issue #{issue[number]} '
102-
'in {repository[full_name]}',
103-
'issues': '{sender[login]} {action} issue #{issue[number]} in '
104-
'{repository[full_name]}',
105-
'member': '{sender[login]} {action} member {member[login]} in '
106-
'{repository[full_name]}',
107-
'membership': '{sender[login]} {action} member {member[login]} to team '
108-
'{team[name]} in {repository[full_name]}',
109-
'page_build': '{sender[login]} built pages in {repository[full_name]}',
110-
'ping': 'ping from {sender[login]}',
111-
'public': '{sender[login]} publicized {repository[full_name]}',
112-
'pull_request': '{sender[login]} {action} pull #{pull_request[number]} in '
113-
'{repository[full_name]}',
114-
'pull_request_review': '{sender[login]} {action} {review[state]} review on pull #{pull_request[number]} in '
115-
'{repository[full_name]}',
116-
'pull_request_review_comment': '{comment[user][login]} {action} comment '
117-
'on pull #{pull_request[number]} in '
118-
'{repository[full_name]}',
119-
'push': '{pusher[name]} pushed {ref} in {repository[full_name]}',
120-
'release': '{release[author][login]} {action} {release[tag_name]} in '
121-
'{repository[full_name]}',
122-
'repository': '{sender[login]} {action} repository '
123-
'{repository[full_name]}',
124-
'status': '{sender[login]} set {sha} status to {state} in '
125-
'{repository[full_name]}',
126-
'team_add': '{sender[login]} added repository {repository[full_name]} to '
127-
'team {team[name]}',
128-
'watch': '{sender[login]} {action} watch in repository '
129-
'{repository[full_name]}'
85+
"commit_comment": "{comment[user][login]} commented on " "{comment[commit_id]} in {repository[full_name]}",
86+
"create": "{sender[login]} created {ref_type} ({ref}) in " "{repository[full_name]}",
87+
"delete": "{sender[login]} deleted {ref_type} ({ref}) in " "{repository[full_name]}",
88+
"deployment": "{sender[login]} deployed {deployment[ref]} to "
89+
"{deployment[environment]} in {repository[full_name]}",
90+
"deployment_status": "deployment of {deployement[ref]} to "
91+
"{deployment[environment]} "
92+
"{deployment_status[state]} in "
93+
"{repository[full_name]}",
94+
"fork": "{forkee[owner][login]} forked {forkee[name]}",
95+
"gollum": "{sender[login]} edited wiki pages in {repository[full_name]}",
96+
"issue_comment": "{sender[login]} commented on issue #{issue[number]} " "in {repository[full_name]}",
97+
"issues": "{sender[login]} {action} issue #{issue[number]} in " "{repository[full_name]}",
98+
"member": "{sender[login]} {action} member {member[login]} in " "{repository[full_name]}",
99+
"membership": "{sender[login]} {action} member {member[login]} to team " "{team[name]} in {repository[full_name]}",
100+
"page_build": "{sender[login]} built pages in {repository[full_name]}",
101+
"ping": "ping from {sender[login]}",
102+
"public": "{sender[login]} publicized {repository[full_name]}",
103+
"pull_request": "{sender[login]} {action} pull #{pull_request[number]} in " "{repository[full_name]}",
104+
"pull_request_review": "{sender[login]} {action} {review[state]} "
105+
"review on pull #{pull_request[number]} in "
106+
"{repository[full_name]}",
107+
"pull_request_review_comment": "{comment[user][login]} {action} comment "
108+
"on pull #{pull_request[number]} in "
109+
"{repository[full_name]}",
110+
"push": "{pusher[name]} pushed {ref} in {repository[full_name]}",
111+
"release": "{release[author][login]} {action} {release[tag_name]} in " "{repository[full_name]}",
112+
"repository": "{sender[login]} {action} repository " "{repository[full_name]}",
113+
"status": "{sender[login]} set {sha} status to {state} in " "{repository[full_name]}",
114+
"team_add": "{sender[login]} added repository {repository[full_name]} to " "team {team[name]}",
115+
"watch": "{sender[login]} {action} watch in repository " "{repository[full_name]}",
130116
}
131117

118+
132119
def _format_event(event_type, data):
133120
try:
134121
return EVENT_DESCRIPTIONS[event_type].format(**data)
135122
except KeyError:
136123
return event_type
137124

125+
138126
# -----------------------------------------------------------------------------
139127
# Copyright 2015 Bloomberg Finance L.P.
140128
#

tests/__init__.py

Whitespace-only changes.

tests/test_webhook.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Tests for github_webhook.webhook"""
2+
3+
from __future__ import print_function
4+
5+
import pytest
6+
import werkzeug
7+
8+
try:
9+
from unittest import mock
10+
except ImportError:
11+
import mock
12+
13+
from github_webhook.webhook import Webhook
14+
15+
16+
@pytest.fixture
17+
def mock_request():
18+
with mock.patch("github_webhook.webhook.request") as req:
19+
req.headers = {"X-Github-Delivery": ""}
20+
yield req
21+
22+
23+
@pytest.fixture
24+
def push_request(mock_request):
25+
mock_request.headers["X-Github-Event"] = "push"
26+
yield mock_request
27+
28+
29+
@pytest.fixture
30+
def app():
31+
yield mock.Mock()
32+
33+
34+
@pytest.fixture
35+
def webhook(app):
36+
yield Webhook(app)
37+
38+
39+
@pytest.fixture
40+
def handler(webhook):
41+
handler = mock.Mock()
42+
webhook.hook()(handler)
43+
yield handler
44+
45+
46+
def test_constructor():
47+
# GIVEN
48+
app = mock.Mock()
49+
50+
# WHEN
51+
webhook = Webhook(app)
52+
53+
# THEN
54+
app.add_url_rule.assert_called_once_with(
55+
endpoint="/postreceive", rule="/postreceive", view_func=webhook._postreceive, methods=["POST"]
56+
)
57+
58+
59+
def test_run_push_hook(webhook, handler, push_request):
60+
# WHEN
61+
webhook._postreceive()
62+
63+
# THEN
64+
handler.assert_called_once_with(push_request.get_json.return_value)
65+
66+
67+
def test_do_not_run_push_hook_on_ping(webhook, handler, mock_request):
68+
# GIVEN
69+
mock_request.headers["X-Github-Event"] = "ping"
70+
71+
# WHEN
72+
webhook._postreceive()
73+
74+
# THEN
75+
handler.assert_not_called()
76+
77+
78+
def test_can_handle_zero_events(webhook, push_request):
79+
# WHEN, THEN
80+
webhook._postreceive() # noop
81+
82+
83+
@pytest.mark.parametrize("secret", [u"secret", b"secret"])
84+
@mock.patch("github_webhook.webhook.hmac")
85+
def test_calls_if_signature_is_correct(mock_hmac, app, push_request, secret):
86+
# GIVEN
87+
webhook = Webhook(app, secret=secret)
88+
push_request.headers["X-Hub-Signature"] = "sha1=hash_of_something"
89+
push_request.data = b"something"
90+
handler = mock.Mock()
91+
mock_hmac.compare_digest.return_value = True
92+
93+
# WHEN
94+
webhook.hook()(handler)
95+
webhook._postreceive()
96+
97+
# THEN
98+
handler.assert_called_once_with(push_request.get_json.return_value)
99+
100+
101+
@mock.patch("github_webhook.webhook.hmac")
102+
def test_does_not_call_if_signature_is_incorrect(mock_hmac, app, push_request):
103+
# GIVEN
104+
webhook = Webhook(app, secret="super_secret")
105+
push_request.headers["X-Hub-Signature"] = "sha1=hash_of_something"
106+
push_request.data = b"something"
107+
handler = mock.Mock()
108+
mock_hmac.compare_digest.return_value = False
109+
110+
# WHEN, THEN
111+
webhook.hook()(handler)
112+
with pytest.raises(werkzeug.exceptions.BadRequest):
113+
webhook._postreceive()
114+
115+
116+
def test_request_has_no_data(webhook, handler, push_request):
117+
# GIVEN
118+
push_request.get_json.return_value = None
119+
120+
# WHEN, THEN
121+
with pytest.raises(werkzeug.exceptions.BadRequest):
122+
webhook._postreceive()
123+
124+
125+
def test_request_had_headers(webhook, handler, mock_request):
126+
# WHEN, THEN
127+
with pytest.raises(werkzeug.exceptions.BadRequest):
128+
webhook._postreceive()
129+
130+
131+
# -----------------------------------------------------------------------------
132+
# Copyright 2015 Bloomberg Finance L.P.
133+
#
134+
# Licensed under the Apache License, Version 2.0 (the "License");
135+
# you may not use this file except in compliance with the License.
136+
# You may obtain a copy of the License at
137+
#
138+
# http://www.apache.org/licenses/LICENSE-2.0
139+
#
140+
# Unless required by applicable law or agreed to in writing, software
141+
# distributed under the License is distributed on an "AS IS" BASIS,
142+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
143+
# See the License for the specific language governing permissions and
144+
# limitations under the License.
145+
# ----------------------------- END-OF-FILE -----------------------------------

tox.ini

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[tox]
2+
envlist = py27,py36,py37,pypy,pypy3,flake8
3+
4+
[testenv]
5+
deps =
6+
pytest
7+
pytest-cov
8+
flask
9+
six
10+
py{27,py}: mock
11+
commands = pytest -vl --cov=github_webhook --cov-report term-missing --cov-fail-under 100
12+
13+
[testenv:flake8]
14+
deps = flake8
15+
commands = flake8 github_webhook
16+
17+
[flake8]
18+
max-line-length = 100

0 commit comments

Comments
 (0)