From bae7a0e05097b75205b9e700966712bfb2ce6b9a Mon Sep 17 00:00:00 2001 From: Michal Konečný Date: Oct 05 2021 14:25:19 +0000 Subject: [PATCH 1/2] Add tox Signed-off-by: Michal Konečný --- diff --git a/.gitignore b/.gitignore index 8f6c324..10a375d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ dist discourse2fedmsg.egg-info *.pyc +.venv + +# Tests +.coverage +.tox +coverage.xml +htmlcov/ diff --git a/README.rst b/README.rst index 0669b28..faba708 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,25 @@ Try it out ---------- +Prepare virtual env:: + python -m venv .venv + source .venv/bin/activate + +Install app with dependencies:: + pip install . + Run the server:: $ DISCOURSE2FEDMSG_SECRET=CHANGEME python discourse2fedmsg.py + + +Testing +------- + +This app is using `tox` for testing. + +Install tox (Fedora):: + dnf install tox + +Run the tests:: + tox diff --git a/discourse2fedmsg.py b/discourse2fedmsg.py index 4fe44a6..ca9e1ba 100644 --- a/discourse2fedmsg.py +++ b/discourse2fedmsg.py @@ -22,59 +22,55 @@ import flask app = flask.Flask(__name__) -secret = os.environ.get('DISCOURSE2FEDMSG_SECRET', 'CHANGEME') +secret = os.environ.get("DISCOURSE2FEDMSG_SECRET", "CHANGEME") -if secret == 'CHANGEME': - raise Exception('Please provide a secret via DISCOURSE2FEDMSG_SECRET env') +if secret == "CHANGEME": # pragma: no cover + raise Exception("Please provide a secret via DISCOURSE2FEDMSG_SECRET env") -@app.route('/') +@app.route("/") def index(): return "Source: https://pagure.io/discourse2fedmsg" -@app.route('/webhook', methods=['POST']) +@app.route("/webhook", methods=["POST"]) def webhook(): - header_sig = flask.request.headers.get('X-Discourse-Event-Signature', None) + header_sig = flask.request.headers.get("X-Discourse-Event-Signature", None) if not header_sig: - error = 'No X-Discourse-Event-Signature found on request.' + error = "No X-Discourse-Event-Signature found on request." return error, 403 - if not header_sig.startswith('sha256='): - return 'No sha256 prefix found.', 400 - header_sig = header_sig[len('sha256='):] + if not header_sig.startswith("sha256="): + return "No sha256 prefix found.", 400 + header_sig = header_sig[len("sha256=") :] payload = flask.request.data - calced_sig = hmac.new( - secret, - payload, - hashlib.sha256 - ).hexdigest() - app.logger.info('Comparing %r with %r' % (header_sig, calced_sig)) + calced_sig = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() + app.logger.info("Comparing %r with %r" % (header_sig, calced_sig)) if header_sig != calced_sig: - return 'Signature not valid.', 403 + return "Signature not valid.", 403 payload = json.loads(payload) - app.logger.info('Payload: %r' % payload) + app.logger.info("Payload: %r" % payload) # Crazy enough..... they don't seem to have this in the signed portion of # the payload.... At least not per the docs... - topic = flask.request.headers.get('X-Discourse-Event-Type', None) - app.logger.info('Topic: %s' % topic) + topic = flask.request.headers.get("X-Discourse-Event-Type", None) + app.logger.info("Topic: %s" % topic) - return "Testing before sending" + # return "Testing before sending" # Having verified the message, we're all set. Republish it on our bus. fedmsg.publish( - modname='discourse', + modname="discourse", topic=topic, msg=payload, ) return "Everything is 200 OK" -if __name__ == '__main__': - app.run(host='0.0.0.0', port=8081, debug=True) +if __name__ == "__main__": # pragma: no cover + app.run(host="0.0.0.0", port=8081, debug=True) # nosec diff --git a/setup.py b/setup.py index fcdd5d4..55e44f6 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,14 @@ from setuptools import setup setup( - name='discourse2fedmsg', - description='discourse2fedmsg bridges discourse to fedmsg', - version='0.1', - author='Ralph Bean, Patrick Uiterwijk', - author_email='rbean@redhat.com, puiterwijk@redhat.com', - license='GPLv2+', - url='https://pagure.io/discourse2fedmsg', - py_modules=['discourse2fedmsg'], + name="discourse2fedmsg", + description="discourse2fedmsg bridges discourse to fedmsg", + version="0.1", + author="Ralph Bean, Patrick Uiterwijk", + author_email="rbean@redhat.com, puiterwijk@redhat.com", + license="GPLv2+", + url="https://pagure.io/discourse2fedmsg", + py_modules=["discourse2fedmsg"], packages=[], - install_requires=['fedmsg', 'flask', 'gunicorn'], + install_requires=["fedmsg", "flask", "gunicorn"], ) diff --git a/test_discourse2fedmsg.py b/test_discourse2fedmsg.py new file mode 100644 index 0000000..8e3aa61 --- /dev/null +++ b/test_discourse2fedmsg.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the Anitya project. +# Copyright (C) 2017-2020 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +""" +Tests for discourse2fedmsg. +""" +import hashlib +import hmac +import json +import unittest.mock as mock + +import pytest +import os + +import discourse2fedmsg as d2f + + +@pytest.fixture +def app(): + """ + Return the flask client. + """ + return d2f.app.test_client() + + +def test_index(app): + """ + Test for index function. + """ + rv = app.get("/") + + assert rv.status_code == 200 + assert b"Source: https://pagure.io/discourse2fedmsg" in rv.data + + +@mock.patch("fedmsg.publish") +def test_webhook(mock_publish, app): + data = {"test_data": "data"} + calced_sig = hmac.new( + d2f.secret.encode(), json.dumps(data).encode(), hashlib.sha256 + ).hexdigest() + rv = app.post( + "/webhook", + data=json.dumps(data), + headers={ + "X-Discourse-Event-Signature": "sha256=" + calced_sig, + "X-Discourse-Event-Type": "test_event", + }, + ) + + assert rv.status_code == 200 + assert b"Everything is 200 OK" in rv.data + + mock_publish.assert_called_with(modname="discourse", topic="test_event", msg=data) + + +def test_webhook_missing_header(app): + data = {"test_data": "data"} + rv = app.post( + "/webhook", + data=json.dumps(data), + ) + + assert rv.status_code == 403 + assert b"No X-Discourse-Event-Signature found on request." in rv.data + + +def test_webhook_wrong_hash(app): + data = {"test_data": "data"} + calced_sig = hmac.new( + d2f.secret.encode(), json.dumps(data).encode(), hashlib.sha256 + ).hexdigest() + rv = app.post( + "/webhook", + data=json.dumps(data), + headers={ + "X-Discourse-Event-Signature": calced_sig, + "X-Discourse-Event-Type": "test_event", + }, + ) + + assert rv.status_code == 400 + assert b"No sha256 prefix found." in rv.data + + +def test_webhook_not_valid_sig(app): + data = {"test_data": "data"} + rv = app.post( + "/webhook", + data=json.dumps(data), + headers={ + "X-Discourse-Event-Signature": "sha256=abcde", + "X-Discourse-Event-Type": "test_event", + }, + ) + + assert rv.status_code == 403 + assert b"Signature not valid." in rv.data diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..5b0a76b --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,7 @@ +bandit +black +coverage +diff-cover +flake8 +pytest +pytest-cov diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..06bdc1f --- /dev/null +++ b/tox.ini @@ -0,0 +1,45 @@ +[tox] +envlist = py38,py39,diff-cover,lint,format,bandit + +[testenv] +setenv = + DISCOURSE2FEDMSG_SECRET="secret" +deps = + -rtest_requirements.txt +whitelist_externals = + rm +commands = + rm -rf htmlcov coverage.xml + py.test -vv --cov-config .coveragerc --cov=discourse2fedmsg \ + --cov-report term --cov-report xml --cov-report html {posargs} + +[testenv:diff-cover] +deps = + -rtest_requirements.txt +commands = + diff-cover coverage.xml --compare-branch=origin/master --fail-under=100 + +[testenv:lint] +deps = + -rtest_requirements.txt +commands = + python -m flake8 discourse2fedmsg.py {posargs} + +[testenv:format] +install_command = pip install --pre {opts} {packages} +deps = + -rtest_requirements.txt +commands = + python -m black --check --diff {posargs:.} + +[testenv:bandit] +deps = + -rtest_requirements.txt +commands = + bandit -r discourse2fedmsg.py -ll + +[flake8] +show-source = True +max-line-length = 100 +ignore = E203,W503 +exclude = .git,.tox,dist,*egg,build,files \ No newline at end of file From c6734ad20dfa6b97031351b8c8116d6b71c0ec78 Mon Sep 17 00:00:00 2001 From: Michal Konečný Date: Oct 06 2021 12:03:52 +0000 Subject: [PATCH 2/2] Remove copy/paste error Signed-off-by: Michal Konečný --- diff --git a/test_discourse2fedmsg.py b/test_discourse2fedmsg.py index 8e3aa61..ccf28a7 100644 --- a/test_discourse2fedmsg.py +++ b/test_discourse2fedmsg.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # -# This file is part of the Anitya project. -# Copyright (C) 2017-2020 Red Hat, Inc. +# Copyright (C) 2021 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by