From 650f09d21a0384df5ca1be750e0f7bd1831f5292 Mon Sep 17 00:00:00 2001 From: Fred Phillips Date: Sat, 28 Oct 2017 14:48:33 +0100 Subject: [PATCH 1/8] Set up continuous integration - add Travis CI - add tox - move tests to own directory - migrate to pytest Signed-off-by: Fred Phillips --- .travis.yml | 10 ++++++++++ setup.py | 2 +- tests/__init__.py | 0 {github_webhook => tests}/test_webhook.py | 22 +++++++++++----------- tox.ini | 11 +++++++++++ 5 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 .travis.yml create mode 100644 tests/__init__.py rename {github_webhook => tests}/test_webhook.py (75%) create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..def46e8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - "2.7" + - "3.6" + - "pypy" + - "pypy3" +install: + - pip install tox-travis +script: + - tox diff --git a/setup.py b/setup.py index ae11487..94014ff 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ license='Apache 2.0', packages=["github_webhook"], install_requires=['flask', 'six'], - tests_require=['mock', 'nose'], + tests_require=['mock', 'pytest'], classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/github_webhook/test_webhook.py b/tests/test_webhook.py similarity index 75% rename from github_webhook/test_webhook.py rename to tests/test_webhook.py index 4df373d..e22b710 100644 --- a/github_webhook/test_webhook.py +++ b/tests/test_webhook.py @@ -2,7 +2,6 @@ from __future__ import print_function -import unittest try: from unittest.mock import Mock except ImportError: @@ -11,18 +10,19 @@ from github_webhook.webhook import Webhook -class TestWebhook(unittest.TestCase): +def test_constructor(): + # GIVEN + app = Mock() - def test_constructor(self): - # GIVEN - app = Mock() + # WHEN + webhook = Webhook(app) - # WHEN - webhook = Webhook(app) - - # THEN - app.add_url_rule.assert_called_once_with( - '/postreceive', view_func=webhook._postreceive, methods=['POST']) + # THEN + app.add_url_rule.assert_called_once_with( + endpoint='/postreceive', + rule='/postreceive', + view_func=webhook._postreceive, + methods=['POST']) # ----------------------------------------------------------------------------- # Copyright 2015 Bloomberg Finance L.P. diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b17c77b --- /dev/null +++ b/tox.ini @@ -0,0 +1,11 @@ +[tox] +envlist = py27,py36,pypy,pypy3 + +[testenv] +deps = + pytest + pytest-cov + flask + six + py{27,py}: mock +commands = pytest -vl --cov=github_webhook --cov-report term-missing \ No newline at end of file From 425b35ee27304de6832782d9aea04c7c9d4ce45e Mon Sep 17 00:00:00 2001 From: Fred Phillips Date: Sat, 28 Oct 2017 18:28:49 +0100 Subject: [PATCH 2/8] Add more appropriate unit tests - 100% coverage - no lint errors from flake8 Signed-off-by: Fred Phillips --- github_webhook/__init__.py | 2 +- github_webhook/webhook.py | 6 +- tests/test_webhook.py | 116 ++++++++++++++++++++++++++++++++++++- tox.ini | 11 +++- 4 files changed, 128 insertions(+), 7 deletions(-) diff --git a/github_webhook/__init__.py b/github_webhook/__init__.py index 89ffe17..fee418e 100644 --- a/github_webhook/__init__.py +++ b/github_webhook/__init__.py @@ -8,7 +8,7 @@ :license: Apache License, Version 2.0 """ -from github_webhook.webhook import Webhook +from github_webhook.webhook import Webhook # noqa # ----------------------------------------------------------------------------- # Copyright 2015 Bloomberg Finance L.P. diff --git a/github_webhook/webhook.py b/github_webhook/webhook.py index 6486566..57616de 100644 --- a/github_webhook/webhook.py +++ b/github_webhook/webhook.py @@ -74,6 +74,7 @@ def _postreceive(self): return '', 204 + def _get_header(key): """Return message header""" @@ -82,6 +83,7 @@ def _get_header(key): except KeyError: abort(400, 'Missing header: ' + key) + EVENT_DESCRIPTIONS = { 'commit_comment': '{comment[user][login]} commented on ' '{comment[commit_id]} in {repository[full_name]}', @@ -110,7 +112,8 @@ def _get_header(key): 'public': '{sender[login]} publicized {repository[full_name]}', 'pull_request': '{sender[login]} {action} pull #{pull_request[number]} in ' '{repository[full_name]}', - 'pull_request_review': '{sender[login]} {action} {review[state]} review on pull #{pull_request[number]} in ' + 'pull_request_review': '{sender[login]} {action} {review[state]} ' + 'review on pull #{pull_request[number]} in ' '{repository[full_name]}', 'pull_request_review_comment': '{comment[user][login]} {action} comment ' 'on pull #{pull_request[number]} in ' @@ -128,6 +131,7 @@ def _get_header(key): '{repository[full_name]}' } + def _format_event(event_type, data): try: return EVENT_DESCRIPTIONS[event_type].format(**data) diff --git a/tests/test_webhook.py b/tests/test_webhook.py index e22b710..6167daf 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -2,17 +2,51 @@ from __future__ import print_function +import pytest +import werkzeug try: - from unittest.mock import Mock + from unittest import mock except ImportError: - from mock import Mock + import mock from github_webhook.webhook import Webhook +@pytest.fixture +def mock_request(): + with mock.patch("github_webhook.webhook.request") as req: + req.headers = { + "X-Github-Delivery": "" + } + yield req + + +@pytest.fixture +def push_request(mock_request): + mock_request.headers["X-Github-Event"] = "push" + yield mock_request + + +@pytest.fixture +def app(): + yield mock.Mock() + + +@pytest.fixture +def webhook(app): + yield Webhook(app) + + +@pytest.fixture +def handler(webhook): + handler = mock.Mock() + webhook.hook()(handler) + yield handler + + def test_constructor(): # GIVEN - app = Mock() + app = mock.Mock() # WHEN webhook = Webhook(app) @@ -24,6 +58,82 @@ def test_constructor(): view_func=webhook._postreceive, methods=['POST']) + +def test_run_push_hook(webhook, handler, push_request): + # WHEN + webhook._postreceive() + + # THEN + handler.assert_called_once_with(push_request.get_json.return_value) + + +def test_do_not_run_push_hook_on_ping(webhook, handler, mock_request): + # GIVEN + mock_request.headers["X-Github-Event"] = "ping" + + # WHEN + webhook._postreceive() + + # THEN + handler.assert_not_called() + + +def test_can_handle_zero_events(webhook, push_request): + # WHEN, THEN + webhook._postreceive() # noop + + +@pytest.mark.parametrize("secret", [ + u"secret", + b"secret" +]) +@mock.patch("github_webhook.webhook.hmac") +def test_calls_if_signature_is_correct(mock_hmac, app, push_request, secret): + # GIVEN + webhook = Webhook(app, secret=secret) + push_request.headers["X-Hub-Signature"] = "sha1=hash_of_something" + push_request.data = b"something" + handler = mock.Mock() + mock_hmac.compare_digest.return_value = True + + # WHEN + webhook.hook()(handler) + webhook._postreceive() + + # THEN + handler.assert_called_once_with(push_request.get_json.return_value) + + +@mock.patch("github_webhook.webhook.hmac") +def test_does_not_call_if_signature_is_incorrect(mock_hmac, app, push_request): + # GIVEN + webhook = Webhook(app, secret="super_secret") + push_request.headers["X-Hub-Signature"] = "sha1=hash_of_something" + push_request.data = b"something" + handler = mock.Mock() + mock_hmac.compare_digest.return_value = False + + # WHEN, THEN + webhook.hook()(handler) + with pytest.raises(werkzeug.exceptions.BadRequest): + webhook._postreceive() + + +def test_request_has_no_data(webhook, handler, push_request): + # GIVEN + push_request.get_json.return_value = None + + # WHEN, THEN + with pytest.raises(werkzeug.exceptions.BadRequest): + webhook._postreceive() + + +def test_request_had_headers(webhook, handler, mock_request): + # WHEN, THEN + with pytest.raises(werkzeug.exceptions.BadRequest): + webhook._postreceive() + + # ----------------------------------------------------------------------------- # Copyright 2015 Bloomberg Finance L.P. # diff --git a/tox.ini b/tox.ini index b17c77b..901d71d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py36,pypy,pypy3 +envlist = py27,py36,pypy,pypy3,flake8 [testenv] deps = @@ -8,4 +8,11 @@ deps = flask six py{27,py}: mock -commands = pytest -vl --cov=github_webhook --cov-report term-missing \ No newline at end of file +commands = pytest -vl --cov=github_webhook --cov-report term-missing --cov-fail-under 100 + +[testenv:flake8] +deps = flake8 +commands = flake8 github_webhook + +[flake8] +max-line-length = 100 \ No newline at end of file From 1a4590b42919caa4daa3f13f77af56d7c6fee5d7 Mon Sep 17 00:00:00 2001 From: Alex Chamberlain Date: Fri, 7 Jun 2019 17:13:38 +0100 Subject: [PATCH 3/8] Apply black formatting Signed-off-by: Alex Chamberlain --- .travis.yml | 7 +++ github_webhook/webhook.py | 108 ++++++++++++++++---------------------- setup.py | 53 ++++++++++--------- tests/test_webhook.py | 16 ++---- tox.ini | 2 +- 5 files changed, 86 insertions(+), 100 deletions(-) diff --git a/.travis.yml b/.travis.yml index def46e8..3493c21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,17 @@ +dist: xenial language: python python: - "2.7" - "3.6" + - "3.7" - "pypy" - "pypy3" install: - pip install tox-travis script: - tox +matrix: + include: + - python: "3.7" + script: black --line-length 120 --check github_webhook tests setup.py + install: pip install black diff --git a/github_webhook/webhook.py b/github_webhook/webhook.py index 57616de..d6addf9 100644 --- a/github_webhook/webhook.py +++ b/github_webhook/webhook.py @@ -16,17 +16,16 @@ class Webhook(object): :param secret: Optional secret, used to authenticate the hook comes from Github """ - def __init__(self, app, endpoint='/postreceive', secret=None): - app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, - methods=['POST']) + def __init__(self, app, endpoint="/postreceive", secret=None): + app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"]) self._hooks = collections.defaultdict(list) - self._logger = logging.getLogger('webhook') + self._logger = logging.getLogger("webhook") if secret is not None and not isinstance(secret, six.binary_type): - secret = secret.encode('utf-8') + secret = secret.encode("utf-8") self._secret = secret - def hook(self, event_type='push'): + def hook(self, event_type="push"): """ Registers a function as a hook. Multiple hooks can be registered for a given type, but the order in which they are invoke is unspecified. @@ -43,8 +42,7 @@ def decorator(func): def _get_digest(self): """Return message digest if a secret key was provided""" - return hmac.new( - self._secret, request.data, hashlib.sha1).hexdigest() if self._secret else None + return hmac.new(self._secret, request.data, hashlib.sha1).hexdigest() if self._secret else None def _postreceive(self): """Callback from Flask""" @@ -52,27 +50,25 @@ def _postreceive(self): digest = self._get_digest() if digest is not None: - sig_parts = _get_header('X-Hub-Signature').split('=', 1) + sig_parts = _get_header("X-Hub-Signature").split("=", 1) if not isinstance(digest, six.text_type): digest = six.text_type(digest) - if (len(sig_parts) < 2 or sig_parts[0] != 'sha1' - or not hmac.compare_digest(sig_parts[1], digest)): - abort(400, 'Invalid signature') + if len(sig_parts) < 2 or sig_parts[0] != "sha1" or not hmac.compare_digest(sig_parts[1], digest): + abort(400, "Invalid signature") - event_type = _get_header('X-Github-Event') + event_type = _get_header("X-Github-Event") data = request.get_json() if data is None: - abort(400, 'Request body must contain json') + abort(400, "Request body must contain json") - self._logger.info( - '%s (%s)', _format_event(event_type, data), _get_header('X-Github-Delivery')) + self._logger.info("%s (%s)", _format_event(event_type, data), _get_header("X-Github-Delivery")) for hook in self._hooks.get(event_type, []): hook(data) - return '', 204 + return "", 204 def _get_header(key): @@ -81,54 +77,41 @@ def _get_header(key): try: return request.headers[key] except KeyError: - abort(400, 'Missing header: ' + key) + abort(400, "Missing header: " + key) EVENT_DESCRIPTIONS = { - 'commit_comment': '{comment[user][login]} commented on ' - '{comment[commit_id]} in {repository[full_name]}', - 'create': '{sender[login]} created {ref_type} ({ref}) in ' - '{repository[full_name]}', - 'delete': '{sender[login]} deleted {ref_type} ({ref}) in ' - '{repository[full_name]}', - 'deployment': '{sender[login]} deployed {deployment[ref]} to ' - '{deployment[environment]} in {repository[full_name]}', - 'deployment_status': 'deployment of {deployement[ref]} to ' - '{deployment[environment]} ' - '{deployment_status[state]} in ' - '{repository[full_name]}', - 'fork': '{forkee[owner][login]} forked {forkee[name]}', - 'gollum': '{sender[login]} edited wiki pages in {repository[full_name]}', - 'issue_comment': '{sender[login]} commented on issue #{issue[number]} ' - 'in {repository[full_name]}', - 'issues': '{sender[login]} {action} issue #{issue[number]} in ' - '{repository[full_name]}', - 'member': '{sender[login]} {action} member {member[login]} in ' - '{repository[full_name]}', - 'membership': '{sender[login]} {action} member {member[login]} to team ' - '{team[name]} in {repository[full_name]}', - 'page_build': '{sender[login]} built pages in {repository[full_name]}', - 'ping': 'ping from {sender[login]}', - 'public': '{sender[login]} publicized {repository[full_name]}', - 'pull_request': '{sender[login]} {action} pull #{pull_request[number]} in ' - '{repository[full_name]}', - 'pull_request_review': '{sender[login]} {action} {review[state]} ' - 'review on pull #{pull_request[number]} in ' - '{repository[full_name]}', - 'pull_request_review_comment': '{comment[user][login]} {action} comment ' - 'on pull #{pull_request[number]} in ' - '{repository[full_name]}', - 'push': '{pusher[name]} pushed {ref} in {repository[full_name]}', - 'release': '{release[author][login]} {action} {release[tag_name]} in ' - '{repository[full_name]}', - 'repository': '{sender[login]} {action} repository ' - '{repository[full_name]}', - 'status': '{sender[login]} set {sha} status to {state} in ' - '{repository[full_name]}', - 'team_add': '{sender[login]} added repository {repository[full_name]} to ' - 'team {team[name]}', - 'watch': '{sender[login]} {action} watch in repository ' - '{repository[full_name]}' + "commit_comment": "{comment[user][login]} commented on " "{comment[commit_id]} in {repository[full_name]}", + "create": "{sender[login]} created {ref_type} ({ref}) in " "{repository[full_name]}", + "delete": "{sender[login]} deleted {ref_type} ({ref}) in " "{repository[full_name]}", + "deployment": "{sender[login]} deployed {deployment[ref]} to " + "{deployment[environment]} in {repository[full_name]}", + "deployment_status": "deployment of {deployement[ref]} to " + "{deployment[environment]} " + "{deployment_status[state]} in " + "{repository[full_name]}", + "fork": "{forkee[owner][login]} forked {forkee[name]}", + "gollum": "{sender[login]} edited wiki pages in {repository[full_name]}", + "issue_comment": "{sender[login]} commented on issue #{issue[number]} " "in {repository[full_name]}", + "issues": "{sender[login]} {action} issue #{issue[number]} in " "{repository[full_name]}", + "member": "{sender[login]} {action} member {member[login]} in " "{repository[full_name]}", + "membership": "{sender[login]} {action} member {member[login]} to team " "{team[name]} in {repository[full_name]}", + "page_build": "{sender[login]} built pages in {repository[full_name]}", + "ping": "ping from {sender[login]}", + "public": "{sender[login]} publicized {repository[full_name]}", + "pull_request": "{sender[login]} {action} pull #{pull_request[number]} in " "{repository[full_name]}", + "pull_request_review": "{sender[login]} {action} {review[state]} " + "review on pull #{pull_request[number]} in " + "{repository[full_name]}", + "pull_request_review_comment": "{comment[user][login]} {action} comment " + "on pull #{pull_request[number]} in " + "{repository[full_name]}", + "push": "{pusher[name]} pushed {ref} in {repository[full_name]}", + "release": "{release[author][login]} {action} {release[tag_name]} in " "{repository[full_name]}", + "repository": "{sender[login]} {action} repository " "{repository[full_name]}", + "status": "{sender[login]} set {sha} status to {state} in " "{repository[full_name]}", + "team_add": "{sender[login]} added repository {repository[full_name]} to " "team {team[name]}", + "watch": "{sender[login]} {action} watch in repository " "{repository[full_name]}", } @@ -138,6 +121,7 @@ def _format_event(event_type, data): except KeyError: return event_type + # ----------------------------------------------------------------------------- # Copyright 2015 Bloomberg Finance L.P. # diff --git a/setup.py b/setup.py index 94014ff..cea1aa4 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,29 @@ from setuptools import setup -setup(name="github-webhook", - version="1.0.2", - description="Very simple, but powerful, microframework for writing Github webhooks in Python", - url="https://github.com/bloomberg/python-github-webhook", - author="Alex Chamberlain, Fred Phillips, Daniel Kiss, Daniel Beer", - author_email="achamberlai9@bloomberg.net, fphillips7@bloomberg.net, dkiss1@bloomberg.net, dbeer1@bloomberg.net", - license='Apache 2.0', - packages=["github_webhook"], - install_requires=['flask', 'six'], - tests_require=['mock', 'pytest'], - - classifiers=[ - 'Development Status :: 4 - Beta', - 'Framework :: Flask', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development :: Version Control' - ], - test_suite='nose.collector') +setup( + name="github-webhook", + version="1.0.2", + description="Very simple, but powerful, microframework for writing Github webhooks in Python", + url="https://github.com/bloomberg/python-github-webhook", + author="Alex Chamberlain, Fred Phillips, Daniel Kiss, Daniel Beer", + author_email="achamberlai9@bloomberg.net, fphillips7@bloomberg.net, dkiss1@bloomberg.net, dbeer1@bloomberg.net", + license="Apache 2.0", + packages=["github_webhook"], + install_requires=["flask", "six"], + tests_require=["mock", "pytest"], + classifiers=[ + "Development Status :: 4 - Beta", + "Framework :: Flask", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Version Control", + ], + test_suite="nose.collector", +) diff --git a/tests/test_webhook.py b/tests/test_webhook.py index 6167daf..6575632 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -4,6 +4,7 @@ import pytest import werkzeug + try: from unittest import mock except ImportError: @@ -15,9 +16,7 @@ @pytest.fixture def mock_request(): with mock.patch("github_webhook.webhook.request") as req: - req.headers = { - "X-Github-Delivery": "" - } + req.headers = {"X-Github-Delivery": ""} yield req @@ -53,10 +52,8 @@ def test_constructor(): # THEN app.add_url_rule.assert_called_once_with( - endpoint='/postreceive', - rule='/postreceive', - view_func=webhook._postreceive, - methods=['POST']) + endpoint="/postreceive", rule="/postreceive", view_func=webhook._postreceive, methods=["POST"] + ) def test_run_push_hook(webhook, handler, push_request): @@ -83,10 +80,7 @@ def test_can_handle_zero_events(webhook, push_request): webhook._postreceive() # noop -@pytest.mark.parametrize("secret", [ - u"secret", - b"secret" -]) +@pytest.mark.parametrize("secret", [u"secret", b"secret"]) @mock.patch("github_webhook.webhook.hmac") def test_calls_if_signature_is_correct(mock_hmac, app, push_request, secret): # GIVEN diff --git a/tox.ini b/tox.ini index 901d71d..ed2ef8b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py36,pypy,pypy3,flake8 +envlist = py27,py36,py37,pypy,pypy3,flake8 [testenv] deps = From 8d3ad248126c15195d0f21b436ca05b3718bc255 Mon Sep 17 00:00:00 2001 From: Shibani Mahapatra Date: Tue, 31 Dec 2019 09:00:59 +0000 Subject: [PATCH 4/8] Add support for 'application/x-www-form-urlencoded' Add support for Content-Type: application/x-www-form-urlencoded, so that `python-github-webhook` supports both Content-Types supported by GitHub webhooks. Signed-Off-By: Shibani Mahapatra --- github_webhook/webhook.py | 9 +++++++-- setup.py | 2 +- tests/test_webhook.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/github_webhook/webhook.py b/github_webhook/webhook.py index d6addf9..85787d3 100644 --- a/github_webhook/webhook.py +++ b/github_webhook/webhook.py @@ -2,7 +2,7 @@ import hashlib import hmac import logging - +import json import six from flask import abort, request @@ -58,7 +58,12 @@ def _postreceive(self): abort(400, "Invalid signature") event_type = _get_header("X-Github-Event") - data = request.get_json() + content_type = _get_header("content-type") + data = ( + json.loads(request.form.to_dict(flat=True)["payload"]) + if content_type == "application/x-www-form-urlencoded" + else request.get_json() + ) if data is None: abort(400, "Request body must contain json") diff --git a/setup.py b/setup.py index cea1aa4..ebd03dc 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="github-webhook", - version="1.0.2", + version="1.0.3", description="Very simple, but powerful, microframework for writing Github webhooks in Python", url="https://github.com/bloomberg/python-github-webhook", author="Alex Chamberlain, Fred Phillips, Daniel Kiss, Daniel Beer", diff --git a/tests/test_webhook.py b/tests/test_webhook.py index 6575632..b6e64a2 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -4,6 +4,7 @@ import pytest import werkzeug +import json try: from unittest import mock @@ -23,6 +24,14 @@ def mock_request(): @pytest.fixture def push_request(mock_request): mock_request.headers["X-Github-Event"] = "push" + mock_request.headers["content-type"] = "application/json" + yield mock_request + + +@pytest.fixture +def push_request_encoded(mock_request): + mock_request.headers["X-Github-Event"] = "push" + mock_request.headers["content-type"] = "application/x-www-form-urlencoded" yield mock_request @@ -64,9 +73,35 @@ def test_run_push_hook(webhook, handler, push_request): handler.assert_called_once_with(push_request.get_json.return_value) +def test_run_push_hook_urlencoded(webhook, handler, push_request_encoded): + github_mock_payload = {"payload": '{"key": "value"}'} + push_request_encoded.form.to_dict.return_value = github_mock_payload + payload = json.loads(github_mock_payload["payload"]) + + # WHEN + webhook._postreceive() + + # THEN + handler.assert_called_once_with(payload) + + def test_do_not_run_push_hook_on_ping(webhook, handler, mock_request): # GIVEN mock_request.headers["X-Github-Event"] = "ping" + mock_request.headers["content-type"] = "application/json" + + # WHEN + webhook._postreceive() + + # THEN + handler.assert_not_called() + + +def test_do_not_run_push_hook_on_ping_urlencoded(webhook, handler, mock_request): + # GIVEN + mock_request.headers["X-Github-Event"] = "ping" + mock_request.headers["content-type"] = "application/x-www-form-urlencoded" + mock_request.form.to_dict.return_value = {"payload": '{"key": "value"}'} # WHEN webhook._postreceive() From 2a1e1fbbb74c66761079778ddf48e5c66ee7be50 Mon Sep 17 00:00:00 2001 From: Sergio Isidoro Date: Sun, 26 Jan 2020 11:19:33 +0200 Subject: [PATCH 5/8] Support for init_app Fixes: #24 --- github_webhook/webhook.py | 13 +++++++++++-- tests/test_webhook.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/github_webhook/webhook.py b/github_webhook/webhook.py index 85787d3..0a8b6c2 100644 --- a/github_webhook/webhook.py +++ b/github_webhook/webhook.py @@ -16,11 +16,20 @@ class Webhook(object): :param secret: Optional secret, used to authenticate the hook comes from Github """ - def __init__(self, app, endpoint="/postreceive", secret=None): - app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"]) + def __init__(self, app=None, endpoint="/postreceive", secret=None): + self.app = app + self.set_secret(secret) + if app is not None: + self.init_app(app, endpoint, secret) + def init_app(self, app, endpoint="/postreceive", secret=None): self._hooks = collections.defaultdict(list) self._logger = logging.getLogger("webhook") + if secret is not None: + self.set_secret(secret) + app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"]) + + def set_secret(self, secret=None): if secret is not None and not isinstance(secret, six.binary_type): secret = secret.encode("utf-8") self._secret = secret diff --git a/tests/test_webhook.py b/tests/test_webhook.py index b6e64a2..b6527b5 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -65,6 +65,44 @@ def test_constructor(): ) +def test_init_app_flow(): + # GIVEN + app = mock.Mock() + + # WHEN + webhook = Webhook() + webhook.init_app(app) + + # THEN + app.add_url_rule.assert_called_once_with( + endpoint="/postreceive", rule="/postreceive", view_func=webhook._postreceive, methods=["POST"] + ) + + +def test_init_app_flow_should_not_accidentally_override_secrets(): + # GIVEN + app = mock.Mock() + + # WHEN + webhook = Webhook(secret="hello-world-of-secrecy") + webhook.init_app(app) + + # THEN + assert webhook._secret is not None + + +def test_init_app_flow_should_override_secrets(): + # GIVEN + app = mock.Mock() + + # WHEN + webhook = Webhook(secret="hello-world-of-secrecy") + webhook.init_app(app, secret="a-new-world-of-secrecy") + + # THEN + assert webhook._secret == "a-new-world-of-secrecy".encode("utf-8") + + def test_run_push_hook(webhook, handler, push_request): # WHEN webhook._postreceive() From ddd33ac743dbac35222057f95cafa91bff78652a Mon Sep 17 00:00:00 2001 From: Alex Chamberlain Date: Fri, 28 Feb 2020 09:18:40 +0000 Subject: [PATCH 6/8] Use property instead of setter Signed-off-by: Alex Chamberlain --- github_webhook/webhook.py | 11 ++++++++--- tests/test_webhook.py | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/github_webhook/webhook.py b/github_webhook/webhook.py index 0a8b6c2..f4ca352 100644 --- a/github_webhook/webhook.py +++ b/github_webhook/webhook.py @@ -18,7 +18,7 @@ class Webhook(object): def __init__(self, app=None, endpoint="/postreceive", secret=None): self.app = app - self.set_secret(secret) + self.secret = secret if app is not None: self.init_app(app, endpoint, secret) @@ -26,10 +26,15 @@ def init_app(self, app, endpoint="/postreceive", secret=None): self._hooks = collections.defaultdict(list) self._logger = logging.getLogger("webhook") if secret is not None: - self.set_secret(secret) + self.secret = secret app.add_url_rule(rule=endpoint, endpoint=endpoint, view_func=self._postreceive, methods=["POST"]) - def set_secret(self, secret=None): + @property + def secret(self): + return self._secret + + @secret.setter + def secret(self, secret): if secret is not None and not isinstance(secret, six.binary_type): secret = secret.encode("utf-8") self._secret = secret diff --git a/tests/test_webhook.py b/tests/test_webhook.py index b6527b5..29eba7d 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -88,7 +88,7 @@ def test_init_app_flow_should_not_accidentally_override_secrets(): webhook.init_app(app) # THEN - assert webhook._secret is not None + assert webhook.secret is not None def test_init_app_flow_should_override_secrets(): @@ -100,7 +100,7 @@ def test_init_app_flow_should_override_secrets(): webhook.init_app(app, secret="a-new-world-of-secrecy") # THEN - assert webhook._secret == "a-new-world-of-secrecy".encode("utf-8") + assert webhook.secret == "a-new-world-of-secrecy".encode("utf-8") def test_run_push_hook(webhook, handler, push_request): From 6a24c6f1589a14a780378937f126282d32b32a35 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 3 Sep 2019 20:29:08 -0400 Subject: [PATCH 7/8] Add .gitignore file As part of the normal process of developing python-github-webhook local artifacts are generated from running python code, running tox, and running tests. These are not things that should be committed to the repo. To avoid accidents or mistakes when a file that shouldn't ever be added to the repo is included in a commit this commit adds a .gitignore file and configures it to not include any of these artifacts of normal development. This way including a file like that by mistake is not likely to ever happen. Signed-off-by: Matthew Treinish --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ee8c50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +.tox/* +*.pyc +.coverage* +!.coveragerc +*egg* From 61e713c3781e2de6e327554be54095df2d666604 Mon Sep 17 00:00:00 2001 From: Alex Chamberlain Date: Fri, 6 Mar 2020 11:18:46 +0000 Subject: [PATCH 8/8] Release version 1.0.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ebd03dc..b1ebbae 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="github-webhook", - version="1.0.3", + version="1.0.4", description="Very simple, but powerful, microframework for writing Github webhooks in Python", url="https://github.com/bloomberg/python-github-webhook", author="Alex Chamberlain, Fred Phillips, Daniel Kiss, Daniel Beer",