├── .coveragerc
├── .gitignore
├── .travis.yml
├── AUTHORS
├── LICENSE
├── MANIFEST.in
├── README.rst
├── VERSION
├── docker
├── docker-compose.yml
├── pywebhooks-server.Dockerfile
└── pywebhooks-worker.Dockerfile
├── pylintrc
├── pywebhooks
├── __init__.py
├── api
│ ├── __init__.py
│ ├── decorators
│ │ ├── __init__.py
│ │ ├── authorization.py
│ │ └── validation.py
│ ├── handlers
│ │ ├── __init__.py
│ │ ├── pagination_handler.py
│ │ └── resources_handler.py
│ └── resources
│ │ ├── __init__.py
│ │ └── v1
│ │ ├── __init__.py
│ │ ├── account
│ │ ├── __init__.py
│ │ ├── account_api.py
│ │ ├── accounts_api.py
│ │ └── reset
│ │ │ ├── __init__.py
│ │ │ ├── api_key_api.py
│ │ │ └── secret_key_api.py
│ │ └── webhook
│ │ ├── __init__.py
│ │ ├── registration_api.py
│ │ ├── registrations_api.py
│ │ ├── subscription.py
│ │ ├── subscriptions.py
│ │ └── triggered_api.py
├── app.py
├── database
│ ├── __init__.py
│ └── rethinkdb
│ │ ├── __init__.py
│ │ ├── bootstrap_admin.py
│ │ ├── drop.py
│ │ ├── initialize.py
│ │ └── interactions.py
├── examples
│ ├── __init__.py
│ ├── endpoint_development_server.py
│ └── ruby_endpoint_developement_server.rb
├── tasks
│ ├── __init__.py
│ └── webhook_notification.py
└── utils
│ ├── __init__.py
│ ├── common.py
│ ├── request_handler.py
│ └── rethinkdb_helper.py
├── requirements.txt
├── setup.cfg
├── setup.py
├── test-requirements.txt
├── tests
├── __init__.py
├── functional
│ ├── __init__.py
│ └── http_interactions.py
└── unit
│ ├── __init__.py
│ ├── api
│ ├── __init__.py
│ ├── decorators
│ │ ├── __init__.py
│ │ ├── test_authorization.py
│ │ └── test_validation.py
│ ├── handlers
│ │ ├── __init__.py
│ │ ├── test_pagination_handler.py
│ │ └── test_resources_handler.py
│ └── resources
│ │ ├── __init__.py
│ │ └── v1
│ │ ├── __init__.py
│ │ └── test_account_api.py
│ ├── database
│ ├── __init__.py
│ └── rethinkdb
│ │ ├── __init__.py
│ │ ├── test_bootstrap_admin.py
│ │ ├── test_drop.py
│ │ └── test_initialize.py
│ ├── tasks
│ ├── __init__.py
│ └── test_webhook_notification.py
│ ├── test_app.py
│ └── utils
│ ├── __init__.py
│ ├── test_common.py
│ ├── test_request_handler.py
│ └── test_rethinkdb_helper.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source = pywebhooks
4 |
5 | [report]
6 | omit = *tests/*,*examples/*,*__init__*,*/database/rethinkdb/interactions.py
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | bin/
12 | build/
13 | develop-eggs/
14 | dist/
15 | eggs/
16 | lib/
17 | lib64/
18 | parts/
19 | sdist/
20 | var/
21 | *.egg-info/
22 | .installed.cfg
23 | *.egg
24 |
25 | # Installer logs
26 | pip-log.txt
27 | pip-delete-this-directory.txt
28 |
29 | # Unit test / coverage reports
30 | htmlcov/
31 | .tox/
32 | .coverage
33 | .cache
34 | nosetests.xml
35 | coverage.xml
36 |
37 | # Translations
38 | *.mo
39 |
40 | # Mr Developer
41 | .mr.developer.cfg
42 | .project
43 | .pydevproject
44 |
45 | # Rope
46 | .ropeproject
47 |
48 | # Django stuff:
49 | *.log
50 | *.pot
51 |
52 | # Sphinx documentation
53 | docs/_build/
54 |
55 | .DS_Store
56 |
57 | # virtualenv venv
58 | .venv/
59 | venv/
60 | ENV/
61 |
62 | # VSCode
63 | .vscode/
64 | settings.json
65 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.6"
4 | # command to install dependencies
5 | install:
6 | - pip install -r requirements.txt
7 | - pip install -r test-requirements.txt
8 | - pip install coveralls
9 | # command to run tests
10 | script: nosetests
11 | # run on container-based infrastructure
12 | sudo: false
13 |
14 | script:
15 | - python setup.py install
16 | - "coverage run --source=pywebhooks setup.py test"
17 | after_success:
18 | - coveralls
19 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | * Chad Lung (chadlung)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Licensed under the Apache License, Version 2.0 (the "License");
2 | you may not use this file except in compliance with the License.
3 | You may obtain a copy of the License at
4 |
5 | http://www.apache.org/licenses/LICENSE-2.0
6 |
7 | Unless required by applicable law or agreed to in writing, software
8 | distributed under the License is distributed on an "AS IS" BASIS,
9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 | See the License for the specific language governing permissions and
11 | limitations under the License.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include VERSION
2 | include LICENSE
3 | include README.md
4 | include setup.cfg
5 | include requirements.txt
6 | include test-requirements.txt
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | PyWebhooks
2 | ==========
3 |
4 | *A simple webhooks service*
5 |
6 | .. image:: https://travis-ci.org/chadlung/pywebhooks.svg?branch=master
7 | :target: https://travis-ci.org/chadlung/pywebhooks
8 | .. image:: https://coveralls.io/repos/chadlung/pywebhooks/badge.svg?branch=master&service=github
9 | :target: https://coveralls.io/github/chadlung/pywebhooks?branch=master
10 | .. image:: https://badge.fury.io/py/pywebhooks.svg
11 | :target: https://badge.fury.io/py/pywebhooks
12 |
13 | **Note:** PyWebhooks is ideally deployed on an internal private cloud/network where you
14 | know and trust the end users and services using it. It should not be considered
15 | secure enough (currently) to be a publicly deployed service.
16 |
17 | Don't like something? Need a feature? Please submit a pull request complete with
18 | tests and an update to the readme if required.
19 |
20 | In order to run PyWebhooks you'll need to have `RethinkDB `__
21 | and `Redis `__ installed on a server or server(s). RethinkDB is
22 | used to store the account, webooks, etc. data. Redis is used by
23 | `Celery `__ to handle the calls to the
24 | webhook endpoints.
25 |
26 | **Note:** PyWebhooks has been tested on Ubuntu 16.04 and OS X.
27 | PyWebhooks has been tested with Python 3.5.x and 3.6.x. Prior Python 3.x versions have not
28 | been tested and Python 2.x support is not planned.
29 |
30 | Why PyWebhooks?
31 | ^^^^^^^^^^^^^^^
32 |
33 | I looked all over for a project that did something similar to this. You can find
34 | plenty of code to listen for incoming webhooks as well as some code for sending webhooks.
35 | However, I couldn't find anything that wrapped it into a complete service where you could
36 | run a server to allow for adding new accounts, letting those users create their
37 | own webhooks and then allow others to listen (subscribe) to those webhooks.
38 |
39 | Update - Feb. 9, 2019
40 | ^^^^^^^^^^^^^^^^^^^^^
41 |
42 | - Vagrant support is dropped. The feedback I've received is only based on Docker support.
43 | - I'm planning to swap out the RethinkDB backend with Postgres/MySQL.
44 | - Also planned is no more static webhook messages - you could have messages sent with custom values.
45 | - Potentially removing Flask and replacing with Falcon.
46 | - More features and updates planned but too early to post them here. Its possible these new features
47 | and changes will just end up in an entirely new repository.
48 |
49 | Quickstart - Docker-Compose
50 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
51 |
52 | Make sure you are running Docker version ``1.10+`` and Docker Compose ``1.6+`` or newer. From a command line run the following from the project's ``docker`` folder:
53 |
54 | ::
55 |
56 | $ cd docker
57 | $ docker-compose up
58 |
59 | If you can don't run that in daemon mode as you can more easily capture the admin ``secret_key`` and ``api_key`` from the console output.
60 | It will look similar to this:
61 |
62 | ::
63 |
64 | pywebhooks-server | Adding admin account
65 | pywebhooks-server | {'secret_key': 'd620fb92a70b7e5c127de74fcd717aa803f7e300', 'api_key': '7e8d21dda1c5738a30882e4520fbbfac55eebe3f'}
66 |
67 | Make sure to record those keys.
68 |
69 | Non-Quickstart
70 | ^^^^^^^^^^^^^^
71 |
72 | If you did't use the quick start mentioned above:
73 |
74 | Once you have Redis and RethinkDB setup and running you can initialize the database and
75 | admin accounts by running the following:
76 |
77 | ::
78 |
79 | $ python app.py --initdb
80 |
81 | **Response:**
82 |
83 | ::
84 |
85 | Dropping database...
86 | Creating database...
87 | Adding admin account
88 | {'secret_key': 'a6d8ff11a7cdb51130ea184b7228e179f3fd3a4c', 'api_key': 'ba86c64c24f361ddbcfe27be187d8d3002c9f43c'}
89 | Complete
90 |
91 | Make note of the admin ``api_key`` as it will be stored as a hash.
92 |
93 | When you create a new user account there are a few things to consider. First,
94 | you need to have an endpoint setup where the account creation process can verify
95 | against. The endpoint can be whatever you want, a simple example would be a
96 | service listening on: ``http://127.0.0.1:9090/account/endpoint``
97 |
98 | When you send the command to create the account if all goes well the PyWebhooks
99 | server will hit the endpoint you specified with a challenge you need to echo back.
100 | This helps ensure that you are actually setting up an endpoint that you control.
101 |
102 | The PyWebhooks server will hit the endpoint you specified like this:
103 | ``/account/endpoint?echo=2cac9beaa2f3b3aa72cc86faefb7575ba9c3c4b8``
104 |
105 | It is your server's job to take that echo value and return it. In Python (using Flask)
106 | this would look like:
107 |
108 | ::
109 |
110 | @app.route('/account/endpoint', methods=['GET'])
111 | def echo():
112 | return make_response(jsonify({'echo': request.args.get('echo')}), client.OK)
113 |
114 | **Note:** PyWebhooks doesn't require your service be written in Python, any
115 | language will work as long as it returns what is expected (in this case the echo value).
116 |
117 | In Ruby 2.2.x using Sinatra a minimal endpoint server (handles Webhook POST traffic
118 | and GET echo requests) might look like this:
119 |
120 | ::
121 |
122 | require 'rubygems'
123 | require 'openssl'
124 | require 'sinatra'
125 | require 'json'
126 |
127 |
128 | SHARED_SECRET = 'c27e823b0a500a537990dcccfc50334fe814fbd2'
129 |
130 | # Handle echo requests
131 | get '/account/endpoint' do
132 | content_type :json
133 | echo_value = params['echo']
134 | puts 'echo value:'
135 | puts(echo_value)
136 |
137 | status 200
138 | { :echo => echo_value }.to_json
139 | end
140 |
141 | # Handle the incoming webhook events
142 | post '/account/endpoint' do
143 | request.body.rewind
144 | data = request.body.read
145 | HMAC_DIGEST = OpenSSL::Digest.new('sha1')
146 | signature = OpenSSL::HMAC.hexdigest(HMAC_DIGEST, SHARED_SECRET, data)
147 | incoming_signature = env['HTTP_PYWEBHOOKS_SIGNATURE']
148 |
149 | puts 'hmac verification results:'
150 | puts Rack::Utils.secure_compare(signature, incoming_signature)
151 |
152 | incoming_event = env['HTTP_EVENT']
153 | puts 'incoming event is:'
154 | puts incoming_event
155 | puts 'incoming json is:'
156 | puts data
157 |
158 | status 200
159 | '{}'
160 | end
161 |
162 |
163 | **Note:** Pardon my Ruby, I'm rusty with it.
164 |
165 | A full Python endpoint example server code (for testing) can be as simple as:
166 |
167 | ::
168 |
169 | import hashlib
170 | import hmac
171 | from http import client
172 | import json
173 |
174 | from flask import Flask
175 | from flask import request, make_response, jsonify
176 |
177 |
178 | app = Flask(__name__)
179 |
180 | # Adjust this as needed
181 | SECRET_KEY = 'c27e823b0a500a537990dcccfc50334fe814fbd2'
182 |
183 |
184 | def verify_hmac_hash(incoming_json, secret_key, incoming_signature):
185 | signature = hmac.new(
186 | str(secret_key).encode('utf-8'),
187 | str(incoming_json).encode('utf-8'),
188 | digestmod=hashlib.sha1
189 | ).hexdigest()
190 |
191 | return hmac.compare_digest(signature, incoming_signature)
192 |
193 |
194 | def create_response(req):
195 | if request.args.get('echo'):
196 | return make_response(jsonify({'echo': req.args.get('echo')}), client.OK)
197 | if request.args.get('api_key'):
198 | print('New api_key: {0}'.format(req.args.get('api_key')))
199 | return make_response(jsonify({}), client.OK)
200 | if request.args.get('secret_key'):
201 | print('New secret_key: {0}'.format(req.args.get('secret_key')))
202 | return make_response(jsonify({}), client.OK)
203 |
204 |
205 | def webhook_listener(request):
206 | print(request.headers)
207 | print(request.data)
208 | print(json.dumps(request.json))
209 |
210 | is_signature_valid = verify_hmac_hash(
211 | json.dumps(request.json),
212 | SECRET_KEY,
213 | request.headers['pywebhooks-signature']
214 | )
215 |
216 | print('Is Signature Valid?: {0}'.format(is_signature_valid))
217 |
218 | return make_response(jsonify({}), client.OK)
219 |
220 |
221 | @app.route('/account/endpoint', methods=['GET'])
222 | def echo():
223 | return create_response(request)
224 |
225 |
226 | @app.route('/account/alternate/endpoint', methods=['GET'])
227 | def echo_alternate():
228 | return create_response(request)
229 |
230 |
231 | @app.route('/account/alternate/endpoint', methods=['POST'])
232 | def account_alternate_listener():
233 | return webhook_listener(request)
234 |
235 |
236 | @app.route('/account/endpoint', methods=['POST'])
237 | def account_listener():
238 | return webhook_listener(request)
239 |
240 |
241 | if __name__ == '__main__':
242 | app.run(debug=True, port=9090, host='0.0.0.0')
243 |
244 |
245 | You can save that code off into it's own project if you want just make sure to
246 | install Flask.
247 |
248 | Next, start one or more celery workers from the project root:
249 |
250 | ::
251 |
252 | $ celery -A pywebhooks.tasks.webhook_notification worker --loglevel=info
253 |
254 | Start the main project in development mode:
255 |
256 | ::
257 |
258 | $ python app.py
259 |
260 | With your endpoint service and Celery worker running you can now perform
261 | the following calls.
262 |
263 | Account Actions
264 | ^^^^^^^^^^^^^^^
265 |
266 | **Creating an account:**
267 |
268 | The examples below use human readable user names. The reality is you should use
269 | a complex username to avoid any potential possibility of someone abusing the
270 | ``api_key`` reset as you only need a ``username`` to trigger a reset which could
271 | allow for a denial of service on your endpoint. A complex username not shared
272 | such as ``cRee82jfkjf09ij23`` is better than ``johndoe``. One potential fix
273 | I will look at is limiting how many ``api_key`` resets can be done in a given
274 | period (rate limiting). Also, the term "username" applies to the endpoint possibly
275 | being a service which is most likely the case so your username may actually be
276 | something like "myservice-listener-001" (as an example).
277 |
278 | If ``127.0.0.1`` is not working below try ``localhost`` or lookup the IP Docker is using.
279 | Make sure to set that IP address in the ``endpoint`` below.
280 |
281 | **Note:** Make sure you are running an endpoint since creating an account will verfiy
282 | the endpoint. You can use the example code above.
283 |
284 | ::
285 |
286 | curl -v -X POST "http://127.0.0.1:8081/v1/account" -d '{"endpoint": "http://127.0.0.1:9090/account/endpoint", "username": "sarahfranks"}' -H "content-type: application/json"
287 |
288 | **Response:**
289 |
290 | **HTTP/1.0 201 CREATED**
291 |
292 | ::
293 |
294 | {
295 | "api_key": "be23d9ccb29082c489ba629077553ba1d8314005",
296 | "endpoint": "http://127.0.0.1:9090/account/endpoint",
297 | "epoch": 1441164550.515677,
298 | "id": "45712a61-a1b3-41a4-aa89-9593b909ae3d",
299 | "is_admin": false,
300 | "failed_count": 0,
301 | "secret_key": "5a4a1cf4895441a1dfaa504c471510be819198e7",
302 | "username": "sarahfranks"
303 | }
304 |
305 | Make note of the ``id``, ``secret_key`` and ``api_key`` (because the ``api_key`` will be
306 | stored hashed).
307 |
308 | The ``secret_key`` will be used to validate the data coming into your endpoint
309 | is indeed from the PyWebhooks server and not something/someone else. If you are
310 | following along on a local dev machine make sure to stop your example endpoint server now
311 | and paste in the new ``secret_key`` value before running the next API call below. Now you
312 | can re-start the example endpoint server.
313 |
314 | The ``api_key`` will be used for any communication with the PyWebhooks server that
315 | isn't a publicly accessible call.
316 |
317 | The ``id`` will be the account id.
318 |
319 | The ``failed_count`` field tracks how many times an attempt (webhook POST) has
320 | failed to contact the specified endpoint. ``MAX_FAILED_COUNT`` is a config value
321 | that can be set (default is 250). If the ``failed_count`` exceeds the
322 | ``MAX_FAILED_COUNT`` value then no more webhook posts will occur for the user
323 | until this is reset. A successful endpoint contact will automatically reset
324 | this value to 0 if ``MAX_FAILED_COUNT`` has not been exceeded. This helps
325 | prevent an endpoint that is no longer responsive or moved (and not updated)
326 | from continuing to utilize system resources. In addition, updating the endpoint
327 | for a account will also reset the ``failed_count``.
328 |
329 | Retries on webhook endpoints are done three times before giving up. The
330 | ``DEFAULT_RETRY`` config value (defaults to 2 minutes) and ``DEFAULT_FINAL_RETRY``
331 | config value (defaults to 1 hour) can be adjusted for the three retries. Each
332 | failed attempt to contact the endpoint results in an increment in the ``failed_count``
333 | field of the user's account. If an endpoint is unreachable through the initial
334 | attempt to contact and the three retires then the ``failed_count`` value will
335 | be four.
336 |
337 | **Get a single account record:**
338 |
339 | You can only look-up your own account record.
340 |
341 | ::
342 |
343 | curl -v -X GET "http://127.0.0.1:8081/v1/account/45712a61-a1b3-41a4-aa89-9593b909ae3d" -H "content-type: application/json" -H "api-key: be23d9ccb29082c489ba629077553ba1d8314005" -H "username: sarahfranks"
344 |
345 | **Response:**
346 |
347 | **HTTP/1.0 200 OK**
348 |
349 | ::
350 |
351 | {
352 | "api_key": "pbkdf2:sha1:1000$vTuQRKeb$eec0bdffebde0d3c28290d41f4d848fbde04571c",
353 | "endpoint": "http://127.0.0.1:9090/account/endpoint",
354 | "epoch": 1441164550.515677,
355 | "id": "45712a61-a1b3-41a4-aa89-9593b909ae3d",
356 | "is_admin": false,
357 | "failed_count": 0,
358 | "secret_key": "5a4a1cf4895441a1dfaa504c471510be819198e7",
359 | "username": "sarahfranks"
360 | }
361 |
362 | **Get all account records (admin only):**
363 |
364 | This is a paginated call with ``start`` and ``limit`` params in the querystring.
365 |
366 | **REQUIRED** ``start`` is where in the records you want to start listing (0..n)
367 |
368 | **REQUIRED** ``limit`` is how many records to return
369 |
370 | In the example below I started at record #0 and asked for up to 10 records to return.
371 | You may also notice that a ``next_start`` field will show up in the JSON so you
372 | know where to set your next start (assuming you want to keep paging the records)
373 |
374 | ::
375 |
376 | curl -v -X GET "http://127.0.0.1:8081/v1/accounts?start=0&limit=10" -H "content-type: application/json" -H "api-key: ba86c64c24f361ddbcfe27be187d8d3002c9f43c" -H "username: admin"
377 |
378 | **Response:**
379 |
380 | **HTTP/1.0 200 OK**
381 |
382 | ::
383 |
384 | {
385 | "accounts": [
386 | {
387 | "api_key": "pbkdf2:sha1:1000$rQDzv29j$5895b2393171d0cc238157c130fc2129d3e871c3",
388 | "endpoint": "",
389 | "epoch": 1441164269.341982,
390 | "id": "ed408f85-200e-481f-a672-30f454e8dcf4",
391 | "is_admin": true,
392 | "secret_key": "ab502753cbb68b90601cace345fe84fb2bb5b8dd",
393 | "username": "admin"
394 | },
395 | {
396 | "api_key": "pbkdf2:sha1:1000$I5r0MTsM$fc50fcce05c526fa19919d874087623571c0c9e0",
397 | "endpoint": "http://127.0.0.1:9090/account/endpoint",
398 | "epoch": 1441164337.607172,
399 | "id": "d969a56d-e520-405d-a24f-497ac6923781",
400 | "is_admin": false,
401 | "failed_count": 0,
402 | "secret_key": "2381a87ba4725786f29ca414d3217e202615f757",
403 | "username": "johndoe"
404 | },
405 | {
406 | "api_key": "pbkdf2:sha1:1000$an7K8KqL$127bb4796de21a832969512fc7c2edea0524e54b",
407 | "endpoint": "http://127.0.0.1:9090/account/endpoint",
408 | "epoch": 1441164337.630147,
409 | "id": "556daec0-fcad-4cae-8d4b-7564d2424669",
410 | "is_admin": false,
411 | "failed_count": 0,
412 | "secret_key": "25b83d9a713e16f1b4fe936787acdf532162ea73",
413 | "username": "janedoe"
414 | },
415 | {
416 | "api_key": "pbkdf2:sha1:1000$nbvEItNd$9d0ab21a122bca95855f6ba0ab271444168e17f4",
417 | "endpoint": "http://127.0.0.1:9090/account/endpoint",
418 | "epoch": 1441164337.65272,
419 | "id": "776236bc-5ca9-4083-bb20-b12043ec87de",
420 | "is_admin": false,
421 | "failed_count": 0,
422 | "secret_key": "d615166b1818ef41b925c40b5483474522bffc94",
423 | "username": "samjones"
424 | },
425 | {
426 | "api_key": "pbkdf2:sha1:1000$vTuQRKeb$eec0bdffebde0d3c28290d41f4d848fbde04571c",
427 | "endpoint": "http://127.0.0.1:9090/account/endpoint",
428 | "epoch": 1441164550.515677,
429 | "id": "45712a61-a1b3-41a4-aa89-9593b909ae3d",
430 | "is_admin": false,
431 | "failed_count": 0,
432 | "secret_key": "5a4a1cf4895441a1dfaa504c471510be819198e7",
433 | "username": "sarahfranks"
434 | }
435 | ]
436 | }
437 |
438 | Example output with ``next_start``:
439 |
440 | ::
441 |
442 | curl -v -X GET "http://127.0.0.1:8081/v1/accounts?start=0&limit=3" -H "content-type: application/json" -H "api-key: 5b3a973f4980f65d5b61101ddf3b40808933f12a" -H "username: admin"
443 |
444 | ::
445 |
446 | {
447 | "accounts": [
448 | {
449 | "api_key": "pbkdf2:sha1:1000$rQDzv29j$5895b2393171d0cc238157c130fc2129d3e871c3",
450 | "endpoint": "",
451 | "epoch": 1441164269.341982,
452 | "id": "ed408f85-200e-481f-a672-30f454e8dcf4",
453 | "is_admin": true,
454 | "secret_key": "ab502753cbb68b90601cace345fe84fb2bb5b8dd",
455 | "username": "admin"
456 | },
457 | {
458 | "api_key": "pbkdf2:sha1:1000$I5r0MTsM$fc50fcce05c526fa19919d874087623571c0c9e0",
459 | "endpoint": "http://127.0.0.1:9090/account/endpoint",
460 | "epoch": 1441164337.607172,
461 | "id": "d969a56d-e520-405d-a24f-497ac6923781",
462 | "is_admin": false,
463 | "failed_count": 0,
464 | "secret_key": "2381a87ba4725786f29ca414d3217e202615f757",
465 | "username": "johndoe"
466 | },
467 | {
468 | "api_key": "pbkdf2:sha1:1000$an7K8KqL$127bb4796de21a832969512fc7c2edea0524e54b",
469 | "endpoint": "http://127.0.0.1:9090/account/endpoint",
470 | "epoch": 1441164337.630147,
471 | "id": "556daec0-fcad-4cae-8d4b-7564d2424669",
472 | "is_admin": false,
473 | "failed_count": 0,
474 | "secret_key": "25b83d9a713e16f1b4fe936787acdf532162ea73",
475 | "username": "janedoe"
476 | }
477 | ],
478 | "next_start": 3
479 | }
480 |
481 | **Update the endpoint field for a username specified account:**
482 |
483 | The only field that can be updated on an account is the ``endpoint`` and when you
484 | do so PyWebhooks will contact that endpoint with the echo challenge as mentioned above
485 | in the section on creating a new account.
486 |
487 | **Note:** The ``api_key`` and ``secret_key`` can both be reset, those calls are
488 | further down this document.
489 |
490 | For this call you need to supply your username and ``api_key`` in the headers.
491 |
492 | ::
493 |
494 | curl -v -X PATCH "http://127.0.0.1:8081/v1/account" -d '{"endpoint": "http://127.0.0.1:9090/account/alternate/endpoint"}' -H "content-type: application/json" -H "api-key: d615166b1818ef41b925c40b5483474522bffc94" -H "username: samjones"
495 |
496 | **Response:**
497 |
498 | **HTTP/1.0 200 OK**
499 |
500 | ::
501 |
502 | {
503 | "deleted": 0,
504 | "errors": 0,
505 | "inserted": 0,
506 | "replaced": 1,
507 | "skipped": 0,
508 | "unchanged": 0
509 | }
510 |
511 | **Delete a single account record:**
512 |
513 | User's can only delete their account record.
514 |
515 | ::
516 |
517 | curl -v -X DELETE "http://127.0.0.1:8081/v1/account/776236bc-5ca9-4083-bb20-b12043ec87de" -H "content-type: application/json" -H "api-key: d615166b1818ef41b925c40b5483474522bffc94" -H "username: samjones"
518 |
519 | **Response:**
520 |
521 | **HTTP/1.0 200 OK**
522 |
523 | ::
524 |
525 | {
526 | "deleted": 1,
527 | "errors": 0,
528 | "inserted": 0,
529 | "replaced": 0,
530 | "skipped": 0,
531 | "unchanged": 0
532 | }
533 |
534 | **Delete all account records (admin only):**
535 |
536 | **Careful:** This deletes all account records (except admin). The ``deleted``
537 | field in the response will contain how many records were deleted.
538 |
539 | ::
540 |
541 | curl -v -X DELETE "http://127.0.0.1:8081/v1/accounts" -H "content-type: application/json" -H "api-key: f2fe92411648dab36532d4256a5d36be0b219d53" -H "username: admin"
542 |
543 | **Response:**
544 |
545 | **HTTP/1.0 200 OK**
546 |
547 | ::
548 |
549 | {
550 | "deleted": 4,
551 | "errors": 0,
552 | "inserted": 0,
553 | "replaced": 0,
554 | "skipped": 0,
555 | "unchanged": 0
556 | }
557 |
558 | **Reset an account API key:**
559 |
560 | Ensure your service endpoint is running as the PyWebhooks server will perform a
561 | ``GET`` against your endpoint with the new ``api_key`` in the querystring as:
562 |
563 | ::
564 |
565 | GET /account/alternate/endpoint?api_key=768a8c2530956c0f2ac52faee785cadf3f5bc68d
566 |
567 | **Note:** A ``GET`` is used on the endpoint like the echo challenge since ``POST`` is
568 | used by incoming webhooks.
569 |
570 | ::
571 |
572 | curl -v -X POST "http://127.0.0.1:8081/v1/account/reset/apikey" -H "content-type: application/json" -H "username: sarahfranks"
573 |
574 | **Response:**
575 |
576 | **HTTP/1.0 200 OK**
577 |
578 | ::
579 |
580 | {
581 | "Message": "New key sent to endpoint"
582 | }
583 |
584 | **Reset an account secret key:**
585 |
586 | Ensure your service endpoint is running as the PyWebhooks server will perform a
587 | ``GET`` against your endpoint with the new ``secret_key`` in the querystring as:
588 |
589 | ::
590 |
591 | GET /account/alternate/endpoint?secret_key=0d7929e61c97e10a70dd71cb839853bcd4f9e230
592 |
593 | **Note:** A ``GET`` is used on the endpoint like the echo challenge since ``POST`` is
594 | used by incoming webhooks.
595 |
596 | ::
597 |
598 | curl -v -X POST "http://127.0.0.1:8081/v1/account/reset/secretkey" -H "content-type: application/json" -H "username: johndoe" -H "api-key: 9241a57a6b4d785d7acb0fe9d99f7983f4d7584b"
599 |
600 | **Response:**
601 |
602 | **HTTP/1.0 200 OK**
603 |
604 | ::
605 |
606 | {
607 | "Message": "New key sent to endpoint"
608 | }
609 |
610 | Webhook Actions
611 | ^^^^^^^^^^^^^^^
612 |
613 | The real essence of PyWebhooks is ultimately registering a webhook with the system
614 | and then having users/services subscribe to those webhooks and posting the data
615 | to your endpoint.
616 |
617 | **Creating a new webhook registration:**
618 |
619 | In this example we will register the following webhook from the ``johndoe``
620 | account.
621 |
622 | ::
623 |
624 | {
625 | "items": [
626 | {
627 | "item1": 1
628 | },
629 | {
630 | "item2": 2
631 | }
632 | ],
633 | "message": "hello world"
634 | }
635 |
636 | There are a few things you need to include in the JSON payload.
637 |
638 | ``description`` is a user comsumable description of what your webhook is about
639 | ``event_data`` is the actual JSON payload that will be delivered to each
640 | subscribed user/service of this webhook when you trigger it
641 | ``event`` is a header field that is a short description of what kind of event
642 | this is
643 |
644 | The full payload would be something like this:
645 |
646 | ::
647 |
648 | {
649 | "description": "This is my registered webhook",
650 | "event_data": {
651 | "items": [
652 | {
653 | "item1": 1
654 | },
655 | {
656 | "item2": 2
657 | }
658 | ],
659 | "message": "hello world"
660 | },
661 | "event": "mywebhook.event"
662 | }
663 |
664 | Create the webhook:
665 |
666 | ::
667 |
668 | curl -v -X POST "http://127.0.0.1:8081/v1/webhook/registration" -H "content-type: application/json" -H "username: johndoe" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97" -d '{"description": "This is my registered webhook", "event_data": {"items": [{"item1": 1}, {"item2": 2}], "message": "hello world"}, "event": "mywebhook.event"}'
669 |
670 | **Response:**
671 |
672 | **HTTP/1.0 201 CREATED**
673 |
674 | ::
675 |
676 | {
677 | "account_id": "d969a56d-e520-405d-a24f-497ac6923781",
678 | "description": "This is my registered webhook",
679 | "epoch": 1441166640.359496,
680 | "event": "mywebhook.event",
681 | "event_data": {
682 | "items": [
683 | {
684 | "item1": 1
685 | },
686 | {
687 | "item2": 2
688 | }
689 | ],
690 | "message": "hello world"
691 | },
692 | "id": "3e25a22e-6a83-4cf0-a2bf-d7617aa32551"
693 | }
694 |
695 | **Delete a webhook registration:**
696 |
697 | Deletes registration record, will also remove the records for this registration
698 | id in the subscription table as well.
699 |
700 | ::
701 |
702 | curl -v -X DELETE "http://127.0.0.1:8081/v1/webhook/registration/0c296ca8-69ce-4274-b377-3010072363f9" -H "content-type: application/json" -H "username: johndoe" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97"
703 |
704 | **Response:**
705 |
706 | **HTTP/1.0 200 OK**
707 |
708 | ::
709 |
710 | {
711 | "deleted": 1,
712 | "errors": 0,
713 | "inserted": 0,
714 | "replaced": 0,
715 | "skipped": 0,
716 | "unchanged": 0
717 | }
718 |
719 | **Get all your registered webhook records:**
720 |
721 | Lists all the calling username's registered webhooks.
722 |
723 | This is a paginated call with ``start`` and ``limit`` params in the querystring.
724 |
725 | **REQUIRED** ``start`` is where in the records you want to start listing (0..n)
726 |
727 | **REQUIRED** ``limit`` is how many records to return
728 |
729 | ::
730 |
731 | curl -v -X GET "http://127.0.0.1:8081/v1/webhook/registration?start=0&limit=10" -H "content-type: application/json" -H "username: johndoe" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97"
732 |
733 | **Response:**
734 |
735 | **HTTP/1.0 200 OK**
736 |
737 | ::
738 |
739 | {
740 | "next_start": 1,
741 | "registrations": [
742 | {
743 | "account_id": "fb8854ba-b7f7-4552-bc13-4d5cdbb444dd",
744 | "description": "This is my registered webhook",
745 | "epoch": 1441139002.671599,
746 | "event": "mywebhook.event",
747 | "event_data": {
748 | "items": [
749 | {
750 | "item1": 1
751 | },
752 | {
753 | "item2": 2
754 | }
755 | ],
756 | "message": "hello world"
757 | },
758 | "id": "4618dc47-aaf9-401e-9aa4-8fda5d59eb25"
759 | }
760 | ]
761 | }
762 |
763 | **Get all registered webhook records:**
764 |
765 | Lists all registered webhooks.
766 |
767 | This is a paginated call with ``start`` and ``limit`` params in the querystring.
768 |
769 | **REQUIRED** ``start`` is where in the records you want to start listing (0..n)
770 |
771 | **REQUIRED** ``limit`` is how many records to return
772 |
773 | ::
774 |
775 | curl -v -X GET "http://127.0.0.1:8081/v1/webhook/registrations?start=0&limit=2" -H "content-type: application/json" -H "username: johndoe" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97"
776 |
777 | **Response:**
778 |
779 | **HTTP/1.0 200 OK**
780 |
781 | ::
782 |
783 | {
784 | "next_start": 2,
785 | "registrations": [
786 | {
787 | "account_id": "a6903d9f-de93-4910-8d8c-06e22f434d05",
788 | "description": "Some description goes here",
789 | "epoch": 1441138315.006409,
790 | "event": "webhook.event.hello",
791 | "event_data": {
792 | "msg": "hello world"
793 | },
794 | "id": "ae8dc785-d4bf-4614-98a7-32dcf03314e8"
795 | },
796 | {
797 | "account_id": "fb8854ba-b7f7-4552-bc13-4d5cdbb444dd",
798 | "description": "This is my registered webhook",
799 | "epoch": 1441139002.671599,
800 | "event": "mywebhook.event",
801 | "event_data": {
802 | "items": [
803 | {
804 | "item1": 1
805 | },
806 | {
807 | "item2": 2
808 | }
809 | ],
810 | "message": "hello world"
811 | },
812 | "id": "4618dc47-aaf9-401e-9aa4-8fda5d59eb25"
813 | }
814 | ]
815 | }
816 |
817 | **Delete all webhook registration records (admin only):**
818 |
819 | **Careful:** This deletes all registration records. The ``deleted``
820 | field in the response will contain how many records were deleted.
821 |
822 | ::
823 |
824 | curl -v -X DELETE "http://127.0.0.1:8081/v1/webhook/registrations" -H "content-type: application/json" -H "api-key: ba86c64c24f361ddbcfe27be187d8d3002c9f43c" -H "username: admin"
825 |
826 | **Response:**
827 |
828 | **HTTP/1.0 200 OK**
829 |
830 | ::
831 |
832 | {
833 | "deleted": 2,
834 | "errors": 0,
835 | "inserted": 0,
836 | "replaced": 0,
837 | "skipped": 0,
838 | "unchanged": 0
839 | }
840 |
841 | **Update a webhook registration record:**
842 |
843 | Only the ``description`` field can be updated on an registration.
844 |
845 | Make sure to supply the webhook registration id as per the example.
846 |
847 | ::
848 |
849 | curl -v -X PATCH "http://127.0.0.1:8081/v1/webhook/registration/4618dc47-aaf9-401e-9aa4-8fda5d59eb25" -d '{"description": "New Description"}' -H "content-type: application/json" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97" -H "username: johndoe"
850 |
851 | **Response:**
852 |
853 | **HTTP/1.0 200 OK**
854 |
855 | ::
856 |
857 | {
858 | "deleted": 0,
859 | "errors": 0,
860 | "inserted": 0,
861 | "replaced": 1,
862 | "skipped": 0,
863 | "unchanged": 0
864 | }
865 |
866 | Subscription Actions
867 | ^^^^^^^^^^^^^^^^^^^^
868 |
869 | **Creating a subscription:**
870 |
871 | Create a subscription for a registered webhook that you want to receive
872 | notifications from when they are triggered.
873 |
874 | ::
875 |
876 | curl -v -X POST "http://127.0.0.1:8081/v1/webhook/subscription/ae8dc785-d4bf-4614-98a7-32dcf03314e8" -H "content-type: application/json" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97" -H "username: johndoe"
877 |
878 |
879 | **Response:**
880 |
881 | **HTTP/1.0 201 CREATED**
882 |
883 | ::
884 |
885 | {
886 | "account_id": "fb8854ba-b7f7-4552-bc13-4d5cdbb444dd",
887 | "epoch": 1441145067.959285,
888 | "id": "cf20c039-6355-40b9-a601-cad4e79dbe52",
889 | "registration_id": "ae8dc785-d4bf-4614-98a7-32dcf03314e8"
890 | }
891 |
892 | **Get all your subscription records:**
893 |
894 | Lists all the calling username's subscription records.
895 |
896 | This is a paginated call with ``start`` and ``limit`` params in the querystring.
897 |
898 | **REQUIRED** ``start`` is where in the records you want to start listing (0..n)
899 |
900 | **REQUIRED** ``limit`` is how many records to return
901 |
902 | ::
903 |
904 | curl -v -X GET "http://127.0.0.1:8081/v1/webhook/subscription?start=0&limit=5" -H "content-type: application/json" -H "username: johndoe" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97"
905 |
906 | **Response:**
907 |
908 | **HTTP/1.0 200 OK**
909 |
910 | ::
911 |
912 | {
913 | "subscriptions": [
914 | {
915 | "account_id": "fb8854ba-b7f7-4552-bc13-4d5cdbb444dd",
916 | "epoch": 1441144968.505692,
917 | "id": "9e596765-da94-46d2-9f9d-a4d7ecc374ab",
918 | "registration_id": "ae8dc785-d4bf-4614-98a7-32dcf03314e8"
919 | },
920 | {
921 | "account_id": "fb8854ba-b7f7-4552-bc13-4d5cdbb444dd",
922 | "epoch": 1441145067.959285,
923 | "id": "cf20c039-6355-40b9-a601-cad4e79dbe52",
924 | "registration_id": "ac18dc47-abf9-401e-8bb3-8fda5d51af48"
925 | }
926 | ]
927 | }
928 |
929 | **Get all subscription records:**
930 |
931 | Lists all subscriptions.
932 |
933 | This is a paginated call with ``start`` and ``limit`` params in the querystring.
934 |
935 | **REQUIRED** ``start`` is where in the records you want to start listing (0..n)
936 |
937 | **REQUIRED** ``limit`` is how many records to return
938 |
939 | ::
940 |
941 | curl -v -X GET "http://127.0.0.1:8081/v1/webhook/subscriptions?start=0&limit=2" -H "content-type: application/json" -H "username: johndoe" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97"
942 |
943 | **Response:**
944 |
945 | **HTTP/1.0 200 OK**
946 |
947 | ::
948 |
949 | {
950 | "next_start": 2,
951 | "subscriptions": [
952 | {
953 | "account_id": "fb8854ba-b7f7-4552-bc13-4d5cdbb444dd",
954 | "epoch": 1441144968.505692,
955 | "id": "9e596765-da94-46d2-9f9d-a4d7ecc374ab",
956 | "registration_id": "ae8dc785-d4bf-4614-98a7-32dcf03314e8"
957 | },
958 | {
959 | "account_id": "fb8854ba-b7f7-4552-bc13-4d5cdbb444dd",
960 | "epoch": 1441145067.959285,
961 | "id": "cf20c039-6355-40b9-a601-cad4e79dbe52",
962 | "registration_id": "ae8dc785-d4bf-4614-98a7-32dcf03314e8"
963 | }
964 | ]
965 | }
966 |
967 | **Delete a single subscription record:**
968 |
969 | Deletes subscription record.
970 |
971 | ::
972 |
973 | curl -v -X DELETE "http://127.0.0.1:8081/v1/webhook/subscription/bfbafaa0-5816-456d-9639-98023ec5dc2e" -H "content-type: application/json" -H "username: johndoe" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97"
974 |
975 | **Response:**
976 |
977 | **HTTP/1.0 200 OK**
978 |
979 | ::
980 |
981 | {
982 | "deleted": 1,
983 | "errors": 0,
984 | "inserted": 0,
985 | "replaced": 0,
986 | "skipped": 0,
987 | "unchanged": 0
988 | }
989 |
990 | **Delete all subscription records (admin only):**
991 |
992 | **Careful:** This deletes all subscription records. The ``deleted``
993 | field in the response will contain how many records were deleted.
994 |
995 | ::
996 |
997 | curl -v -X DELETE "http://127.0.0.1:8081/v1/webhook/subscriptions" -H "content-type: application/json" -H "api-key: ba86c64c24f361ddbcfe27be187d8d3002c9f43c" -H "username: admin"
998 |
999 | **Response:**
1000 |
1001 | **HTTP/1.0 200 OK**
1002 |
1003 | ::
1004 |
1005 | {
1006 | "deleted": 4,
1007 | "errors": 0,
1008 | "inserted": 0,
1009 | "replaced": 0,
1010 | "skipped": 0,
1011 | "unchanged": 0
1012 | }
1013 |
1014 | Triggered Actions
1015 | ^^^^^^^^^^^^^^^^^
1016 |
1017 | There are two actions that can be done:
1018 |
1019 | 1. Trigger a webhook
1020 |
1021 | 2. List all the triggered webhooks
1022 |
1023 | **Trigger a webhook:**
1024 |
1025 | Use a registration id to trigger the webhook (inserts a triggered record).
1026 |
1027 | ::
1028 |
1029 | curl -v -X POST "http://127.0.0.1:8081/v1/webhook/triggered/bfbafaa0-5816-456d-9639-98023ec5dc2e" -H "content-type: application/json" -H "api-key: ee98cb7b5da901c12bac7c263b28f7a028a5de97" -H "username: johndoe"
1030 |
1031 | **Response:**
1032 |
1033 | **HTTP/1.0 201 CREATED**
1034 |
1035 | ::
1036 |
1037 | {
1038 | "epoch": 1441334032.467688,
1039 | "id": "7c9cfb5c-dd9b-47cc-8579-32e06337e0f9",
1040 | "registration_id": "bfbafaa0-5816-456d-9639-98023ec5dc2e"
1041 | }
1042 |
1043 | **Get all triggered webhooks:**
1044 |
1045 | Lists all triggered records.
1046 |
1047 | This is a paginated call with ``start`` and ``limit`` params in the querystring.
1048 |
1049 | **REQUIRED** ``start`` is where in the records you want to start listing (0..n)
1050 |
1051 | **REQUIRED** ``limit`` is how many records to return
1052 |
1053 | ::
1054 |
1055 | {
1056 | "triggered_webhooks": [
1057 | {
1058 | "epoch": 1441333750.649395,
1059 | "id": "fc20ee3f-2278-4d14-1058-afab5b2c1b34",
1060 | "registration_id": "bfbafaa0-5816-456d-9639-98023ec5dc2e"
1061 | },
1062 | {
1063 | "epoch": 1441333775.45855,
1064 | "id": "abf196cf-e3cd-47d5-9458-ecc22e5e1ae3",
1065 | "registration_id": "3279b8af-3a90-4cf1-afb8-12872849b2ac"
1066 | },
1067 | {
1068 | "epoch": 1441333841.789931,
1069 | "id": "77c674fc-1907-499e-8e52-3faa57804977",
1070 | "registration_id": "3279b8af-3a90-4cf1-afb8-12872849b2ac"
1071 | },
1072 | {
1073 | "epoch": 1441334032.467688,
1074 | "id": "7c9cfb5c-dd9b-47cc-8579-32e06337e0f9",
1075 | "registration_id": "3279b8af-3a90-4cf1-afb8-12872849b2ac"
1076 | }
1077 | ]
1078 | }
1079 |
1080 | **Response:**
1081 |
1082 | **HTTP/1.0 200 OK**
1083 |
1084 | License
1085 | ^^^^^^^
1086 |
1087 | Licensed under the Apache License, Version 2.0 (the "License");
1088 | you may not use this file except in compliance with the License.
1089 | You may obtain a copy of the License at
1090 |
1091 | http://www.apache.org/licenses/LICENSE-2.0
1092 |
1093 | Unless required by applicable law or agreed to in writing, software
1094 | distributed under the License is distributed on an "AS IS" BASIS,
1095 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1096 | See the License for the specific language governing permissions and
1097 | limitations under the License.
1098 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.5.5
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | rethinkdb:
4 | image: rethinkdb:latest
5 |
6 | redis:
7 | image: redis:latest
8 |
9 | pywebhooks-worker:
10 | container_name: pywebhooks-worker
11 | build:
12 | context: ../
13 | dockerfile: docker/pywebhooks-worker.Dockerfile
14 | command: "celery -A pywebhooks.tasks.webhook_notification worker --loglevel=info"
15 | links:
16 | - rethinkdb
17 | - redis
18 | depends_on:
19 | - rethinkdb
20 | - redis
21 |
22 | pywebhooks-server:
23 | container_name: pywebhooks-server
24 | build:
25 | context: ../
26 | dockerfile: docker/pywebhooks-server.Dockerfile
27 | command: "bash -c 'pywebhooks --initdb && pywebhooks'"
28 | # If you don't want to wipe out the database each time do this instead:
29 | # command: "pywebhooks'"
30 | ports:
31 | - "8081:8081"
32 | links:
33 | - rethinkdb
34 | - redis
35 | depends_on:
36 | - rethinkdb
37 | - redis
38 |
--------------------------------------------------------------------------------
/docker/pywebhooks-server.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6
2 |
3 | COPY . /opt/pywebhooks
4 | WORKDIR /opt/pywebhooks
5 |
6 | RUN pip install -U pip
7 | RUN pip install -r requirements.txt
8 | RUN pip install -e .
9 |
10 | EXPOSE 8081
11 |
--------------------------------------------------------------------------------
/docker/pywebhooks-worker.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.6-slim
2 |
3 | RUN groupadd user && useradd --create-home --home-dir /home/user -g user user
4 | WORKDIR /home/user
5 |
6 | COPY . /home/user/pywebhooks-worker
7 | WORKDIR /home/user/pywebhooks-worker
8 |
9 | RUN pip install -U pip
10 | RUN pip install -r requirements.txt
11 | RUN pip install -e .
12 |
13 | USER user
14 | CMD ["celery", "worker"]
15 |
--------------------------------------------------------------------------------
/pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 | ignore=tests
3 | [REPORTS]
4 | # set the output format. Available formats are text, parseable, colorized and
5 | # html
6 | output-format=colorized
7 | # Include message's id in output
8 | include-ids=yes
9 | # Put messages in a separate file for each module / package specified on the
10 | # command line instead of printing them on stdout. Reports (if any) will be
11 | # written in a file name "pylint_global.[txt|html]".
12 | files-output=no
13 | # Tells whether to display a full report or only the messages
14 | reports=yes
15 | [DESIGN]
16 | # Maximum number of arguments for function / method
17 | max-args=9
18 | # Maximum number of attributes for a class (see R0902).
19 | max-attributes=10
20 |
--------------------------------------------------------------------------------
/pywebhooks/__init__.py:
--------------------------------------------------------------------------------
1 | DEFAULT_DB_NAME = 'pywebhooks'
2 | DEFAULT_ACCOUNTS_TABLE = 'accounts'
3 | DEFAULT_REGISTRATIONS_TABLE = 'registrations'
4 | DEFAULT_TRIGGERED_TABLE = 'triggered_webhooks'
5 | DEFAULT_SUBSCRIPTIONS_TABLE = 'subscriptions'
6 |
7 | DEFAULT_TABLE_NAMES = [
8 | DEFAULT_ACCOUNTS_TABLE,
9 | DEFAULT_REGISTRATIONS_TABLE,
10 | DEFAULT_TRIGGERED_TABLE,
11 | DEFAULT_SUBSCRIPTIONS_TABLE
12 | ]
13 |
14 | # This is the timeout for the response time from the client's endpoint. This is
15 | # used when validating a new account or they attempt to change a secret or
16 | # api key and in sending out webhook events. This should be a low value and end
17 | # users should be aware of this time (in seconds) in which to respond.
18 | REQUEST_TIMEOUT = 5.0
19 |
20 | # Retry a failed webhook notification to an endpoint in 2 minutes
21 | DEFAULT_RETRY = 120
22 | DEFAULT_FINAL_RETRY = 3600 # On the final retry, try again in an hour
23 |
24 | # How many times a webhook post can fail to contact the endpoint before
25 | # its ignored
26 | MAX_FAILED_COUNT = 250
27 |
28 | RETHINK_HOST = 'rethinkdb'
29 | CELERY_BROKER_URL = 'redis://redis:6379/0'
30 |
31 | RETHINK_PORT = 28015
32 | RETHINK_AUTH_KEY = ''
33 |
--------------------------------------------------------------------------------
/pywebhooks/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/api/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/api/decorators/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/api/decorators/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/api/decorators/authorization.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | from functools import wraps
4 |
5 | # Third-party imports
6 | from flask import request, jsonify, make_response
7 | from werkzeug.security import check_password_hash
8 |
9 | # Project-level imports
10 | from pywebhooks import DEFAULT_ACCOUNTS_TABLE
11 | from pywebhooks.database.rethinkdb.interactions import Interactions
12 |
13 |
14 | def api_key_restricted_resource(verify_admin=False):
15 | """
16 | Validate the API Key and Username in the header
17 | Note: This is very basic authorization for a proof of concept
18 | """
19 |
20 | def decorated(f):
21 | @wraps(f)
22 | def wrapper(*args, **kwargs):
23 | try:
24 | api_key = request.headers['api-key']
25 | except KeyError:
26 | return make_response(
27 | jsonify(
28 | {'Error': 'Missing API key header value'}
29 | ), client.UNAUTHORIZED
30 | )
31 |
32 | try:
33 | username = request.headers['username']
34 | except KeyError:
35 | return make_response(
36 | jsonify(
37 | {'Error': 'Missing username header value'}
38 | ), client.UNAUTHORIZED
39 | )
40 |
41 | record = Interactions.query(DEFAULT_ACCOUNTS_TABLE,
42 | filters={'username': username})
43 |
44 | if not record:
45 | return make_response(
46 | jsonify({'Error': 'Invalid API key or Username'}),
47 | client.UNAUTHORIZED
48 | )
49 |
50 | if not check_password_hash(record[0]['api_key'], api_key):
51 | return make_response(
52 | jsonify({'Error': 'Invalid API key'}), client.UNAUTHORIZED)
53 |
54 | if verify_admin:
55 | if not record[0]['is_admin']:
56 | return make_response(
57 | jsonify({'Error': 'Not an Admin'}),
58 | client.UNAUTHORIZED
59 | )
60 |
61 | return f(*args, **kwargs)
62 |
63 | return wrapper
64 | return decorated
65 |
--------------------------------------------------------------------------------
/pywebhooks/api/decorators/validation.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | from functools import wraps
4 |
5 | # Third-party imports
6 | from flask import request, jsonify, make_response
7 |
8 | # Project-level imports
9 | from pywebhooks import DEFAULT_ACCOUNTS_TABLE
10 | from pywebhooks.database.rethinkdb.interactions import Interactions
11 |
12 |
13 | def validate_pagination_params():
14 | """
15 | Validate the API Key and Username in the header
16 | Note: This is very basic authorization for a proof of concept
17 | """
18 |
19 | def decorated(f):
20 | @wraps(f)
21 | def wrapper(*args, **kwargs):
22 | try:
23 | limit = int(request.args.get('limit'))
24 | start = int(request.args.get('start'))
25 |
26 | if start > 999999999999999 or start < 0:
27 | raise ValueError()
28 | if limit > 100 or limit <= 0:
29 | raise ValueError()
30 | except (ValueError, TypeError):
31 | return make_response(
32 | jsonify({'Error': 'Invalid limit or start parameter'}),
33 | client.BAD_REQUEST
34 | )
35 |
36 | return f(*args, **kwargs)
37 |
38 | return wrapper
39 | return decorated
40 |
41 |
42 | def validate_username_in_header():
43 | """
44 | Validate that the username header is set and exists in the accounts table
45 | """
46 |
47 | def decorated(f):
48 | @wraps(f)
49 | def wrapper(*args, **kwargs):
50 | try:
51 | username = request.headers['username']
52 | except KeyError:
53 | return make_response(
54 | jsonify(
55 | {'Error': 'Missing the username header value'}
56 | ), client.BAD_REQUEST
57 | )
58 |
59 | record = Interactions.query(DEFAULT_ACCOUNTS_TABLE,
60 | filters={'username': username})
61 |
62 | if not record:
63 | return make_response(
64 | jsonify({'Error': 'Username not found'}), client.NOT_FOUND)
65 |
66 | return f(*args, **kwargs)
67 |
68 | return wrapper
69 | return decorated
70 |
71 |
72 | def validate_id_params(param_name):
73 | """
74 | Validate the that the param_name is in the request
75 | """
76 | def decorated(f):
77 | @wraps(f)
78 | def wrapper(*args, **kwargs):
79 | try:
80 | if not kwargs.get(param_name):
81 | return make_response(jsonify(
82 | {'Error': 'Missing {0}'.format(param_name)}),
83 | client.BAD_REQUEST)
84 | except Exception as ex:
85 | return make_response(
86 | jsonify({'Error': ex}),
87 | client.BAD_REQUEST
88 | )
89 |
90 | return f(*args, **kwargs)
91 |
92 | return wrapper
93 | return decorated
94 |
--------------------------------------------------------------------------------
/pywebhooks/api/handlers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/api/handlers/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/api/handlers/pagination_handler.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 |
4 | # Third-party imports
5 | from flask import make_response, jsonify
6 |
7 | # Project-level imports
8 | from pywebhooks.database.rethinkdb.interactions import Interactions
9 |
10 |
11 | def paginate(request, table_name, resource_name, filters=None):
12 |
13 | limit = int(request.args.get('limit'))
14 | start = int(request.args.get('start'))
15 |
16 | if not filters:
17 | filters = {}
18 |
19 | end = start + limit
20 |
21 | returned_records = Interactions.list(
22 | table_name, start, end, 'epoch', filters=filters)
23 |
24 | records = []
25 |
26 | for item in returned_records:
27 | records.append(item)
28 |
29 | if len(returned_records) == 0:
30 | return make_response('', client.NO_CONTENT)
31 |
32 | if len(returned_records) < limit:
33 | return_json = {
34 | resource_name: records
35 | }
36 | else:
37 | return_json = {
38 | 'next_start': end,
39 | resource_name: records
40 | }
41 |
42 | return make_response(jsonify(return_json), client.OK)
43 |
--------------------------------------------------------------------------------
/pywebhooks/api/handlers/resources_handler.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 |
4 | # Third-party imports
5 | from flask import make_response, jsonify
6 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
7 | from werkzeug.security import generate_password_hash
8 |
9 | # Project-level imports
10 | from pywebhooks import DEFAULT_SUBSCRIPTIONS_TABLE, \
11 | REQUEST_TIMEOUT, DEFAULT_ACCOUNTS_TABLE, DEFAULT_REGISTRATIONS_TABLE
12 | from pywebhooks.database.rethinkdb.interactions import Interactions
13 | from pywebhooks.utils import common
14 | from pywebhooks.utils.request_handler import RequestHandler
15 |
16 |
17 | def insert_account(table_name, **kwargs):
18 | """
19 | Creates new account records (handles POST traffic)
20 | Salts the api_key
21 | """
22 | try:
23 | # Username cannot already exist
24 | record = Interactions.query(
25 | table_name, filters={'username': kwargs['username']})
26 |
27 | if record:
28 | return make_response(
29 | jsonify({'Error': 'Username already exists'}), client.CONFLICT)
30 |
31 | original_api_key = kwargs['api_key']
32 | kwargs['api_key'] = generate_password_hash(kwargs['api_key'])
33 | account_data = Interactions.insert(table_name, **kwargs)
34 | account_data['api_key'] = original_api_key
35 |
36 | return make_response(jsonify(account_data), client.CREATED)
37 | except RqlRuntimeError as runtime_err:
38 | return make_response(jsonify({'Error': runtime_err.message}),
39 | client.INTERNAL_SERVER_ERROR)
40 | except RqlDriverError as rql_err:
41 | return make_response(jsonify({'Error': rql_err.message}),
42 | client.INTERNAL_SERVER_ERROR)
43 | except TypeError:
44 | return make_response(
45 | jsonify({'Error': 'Invalid parameter(s)'}), client.BAD_REQUEST)
46 |
47 |
48 | def insert(table_name, **kwargs):
49 | """
50 | Creates new records (handles POST traffic)
51 | """
52 | try:
53 | return make_response(
54 | jsonify(Interactions.insert(table_name, **kwargs)), client.CREATED)
55 | except RqlRuntimeError as runtime_err:
56 | return make_response(jsonify({'Error': runtime_err.message}),
57 | client.INTERNAL_SERVER_ERROR)
58 | except RqlDriverError as rql_err:
59 | return make_response(jsonify({'Error': rql_err.message}),
60 | client.INTERNAL_SERVER_ERROR)
61 | except TypeError:
62 | return make_response(
63 | jsonify({'Error': 'Invalid parameter(s)'}), client.BAD_REQUEST)
64 |
65 |
66 | def delete(table_name, record_id):
67 | """
68 | Deletes a single record (handles DELETE traffic)
69 | """
70 | try:
71 | return make_response(
72 | jsonify(Interactions.delete(table_name, record_id)), client.OK)
73 | except RqlRuntimeError as runtime_err:
74 | return make_response(jsonify({'Error': runtime_err.message}),
75 | client.INTERNAL_SERVER_ERROR)
76 | except RqlDriverError as rql_err:
77 | return make_response(jsonify({'Error': rql_err.message}),
78 | client.INTERNAL_SERVER_ERROR)
79 | except TypeError:
80 | return make_response(
81 | jsonify({'Error': 'Invalid id parameter'}), client.BAD_REQUEST)
82 |
83 |
84 | def delete_account(record_id):
85 | """
86 | Deletes a single account record, removes all traces of account from other
87 | tables
88 | """
89 | try:
90 | # Delete this account's subscriptions
91 | Interactions.delete_specific(
92 | DEFAULT_SUBSCRIPTIONS_TABLE, filters={'account_id': record_id})
93 |
94 | # Loop and delete any records subscribed to their registrations
95 | registrations = Interactions.query(DEFAULT_REGISTRATIONS_TABLE,
96 | filters={'account_id': record_id})
97 |
98 | for registration in registrations:
99 | delete_registration(registration['id'])
100 |
101 | return make_response(
102 | jsonify(Interactions.delete(DEFAULT_ACCOUNTS_TABLE, record_id)),
103 | client.OK)
104 | except RqlRuntimeError as runtime_err:
105 | return make_response(jsonify({'Error': runtime_err.message}),
106 | client.INTERNAL_SERVER_ERROR)
107 | except RqlDriverError as rql_err:
108 | return make_response(jsonify({'Error': rql_err.message}),
109 | client.INTERNAL_SERVER_ERROR)
110 | except TypeError:
111 | return make_response(
112 | jsonify({'Error': 'Invalid id parameter'}), client.BAD_REQUEST)
113 |
114 |
115 | def delete_registration(registration_id):
116 | """
117 | Deletes a single registration record, removes all traces of this
118 | registration from the subscription table
119 | """
120 | try:
121 | Interactions.delete_specific(
122 | DEFAULT_SUBSCRIPTIONS_TABLE,
123 | filters={'registration_id': registration_id})
124 |
125 | return make_response(
126 | jsonify(Interactions.delete_specific(
127 | DEFAULT_REGISTRATIONS_TABLE,
128 | filters={'id': registration_id})), client.OK)
129 | except RqlRuntimeError as runtime_err:
130 | return make_response(jsonify({'Error': runtime_err.message}),
131 | client.INTERNAL_SERVER_ERROR)
132 | except RqlDriverError as rql_err:
133 | return make_response(jsonify({'Error': rql_err.message}),
134 | client.INTERNAL_SERVER_ERROR)
135 | except TypeError:
136 | return make_response(
137 | jsonify({'Error': 'Invalid id parameter'}), client.BAD_REQUEST)
138 |
139 |
140 | def delete_accounts_except_admins():
141 | """
142 | Deletes all account records except those marked as admins, removes all
143 | traces of account from other tables
144 | """
145 | try:
146 | return make_response(
147 | jsonify(Interactions.delete_specific(
148 | DEFAULT_ACCOUNTS_TABLE,
149 | filters={'is_admin': False})), client.OK)
150 | except RqlRuntimeError as runtime_err:
151 | return make_response(jsonify({'Error': runtime_err.message}),
152 | client.INTERNAL_SERVER_ERROR)
153 | except RqlDriverError as rql_err:
154 | return make_response(jsonify({'Error': rql_err.message}),
155 | client.INTERNAL_SERVER_ERROR)
156 |
157 |
158 | def delete_all(table_name):
159 | """
160 | Deletes all records (handles DELETE traffic)
161 | """
162 | try:
163 | return make_response(
164 | jsonify(Interactions.delete_all(table_name)), client.OK)
165 | except RqlRuntimeError as runtime_err:
166 | return make_response(jsonify({'Error': runtime_err.message}),
167 | client.INTERNAL_SERVER_ERROR)
168 | except RqlDriverError as rql_err:
169 | return make_response(jsonify({'Error': rql_err.message}),
170 | client.INTERNAL_SERVER_ERROR)
171 |
172 |
173 | def query(table_name, record_id):
174 | """
175 | Gets a single record (handles GET traffic)
176 | """
177 | try:
178 | return make_response(
179 | jsonify(Interactions.get(table_name, record_id)), client.OK)
180 | except RqlRuntimeError as runtime_err:
181 | return make_response(jsonify({'Error': runtime_err.message}),
182 | client.INTERNAL_SERVER_ERROR)
183 | except RqlDriverError as rql_err:
184 | return make_response(jsonify({'Error': rql_err.message}),
185 | client.INTERNAL_SERVER_ERROR)
186 | except TypeError:
187 | return make_response(
188 | jsonify({'Error': 'Invalid id parameter'}), client.BAD_REQUEST)
189 |
190 |
191 | def update(table_name, record_id=None, username=None, updates={}):
192 | """
193 | Updates a single record (handles GET traffic)
194 | """
195 | try:
196 | if record_id:
197 | return make_response(
198 | jsonify(Interactions.update(
199 | table_name, record_id=record_id, updates=updates)),
200 | client.OK)
201 | else:
202 | return make_response(
203 | jsonify(Interactions.update(
204 | table_name,
205 | filters={'username': username},
206 | updates=updates)
207 | ), client.OK)
208 | except RqlRuntimeError as runtime_err:
209 | return make_response(jsonify({'Error': runtime_err.message}),
210 | client.INTERNAL_SERVER_ERROR)
211 | except RqlDriverError as rql_err:
212 | return make_response(jsonify({'Error': rql_err.message}),
213 | client.INTERNAL_SERVER_ERROR)
214 | except TypeError:
215 | return make_response(
216 | jsonify({'Error': 'Invalid parameter(s)'}), client.BAD_REQUEST)
217 |
218 |
219 | def client_echo_valid(endpoint):
220 | """
221 | This will validate if the user's endpoint is valid and returning the echo
222 | data sent to it
223 | """
224 | try:
225 | request_handler = RequestHandler(
226 | verify_ssl=False, request_timeout=REQUEST_TIMEOUT)
227 | validation_key = common.generate_key()
228 |
229 | try:
230 | returned_json, status_code = request_handler.get(
231 | endpoint, params={'echo': validation_key})
232 | # pylint: disable=W0703
233 | except:
234 | return False
235 |
236 | if status_code != client.OK:
237 | return False
238 | if returned_json['echo'] != validation_key:
239 | return False
240 | # pylint: disable=W0703
241 | except Exception:
242 | return False
243 |
244 | return True
245 |
246 |
247 | def client_reset_key(endpoint, key_type, key_value):
248 | """
249 | This will send an api_key or secret_key to the configured endpoint
250 | (assists with resets of an api_key or secret_key)
251 | """
252 | try:
253 | request_handler = RequestHandler(
254 | verify_ssl=False, request_timeout=REQUEST_TIMEOUT)
255 |
256 | try:
257 | returned_json, status_code = request_handler.get(
258 | endpoint, params={key_type: key_value})
259 | # pylint: disable=W0703
260 | except:
261 | return False
262 |
263 | if status_code != client.OK:
264 | return False
265 | # pylint: disable=W0703
266 | except Exception:
267 | return False
268 |
269 | return True
270 |
271 |
272 | def reset_key(username, key_type):
273 | """
274 | Resets either a secret key or api key
275 | """
276 | try:
277 | # Note: The validate_username_in_header decorator will verify the
278 | # username and record. The api_key_restricted_resource will validate
279 | # the username as well as a valid API key
280 | record = Interactions.query(DEFAULT_ACCOUNTS_TABLE,
281 | filters={"username": username})
282 | endpoint = record[0]['endpoint']
283 |
284 | if not endpoint:
285 | return make_response(
286 | jsonify({'Error': 'Endpoint not found'}),
287 | client.NOT_FOUND
288 | )
289 |
290 | new_key = common.generate_key()
291 | salted_new_key = generate_password_hash(new_key)
292 |
293 | if not client_reset_key(endpoint, key_type, new_key):
294 | return make_response(
295 | jsonify({'Error': 'Failed to contact the endpoint or wrong '
296 | 'HTTP status code returned'}),
297 | client.BAD_REQUEST
298 | )
299 |
300 | if key_type == 'api_key':
301 | update = {key_type: salted_new_key}
302 | else:
303 | update = {key_type: new_key}
304 |
305 | Interactions.update(DEFAULT_ACCOUNTS_TABLE,
306 | filters={"username": username},
307 | updates=update)
308 |
309 | return make_response(jsonify({'Message': 'New key sent to endpoint'}),
310 | client.OK)
311 | except RqlRuntimeError as runtime_err:
312 | return make_response(jsonify({'Error': runtime_err.message}),
313 | client.INTERNAL_SERVER_ERROR)
314 | except RqlDriverError as rql_err:
315 | return make_response(jsonify({'Error': rql_err.message}),
316 | client.INTERNAL_SERVER_ERROR)
317 |
318 |
319 | def lookup_account_id(username):
320 | """
321 | Looks up the user's account id based on username
322 | """
323 | try:
324 | record = Interactions.query(
325 | DEFAULT_ACCOUNTS_TABLE, filters={'username': username})
326 | return record[0]['id']
327 | except RqlRuntimeError as runtime_err:
328 | return runtime_err
329 | except RqlDriverError as rql_err:
330 | return rql_err
331 |
332 |
333 | def lookup_registration_id(account_id, registration_id):
334 | """
335 | Looks up registration based on account_id and pass the record back
336 | """
337 | try:
338 | return Interactions.query(
339 | DEFAULT_REGISTRATIONS_TABLE,
340 | filters={'account_id': account_id, 'id': registration_id})
341 | except RqlRuntimeError as runtime_err:
342 | return runtime_err
343 | except RqlDriverError as rql_err:
344 | return rql_err
345 |
346 |
347 | def lookup_subscription_id(account_id, subscription_id):
348 | """
349 | Looks up subscription based on account_id and pass the record back
350 | """
351 | try:
352 | return Interactions.query(
353 | DEFAULT_SUBSCRIPTIONS_TABLE,
354 | filters={'account_id': account_id, 'id': subscription_id})
355 | except RqlRuntimeError as runtime_err:
356 | return runtime_err
357 | except RqlDriverError as rql_err:
358 | return rql_err
359 |
360 |
361 | def validate_access(username, registration_id=None, subscription_id=None,
362 | incoming_account_id=None):
363 | """
364 | Validate access to resources
365 | """
366 | if username == 'admin':
367 | return None
368 |
369 | account_id = lookup_account_id(username)
370 |
371 | try:
372 | if registration_id:
373 | if not lookup_registration_id(account_id, registration_id):
374 | return make_response(
375 | jsonify({'Error': 'Not authorized'}), client.UNAUTHORIZED)
376 | if subscription_id:
377 | if not lookup_subscription_id(account_id, subscription_id):
378 | return make_response(
379 | jsonify({'Error': 'Not authorized'}), client.UNAUTHORIZED)
380 | if incoming_account_id:
381 | if incoming_account_id != account_id:
382 | return make_response(
383 | jsonify({'Error': 'Not authorized'}), client.UNAUTHORIZED)
384 | except (RqlRuntimeError, RqlDriverError, Exception):
385 | return make_response(
386 | jsonify({'Error': 'Account or registration record not found'}),
387 | client.NOT_FOUND)
388 |
389 | return None
390 |
391 |
392 | def registration_id_exists(registration_id):
393 | """
394 | Looks up registration based on record_id and pass the record back
395 | """
396 | try:
397 | registration = Interactions.query(
398 | DEFAULT_REGISTRATIONS_TABLE, filters={'id': registration_id})
399 | if registration:
400 | return True
401 | return False
402 | except RqlRuntimeError as runtime_err:
403 | return runtime_err
404 | except RqlDriverError as rql_err:
405 | return rql_err
406 |
--------------------------------------------------------------------------------
/pywebhooks/api/resources/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/api/resources/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/api/resources/v1/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/account/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/api/resources/v1/account/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/account/account_api.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 |
4 | # Third-party imports
5 | from flask import request, jsonify, make_response
6 | from flask_restful import Resource
7 |
8 | # Project-level imports
9 | from pywebhooks import DEFAULT_ACCOUNTS_TABLE
10 | from pywebhooks.api.decorators.authorization import api_key_restricted_resource
11 | from pywebhooks.api.handlers.resources_handler import client_echo_valid, \
12 | insert_account, delete_account, update, query, lookup_account_id, \
13 | validate_access
14 | from pywebhooks.utils.common import generate_key
15 | from pywebhooks.api.decorators.validation import validate_id_params
16 |
17 |
18 | class AccountAPI(Resource):
19 | """
20 | Handles the REST API interaction for accounts
21 | """
22 |
23 | @validate_id_params('account_id')
24 | @api_key_restricted_resource(verify_admin=False)
25 | def get(self, account_id):
26 | """
27 | Gets a user account. Users can only see their own account
28 | """
29 | if lookup_account_id(request.headers['username']) == account_id:
30 | return query(DEFAULT_ACCOUNTS_TABLE, account_id)
31 | else:
32 | return make_response(jsonify(
33 | {'Error': 'Not authorized'}),
34 | client.UNAUTHORIZED)
35 |
36 | @api_key_restricted_resource(verify_admin=False)
37 | def patch(self):
38 | """
39 | Updates account. Only one field can be updated: endpoint
40 | Updating the endpoint also resets the failed_count
41 | """
42 | json_data = request.get_json()
43 | username = request.headers['username']
44 |
45 | if 'endpoint' in json_data:
46 | update_json = {
47 | 'endpoint': json_data['endpoint'],
48 | 'failed_count': 0
49 | }
50 | else:
51 | return make_response(jsonify(
52 | {'Error': 'Missing endpoint field'}), client.BAD_REQUEST)
53 |
54 | return update(DEFAULT_ACCOUNTS_TABLE, username=username,
55 | updates=update_json)
56 |
57 | def post(self):
58 | """
59 | Creates a new account
60 | """
61 | json_data = request.get_json()
62 |
63 | if not client_echo_valid(json_data['endpoint']):
64 | return make_response(jsonify({'Error': 'Echo response failed'}),
65 | client.BAD_REQUEST)
66 |
67 | return insert_account(DEFAULT_ACCOUNTS_TABLE,
68 | **{'username': json_data['username'],
69 | 'endpoint': json_data['endpoint'],
70 | 'is_admin': False,
71 | 'failed_count': 0,
72 | 'api_key': generate_key(),
73 | 'secret_key': generate_key()})
74 |
75 | @validate_id_params('account_id')
76 | @api_key_restricted_resource(verify_admin=False)
77 | def delete(self, account_id):
78 | """
79 | Deletes account record
80 | """
81 | return_val = validate_access(
82 | request.headers['username'],
83 | incoming_account_id=account_id)
84 |
85 | if return_val:
86 | return return_val
87 |
88 | return delete_account(account_id)
89 |
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/account/accounts_api.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | # None
3 | # Third-party imports
4 | from flask import request
5 | from flask_restful import Resource
6 |
7 | # Project-level imports
8 | from pywebhooks import DEFAULT_ACCOUNTS_TABLE
9 | from pywebhooks.api.decorators.authorization import api_key_restricted_resource
10 | from pywebhooks.api.handlers.pagination_handler import paginate
11 | from pywebhooks.api.handlers.resources_handler import \
12 | delete_accounts_except_admins
13 | from pywebhooks.api.decorators.validation import validate_pagination_params
14 |
15 |
16 | class AccountsAPI(Resource):
17 | """
18 | Handles the REST API interaction for accounts
19 | """
20 |
21 | @api_key_restricted_resource(verify_admin=True)
22 | @validate_pagination_params()
23 | def get(self):
24 | """
25 | Get a listing of accounts (paginated if need be)
26 | """
27 | return paginate(request, DEFAULT_ACCOUNTS_TABLE, 'accounts')
28 |
29 | @api_key_restricted_resource(verify_admin=True)
30 | def delete(self):
31 | """
32 | Deletes all records (except admin) in the Accounts table
33 | """
34 | return delete_accounts_except_admins()
35 |
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/account/reset/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/api/resources/v1/account/reset/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/account/reset/api_key_api.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | # None
3 |
4 | # Third-party imports
5 | from flask import request
6 | from flask_restful import Resource
7 |
8 | # Project-level imports
9 | from pywebhooks.api.handlers.resources_handler import reset_key
10 | from pywebhooks.api.decorators.validation import validate_username_in_header
11 |
12 |
13 | class ApiKeyAPI(Resource):
14 | """
15 | Handles the REST API interaction for resetting api keys
16 | """
17 |
18 | @validate_username_in_header()
19 | def post(self):
20 | """
21 | Resets an api key
22 | """
23 | return reset_key(request.headers['username'], 'api_key')
24 |
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/account/reset/secret_key_api.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | # None
3 |
4 | # Third-party imports
5 | from flask import request
6 | from flask_restful import Resource
7 |
8 | # Project-level imports
9 | from pywebhooks.api.handlers.resources_handler import reset_key
10 | from pywebhooks.api.decorators.authorization import api_key_restricted_resource
11 |
12 |
13 | class SecretKeyAPI(Resource):
14 | """
15 | Handles the REST API interaction for resetting secret keys
16 | """
17 |
18 | @api_key_restricted_resource()
19 | def post(self):
20 | """
21 | Resets a secret key
22 | """
23 | return reset_key(request.headers['username'], 'secret_key')
24 |
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/webhook/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/api/resources/v1/webhook/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/webhook/registration_api.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 |
4 | # Third-party imports
5 | from flask import request, jsonify, make_response
6 | from flask_restful import Resource
7 |
8 | # Project-level imports
9 | from pywebhooks import DEFAULT_REGISTRATIONS_TABLE
10 | from pywebhooks.api.handlers.pagination_handler import paginate
11 | from pywebhooks.api.handlers.resources_handler import lookup_account_id, \
12 | validate_access, delete_registration, insert, update
13 | from pywebhooks.api.decorators.authorization import api_key_restricted_resource
14 | from pywebhooks.api.decorators.validation import validate_pagination_params, \
15 | validate_id_params
16 |
17 |
18 | class RegistrationAPI(Resource):
19 |
20 | @api_key_restricted_resource(verify_admin=False)
21 | @validate_pagination_params()
22 | def get(self):
23 | """
24 | Get the user's registered webhooks
25 | """
26 | account_id = lookup_account_id(request.headers['username'])
27 |
28 | return paginate(request, DEFAULT_REGISTRATIONS_TABLE, 'registrations',
29 | filters={'account_id': account_id})
30 |
31 | @validate_id_params('registration_id')
32 | @api_key_restricted_resource(verify_admin=False)
33 | def patch(self, registration_id):
34 | """
35 | Updates registration. Only one field can be updated: description
36 | """
37 | return_val = validate_access(
38 | request.headers['username'],
39 | registration_id=registration_id)
40 |
41 | if return_val:
42 | return return_val
43 |
44 | json_data = request.get_json()
45 | update_json = {}
46 |
47 | if 'description' in json_data:
48 | update_json['description'] = json_data['description']
49 | else:
50 | return make_response(
51 | jsonify({'Error': 'Description field missing'}),
52 | client.BAD_REQUEST)
53 |
54 | return update(DEFAULT_REGISTRATIONS_TABLE,
55 | record_id=registration_id,
56 | updates=update_json)
57 |
58 | @api_key_restricted_resource(verify_admin=False)
59 | def post(self):
60 | """
61 | Creates a new registration
62 | """
63 | json_data = request.get_json()
64 |
65 | # Look up account id based on username, username will be valid since
66 | # the api_key_restricted_resource decorator runs first
67 | account_id = lookup_account_id(request.headers['username'])
68 |
69 | return insert(DEFAULT_REGISTRATIONS_TABLE,
70 | **{'account_id': account_id,
71 | 'event': json_data['event'],
72 | 'description': json_data['description'],
73 | 'event_data': json_data['event_data']})
74 |
75 | @validate_id_params('registration_id')
76 | @api_key_restricted_resource(verify_admin=False)
77 | def delete(self, registration_id):
78 | """
79 | Deletes registration record, will also remove the records for this
80 | registration_id in the subscription table as well
81 | """
82 | return_val = validate_access(
83 | request.headers['username'],
84 | registration_id=registration_id)
85 |
86 | if return_val:
87 | return return_val
88 |
89 | return delete_registration(registration_id)
90 |
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/webhook/registrations_api.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | # None
3 |
4 | # Third-party imports
5 | from flask import request
6 | from flask_restful import Resource
7 |
8 | # Project-level imports
9 | from pywebhooks import DEFAULT_REGISTRATIONS_TABLE
10 | from pywebhooks.api.decorators.authorization import api_key_restricted_resource
11 | from pywebhooks.api.handlers.pagination_handler import paginate
12 | from pywebhooks.api.handlers.resources_handler import delete_all
13 | from pywebhooks.api.decorators.validation import validate_pagination_params
14 |
15 |
16 | class RegistrationsAPI(Resource):
17 | """
18 | Handles the REST API interaction for Registrations
19 | """
20 |
21 | @api_key_restricted_resource(verify_admin=False)
22 | @validate_pagination_params()
23 | def get(self):
24 | """
25 | Get a listing of Registrations (paginated if need be)
26 | """
27 | return paginate(request, DEFAULT_REGISTRATIONS_TABLE, 'registrations')
28 |
29 | @api_key_restricted_resource(verify_admin=True)
30 | def delete(self):
31 | """
32 | Deletes all records in the Registrations table
33 | """
34 | return delete_all(DEFAULT_REGISTRATIONS_TABLE)
35 |
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/webhook/subscription.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 |
4 | # Third-party imports
5 | from flask import request, jsonify, make_response
6 | from flask_restful import Resource
7 |
8 | # Project-level imports
9 | from pywebhooks import DEFAULT_SUBSCRIPTIONS_TABLE
10 | from pywebhooks.api.handlers.pagination_handler import paginate
11 | from pywebhooks.api.handlers.resources_handler import insert, delete, \
12 | registration_id_exists, lookup_account_id, validate_access
13 | from pywebhooks.api.decorators.authorization import api_key_restricted_resource
14 | from pywebhooks.api.decorators.validation import validate_pagination_params, \
15 | validate_id_params
16 |
17 |
18 | class Subscription(Resource):
19 | """
20 | Handles the (webhook) subscriptions table interactions
21 | """
22 |
23 | @api_key_restricted_resource(verify_admin=False)
24 | @validate_pagination_params()
25 | def get(self):
26 | """
27 | Get the user's webhook subscriptions
28 | """
29 | try:
30 | account_id = lookup_account_id(request.headers['username'])
31 | # pylint: disable=W0703
32 | except Exception:
33 | return make_response(
34 | jsonify({'Error': 'Invalid username or account'}),
35 | client.BAD_REQUEST)
36 |
37 | return paginate(request, DEFAULT_SUBSCRIPTIONS_TABLE, 'subscriptions',
38 | filters={'account_id': account_id})
39 |
40 | @validate_id_params('subscription_id')
41 | @api_key_restricted_resource(verify_admin=False)
42 | def post(self, subscription_id):
43 | """
44 | Creates new subscription
45 | """
46 | # subscription_id is actually the registration_id
47 | registration_id = subscription_id
48 |
49 | account_id = lookup_account_id(request.headers['username'])
50 |
51 | if not registration_id_exists(registration_id):
52 | return make_response(
53 | jsonify({'Error': 'The registration id does not exist'}),
54 | client.NOT_FOUND)
55 |
56 | return insert(DEFAULT_SUBSCRIPTIONS_TABLE,
57 | **{'account_id': account_id,
58 | 'registration_id': registration_id})
59 |
60 | @validate_id_params('subscription_id')
61 | @api_key_restricted_resource(verify_admin=False)
62 | def delete(self, subscription_id):
63 | """
64 | Deletes subscription record
65 | """
66 | return_val = validate_access(
67 | request.headers['username'],
68 | subscription_id=subscription_id)
69 |
70 | if return_val:
71 | return return_val
72 |
73 | return delete(DEFAULT_SUBSCRIPTIONS_TABLE, subscription_id)
74 |
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/webhook/subscriptions.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 |
3 | # Third-party imports
4 | from flask import request
5 | from flask_restful import Resource
6 |
7 | # Project-level imports
8 | from pywebhooks import DEFAULT_SUBSCRIPTIONS_TABLE
9 | from pywebhooks.api.decorators.authorization import api_key_restricted_resource
10 | from pywebhooks.api.handlers.resources_handler import delete_all
11 | from pywebhooks.api.handlers.pagination_handler import paginate
12 | from pywebhooks.api.decorators.validation import validate_pagination_params
13 |
14 |
15 | class Subscriptions(Resource):
16 | """
17 | Handles the (webhook) subscriptions table interactions
18 | """
19 |
20 | @validate_pagination_params()
21 | @api_key_restricted_resource(verify_admin=False)
22 | def get(self):
23 | """
24 | Get a listing of subscriptions (paginated if need be)
25 | """
26 | return paginate(request, DEFAULT_SUBSCRIPTIONS_TABLE, 'subscriptions')
27 |
28 | @api_key_restricted_resource(verify_admin=True)
29 | def delete(self):
30 | """
31 | Deletes all records in the subscriptions table
32 | """
33 | return delete_all(DEFAULT_SUBSCRIPTIONS_TABLE)
34 |
--------------------------------------------------------------------------------
/pywebhooks/api/resources/v1/webhook/triggered_api.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 |
4 | # Third-party imports
5 | from flask import request, jsonify, make_response
6 | from flask_restful import Resource
7 |
8 | # Project-level imports
9 | from pywebhooks import DEFAULT_TRIGGERED_TABLE, DEFAULT_REGISTRATIONS_TABLE, \
10 | DEFAULT_SUBSCRIPTIONS_TABLE, DEFAULT_ACCOUNTS_TABLE, MAX_FAILED_COUNT
11 | from pywebhooks.api.decorators.authorization import api_key_restricted_resource
12 | from pywebhooks.api.decorators.validation import validate_pagination_params, \
13 | validate_id_params
14 | from pywebhooks.api.handlers.resources_handler import insert, \
15 | lookup_account_id, lookup_registration_id
16 | from pywebhooks.api.handlers.pagination_handler import paginate
17 | from pywebhooks.database.rethinkdb.interactions import Interactions
18 |
19 |
20 | class TriggeredAPI(Resource):
21 |
22 | @validate_pagination_params()
23 | @api_key_restricted_resource(verify_admin=False)
24 | def get(self):
25 | """
26 | Get a listing of triggered webhooks (paginated if need be)
27 | """
28 | return paginate(request, DEFAULT_TRIGGERED_TABLE, 'triggered_webhooks')
29 |
30 | @validate_id_params('registration_id')
31 | @api_key_restricted_resource(verify_admin=False)
32 | def post(self, registration_id):
33 | """
34 | Creates new triggered webhook event
35 | """
36 | registration = Interactions.query(
37 | DEFAULT_REGISTRATIONS_TABLE, filters={'id': registration_id})
38 |
39 | if not registration:
40 | return make_response(
41 | jsonify(
42 | {'Error': 'Registration id not found'}
43 | ), client.NOT_FOUND)
44 |
45 | # Other users cannot trigger webhooks they didn't create
46 | calling_account_id = lookup_account_id(request.headers['username'])
47 |
48 | if not lookup_registration_id(calling_account_id, registration_id):
49 | return make_response(
50 | jsonify({'Error': 'You don\'t have access '
51 | 'to this registration record or it no '
52 | 'longer exists'}),
53 | client.UNAUTHORIZED)
54 |
55 | # Notify subscribed endpoints (send the webhooks out)
56 | subscriptions = Interactions.list_all(
57 | DEFAULT_SUBSCRIPTIONS_TABLE, order_by='epoch',
58 | filters={'registration_id': registration_id})
59 |
60 | if subscriptions:
61 | for record in subscriptions:
62 | account = Interactions.get(DEFAULT_ACCOUNTS_TABLE,
63 | record['account_id'])
64 | # Only hit the endpoint if their failed count is low enough
65 | if int(account['failed_count']) < MAX_FAILED_COUNT:
66 | # This import is required to be here so the flask-restful
67 | # piece works properly with Celery
68 | from pywebhooks.tasks.webhook_notification import \
69 | notify_subscribed_accounts
70 |
71 | notify_subscribed_accounts.delay(
72 | event=registration[0]['event'],
73 | event_data=registration[0]['event_data'],
74 | secret_key=account['secret_key'],
75 | endpoint=account['endpoint'],
76 | account_id=record['account_id'])
77 |
78 | return insert(DEFAULT_TRIGGERED_TABLE,
79 | **{'registration_id': registration_id})
80 |
--------------------------------------------------------------------------------
/pywebhooks/app.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | import argparse
3 | import sys
4 |
5 | # Third-party imports
6 | from celery import Celery
7 | from flask import Flask, request
8 | from flask_restful import Api
9 |
10 | # Project-level imports
11 | from pywebhooks import CELERY_BROKER_URL
12 | from pywebhooks.api.resources.v1.account.account_api import AccountAPI
13 | from pywebhooks.api.resources.v1.account.accounts_api import AccountsAPI
14 | from pywebhooks.api.resources.v1.webhook.registration_api import \
15 | RegistrationAPI
16 | from pywebhooks.api.resources.v1.webhook.registrations_api import \
17 | RegistrationsAPI
18 | from pywebhooks.api.resources.v1.account.reset.secret_key_api import \
19 | SecretKeyAPI
20 | from pywebhooks.api.resources.v1.account.reset.api_key_api import ApiKeyAPI
21 | from pywebhooks.api.resources.v1.webhook.subscription import Subscription
22 | from pywebhooks.api.resources.v1.webhook.subscriptions import Subscriptions
23 | from pywebhooks.api.resources.v1.webhook.triggered_api import TriggeredAPI
24 | from pywebhooks.database.rethinkdb.initialize import create_database
25 | from pywebhooks.database.rethinkdb.drop import drop_database
26 | from pywebhooks.database.rethinkdb.bootstrap_admin import create_admin_account
27 |
28 |
29 | def create_wsgi_app():
30 | flask_app = Flask(__name__)
31 | flask_app.url_map.strict_slashes = False
32 | api = Api(flask_app)
33 |
34 | api.add_resource(AccountsAPI, '/v1/accounts')
35 | api.add_resource(AccountAPI, '/v1/account/', '/v1/account')
36 |
37 | api.add_resource(SecretKeyAPI, '/v1/account/reset/secretkey')
38 |
39 | api.add_resource(ApiKeyAPI, '/v1/account/reset/apikey')
40 |
41 | api.add_resource(RegistrationAPI, '/v1/webhook/registration',
42 | '/v1/webhook/registration/')
43 | api.add_resource(RegistrationsAPI, '/v1/webhook/registrations')
44 |
45 | api.add_resource(TriggeredAPI, '/v1/webhook/triggered',
46 | '/v1/webhook/triggered/')
47 |
48 | api.add_resource(Subscriptions, '/v1/webhook/subscriptions')
49 | api.add_resource(Subscription, '/v1/webhook/subscription',
50 | '/v1/webhook/subscription/')
51 |
52 | # There is no need for rate limits so it can be turned off
53 | flask_app.config['CELERY_DISABLE_RATE_LIMITS'] = True
54 | CELERY.conf.update(flask_app.config)
55 |
56 | return flask_app
57 |
58 |
59 | CELERY = Celery(__name__, broker=CELERY_BROKER_URL)
60 | CELERY.conf.update(CELERY_ACCEPT_CONTENT=['json'])
61 | app = create_wsgi_app()
62 |
63 |
64 | @app.before_request
65 | def before_request():
66 | if request.headers['content-type'].lower().find('application/json'):
67 | return 'Unsupported Media Type', 415
68 |
69 |
70 | def main(arguments=None): # pragma: no cover
71 | parser = argparse.ArgumentParser(description='Run the PyWebHooks app')
72 | parser.add_argument('--initdb', dest='initdb', action='store_true')
73 | args = parser.parse_args(arguments)
74 |
75 | if args.initdb:
76 | print('Dropping database...')
77 | try:
78 | drop_database()
79 | except Exception as ex:
80 | print(str(ex))
81 | print('Creating database...')
82 | create_database()
83 | print('Adding admin account')
84 | print(create_admin_account())
85 | print('Complete')
86 | else:
87 | app.run(debug=True, port=8081, host='0.0.0.0')
88 |
89 |
90 | if __name__ == "__main__": # pragma: no cover
91 | main(sys.argv[1:])
92 |
--------------------------------------------------------------------------------
/pywebhooks/database/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/database/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/database/rethinkdb/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/database/rethinkdb/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/database/rethinkdb/bootstrap_admin.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | # None
3 |
4 | # Third-party imports
5 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
6 | from werkzeug.security import generate_password_hash
7 |
8 | # Project-level imports
9 | from pywebhooks import DEFAULT_ACCOUNTS_TABLE
10 | from pywebhooks.database.rethinkdb.interactions import Interactions
11 | from pywebhooks.utils.common import generate_key
12 |
13 |
14 | def create_admin_account():
15 | """
16 | Creates a new admin account
17 | """
18 | try:
19 | original_api_key = generate_key()
20 | secret_key = generate_key()
21 | hashed_api_key = generate_password_hash(original_api_key)
22 |
23 | Interactions.insert(DEFAULT_ACCOUNTS_TABLE,
24 | **{'username': 'admin',
25 | 'endpoint': '',
26 | 'is_admin': True,
27 | 'api_key': hashed_api_key,
28 | 'secret_key': secret_key})
29 |
30 | return {'api_key': original_api_key, 'secret_key': secret_key}
31 | except (RqlRuntimeError, RqlDriverError) as err:
32 | raise err
33 |
--------------------------------------------------------------------------------
/pywebhooks/database/rethinkdb/drop.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | # None
3 |
4 | # Third-party imports
5 | import rethinkdb as rethink
6 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
7 |
8 | # Project-level imports
9 | from pywebhooks import DEFAULT_DB_NAME
10 | from pywebhooks.utils.rethinkdb_helper import get_connection
11 |
12 |
13 | def drop_database():
14 | """
15 | Deletes the RethinkDB database
16 | """
17 | try:
18 | with get_connection() as conn:
19 | rethink.db_drop(DEFAULT_DB_NAME).run(conn)
20 | except (RqlRuntimeError, RqlDriverError) as err:
21 | raise err
22 |
--------------------------------------------------------------------------------
/pywebhooks/database/rethinkdb/initialize.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | # None
3 |
4 | # Third-party imports
5 | import rethinkdb as rethink
6 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
7 |
8 | # Project-level imports
9 | from pywebhooks import DEFAULT_DB_NAME, DEFAULT_TABLE_NAMES
10 | from pywebhooks.utils.rethinkdb_helper import get_connection
11 |
12 |
13 | def create_database():
14 | """
15 | Creates a new RethinkDB database with tables if it doesn't already exist
16 | """
17 | try:
18 | with get_connection() as conn:
19 | db_list = rethink.db_list().run(conn)
20 |
21 | if DEFAULT_DB_NAME not in db_list:
22 | # Default db doesn't exist so add it
23 | rethink.db_create(DEFAULT_DB_NAME).run(conn)
24 |
25 | table_list = rethink.db(DEFAULT_DB_NAME).table_list().run(conn)
26 |
27 | for table_name in DEFAULT_TABLE_NAMES:
28 | if table_name not in table_list:
29 | # Add the missing table(s)
30 | rethink.db(DEFAULT_DB_NAME).table_create(table_name)\
31 | .run(conn)
32 |
33 | except (RqlRuntimeError, RqlDriverError) as err:
34 | raise err
35 |
--------------------------------------------------------------------------------
/pywebhooks/database/rethinkdb/interactions.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from time import time
3 | import uuid
4 |
5 | # Third-party imports
6 | import rethinkdb as rethink
7 |
8 | # Project-level imports
9 | from pywebhooks.utils.rethinkdb_helper import get_connection
10 |
11 |
12 | class Interactions(object):
13 | """
14 | Handles basic table interactions
15 | """
16 |
17 | @staticmethod
18 | def list(table_name, start, limit, order_by='epoch', filters=None):
19 | """
20 | Gets a list of records, meant for pagination.
21 | """
22 | if not filters:
23 | filters = {}
24 |
25 | with get_connection() as conn:
26 | return rethink.table(table_name)\
27 | .filter(filters).order_by(order_by) \
28 | .slice(start, limit).run(conn)
29 |
30 | @staticmethod
31 | def list_all(table_name, order_by='epoch', filters=None):
32 | """
33 | Gets a full list of records - no pagination.
34 | """
35 | if not filters:
36 | filters = {}
37 |
38 | with get_connection() as conn:
39 | return list(rethink.table(table_name)
40 | .order_by(order_by).filter(filters).run(conn))
41 |
42 | @staticmethod
43 | def query(table_name, order_by='epoch', filters=None):
44 | """
45 | Query for record(s)
46 | """
47 | if not filters:
48 | filters = {}
49 |
50 | with get_connection() as conn:
51 | return rethink.table(table_name)\
52 | .order_by(order_by).filter(filters).run(conn)
53 |
54 | @staticmethod
55 | def get(table_name, record_id):
56 | """
57 | Get a single record based on id.
58 | """
59 | with get_connection() as conn:
60 | return rethink.table(table_name).get(record_id).run(conn)
61 |
62 | @staticmethod
63 | def insert(table_name, **kwargs):
64 | """
65 | Inserts a new record. id and epoch are common to all records.
66 | """
67 | if 'id' not in kwargs:
68 | kwargs['id'] = str(uuid.uuid4())
69 |
70 | kwargs['epoch'] = time()
71 |
72 | with get_connection() as conn:
73 | rethink.table(table_name).insert(kwargs).run(conn)
74 | return kwargs
75 |
76 | @staticmethod
77 | def delete_all(table_name):
78 | """
79 | Deletes all records in a specified table
80 | """
81 | with get_connection() as conn:
82 | return rethink.table(table_name).delete().run(conn)
83 |
84 | @staticmethod
85 | def delete(table_name, record_id):
86 | """
87 | Deletes a single record in a specified table
88 | """
89 | with get_connection() as conn:
90 | return rethink.table(table_name).get(record_id).delete().run(conn)
91 |
92 | @staticmethod
93 | def delete_specific(table_name, filters=None):
94 | """
95 | Deletes all records in a table matching the filter
96 | """
97 | if not filters:
98 | filters = {}
99 |
100 | with get_connection() as conn:
101 | return rethink.table(table_name).filter(filters).delete().run(conn)
102 |
103 | @staticmethod
104 | def update(table_name, record_id=None, filters=None, updates=None):
105 | """
106 | Perform an update on one more fields
107 | """
108 | if not filters:
109 | filters = {}
110 | if not updates:
111 | updates = {}
112 |
113 | with get_connection() as conn:
114 | if record_id:
115 | return rethink.table(table_name).get(record_id)\
116 | .update(updates).run(conn)
117 | else:
118 | return rethink.table(table_name).filter(filters)\
119 | .update(updates).run(conn)
120 |
--------------------------------------------------------------------------------
/pywebhooks/examples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/examples/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/examples/endpoint_development_server.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import hmac
3 | from http import client
4 | import json
5 |
6 | from flask import Flask
7 | from flask import request, make_response, jsonify
8 |
9 |
10 | app = Flask(__name__)
11 |
12 | # Adjust this as needed
13 | SECRET_KEY = 'c27e823b0a500a537990dcccfc50334fe814fbd2'
14 |
15 |
16 | def verify_hmac_hash(incoming_json, secret_key, incoming_signature):
17 | signature = hmac.new(
18 | str(secret_key).encode('utf-8'),
19 | str(incoming_json).encode('utf-8'),
20 | digestmod=hashlib.sha1
21 | ).hexdigest()
22 |
23 | return hmac.compare_digest(signature, incoming_signature)
24 |
25 |
26 | def create_response(req):
27 | if request.args.get('echo'):
28 | return make_response(jsonify({'echo': req.args.get('echo')}),
29 | client.OK)
30 | if request.args.get('api_key'):
31 | print('New api_key: {0}'.format(req.args.get('api_key')))
32 | return make_response(jsonify({}), client.OK)
33 | if request.args.get('secret_key'):
34 | print('New secret_key: {0}'.format(req.args.get('secret_key')))
35 | return make_response(jsonify({}), client.OK)
36 |
37 |
38 | def webhook_listener(request):
39 | print(request.headers)
40 | print(request.data)
41 | print(json.dumps(request.json))
42 |
43 | is_signature_valid = verify_hmac_hash(
44 | json.dumps(request.json),
45 | SECRET_KEY,
46 | request.headers['pywebhooks-signature']
47 | )
48 |
49 | print('Is Signature Valid?: {0}'.format(is_signature_valid))
50 |
51 | return make_response(jsonify({}), client.OK)
52 |
53 |
54 | @app.route('/account/endpoint', methods=['GET'])
55 | def echo():
56 | return create_response(request)
57 |
58 |
59 | @app.route('/account/alternate/endpoint', methods=['GET'])
60 | def echo_alternate():
61 | return create_response(request)
62 |
63 |
64 | @app.route('/account/alternate/endpoint', methods=['POST'])
65 | def account_alternate_listener():
66 | return webhook_listener(request)
67 |
68 |
69 | @app.route('/account/endpoint', methods=['POST'])
70 | def account_listener():
71 | return webhook_listener(request)
72 |
73 |
74 | if __name__ == '__main__':
75 | app.run(debug=True, port=9090, host='0.0.0.0')
76 |
--------------------------------------------------------------------------------
/pywebhooks/examples/ruby_endpoint_developement_server.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'openssl'
3 | require 'sinatra'
4 | require 'json'
5 |
6 |
7 | SHARED_SECRET = 'c27e823b0a500a537990dcccfc50334fe814fbd2'
8 |
9 | # Handle echo requests
10 | get '/account/endpoint' do
11 | content_type :json
12 | echo_value = params['echo']
13 | puts 'echo value:'
14 | puts(echo_value)
15 |
16 | status 200
17 | { :echo => echo_value }.to_json
18 | end
19 |
20 | # Handle the incoming webhook events
21 | post '/account/endpoint' do
22 | request.body.rewind
23 | data = request.body.read
24 | HMAC_DIGEST = OpenSSL::Digest.new('sha1')
25 | signature = OpenSSL::HMAC.hexdigest(HMAC_DIGEST, SHARED_SECRET, data)
26 | incoming_signature = env['HTTP_PYWEBHOOKS_SIGNATURE']
27 |
28 | puts 'hmac verification results:'
29 | puts Rack::Utils.secure_compare(signature, incoming_signature)
30 |
31 | incoming_event = env['HTTP_EVENT']
32 | puts 'incoming event is:'
33 | puts incoming_event
34 | puts 'incoming json is:'
35 | puts data
36 |
37 | status 200
38 | '{}'
39 | end
40 |
--------------------------------------------------------------------------------
/pywebhooks/tasks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/tasks/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/tasks/webhook_notification.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | import logging
4 |
5 | # Third-party imports
6 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
7 |
8 | # Project-level imports
9 | from pywebhooks.app import CELERY
10 | from pywebhooks import DEFAULT_RETRY, DEFAULT_FINAL_RETRY, REQUEST_TIMEOUT, \
11 | DEFAULT_ACCOUNTS_TABLE
12 | from pywebhooks.database.rethinkdb.interactions import Interactions
13 | from pywebhooks.utils.common import create_signature
14 | from pywebhooks.utils.request_handler import RequestHandler
15 |
16 |
17 | _LOG = logging.getLogger(__name__)
18 |
19 |
20 | def update_failed_count(account_id=None, increment_failed_count=False):
21 | """
22 | Update the failed_count field on the user's account
23 | """
24 | try:
25 | # Get the failed_count value
26 | account_record = Interactions.get(
27 | DEFAULT_ACCOUNTS_TABLE, record_id=account_id)
28 | failed_count = int(account_record['failed_count'])
29 |
30 | if increment_failed_count:
31 | failed_count += 1
32 | else:
33 | failed_count = 0
34 |
35 | Interactions.update(DEFAULT_ACCOUNTS_TABLE, record_id=account_id,
36 | updates={'failed_count': failed_count})
37 | except (RqlRuntimeError, RqlDriverError, Exception):
38 | pass
39 |
40 |
41 | # Running the Worker from the project root:
42 | #
43 | # celery -A pywebhooks.tasks.webhook_notification worker --loglevel=info
44 | #
45 | @CELERY.task(bind=True, serializer='json', max_retries=3, ignore_result=True)
46 | def notify_subscribed_accounts(self, event=None, event_data=None,
47 | secret_key=None, endpoint=None,
48 | account_id=None):
49 | """
50 | Send Webhook requests to all subscribed accounts
51 | """
52 | signature = create_signature(secret_key, event_data)
53 |
54 | request_handler = RequestHandler(
55 | verify_ssl=False, request_timeout=REQUEST_TIMEOUT)
56 |
57 | try:
58 | _, status_code = request_handler.post(
59 | url=endpoint,
60 | json_payload=event_data, event=event,
61 | signature=signature)
62 |
63 | # We don't care about anything but the return status code
64 | if client.OK != status_code:
65 | raise Exception('Endpoint returning non HTTP 200 status. '
66 | 'Actual code returned: {0}'.format(status_code))
67 |
68 | if client.OK == status_code:
69 | # Failed count will reset on a good contact
70 | update_failed_count(account_id, increment_failed_count=False)
71 |
72 | except Exception as exc:
73 | update_failed_count(account_id, increment_failed_count=True)
74 |
75 | if self.request.retries == 3:
76 | raise self.retry(exc=exc, countdown=DEFAULT_FINAL_RETRY)
77 | else:
78 | raise self.retry(exc=exc, countdown=DEFAULT_RETRY)
79 |
--------------------------------------------------------------------------------
/pywebhooks/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/pywebhooks/utils/__init__.py
--------------------------------------------------------------------------------
/pywebhooks/utils/common.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | import json
3 | import hashlib
4 | import hmac
5 | import os
6 |
7 | # Third-party imports
8 | # None
9 |
10 | # Project-level imports
11 | # None
12 |
13 |
14 | def generate_key():
15 | return str(hashlib.sha1(os.urandom(128)).hexdigest())
16 |
17 |
18 | def create_signature(secret_key, json_data, digestmod=hashlib.sha1):
19 | return hmac.new(
20 | str(secret_key).encode('utf-8'),
21 | str(json.dumps(json_data)).encode('utf-8'),
22 | digestmod=digestmod
23 | ).hexdigest()
24 |
--------------------------------------------------------------------------------
/pywebhooks/utils/request_handler.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | import json
3 |
4 | # Third party imports
5 | import requests
6 |
7 | # Project level imports
8 | # None
9 |
10 |
11 | # Suppress the insecure request warning
12 | # https://urllib3.readthedocs.org/en/
13 | # latest/security.html#insecurerequestwarning
14 | requests.packages.urllib3.disable_warnings()
15 |
16 |
17 | class RequestHandler(object):
18 |
19 | def __init__(self, verify_ssl=False, request_timeout=15.0):
20 |
21 | self.verify_ssl = verify_ssl
22 | self.request_timeout = request_timeout
23 | self._session = requests.Session()
24 | self.headers = {'Accept': 'application/json',
25 | 'Content-Type': 'application/json'}
26 |
27 | def get(self, url, params=None, api_key=None, username=None):
28 | return self._request(url, params=params, api_key=api_key,
29 | username=username)
30 |
31 | def post(self, url, json_payload='{}', api_key=None, username=None,
32 | event=None, signature=None):
33 | return self._request(url, json_payload, http_verb='POST',
34 | api_key=api_key, username=username,
35 | event=event, signature=signature)
36 |
37 | def patch(self, url, json_payload='{}', api_key=None, username=None):
38 | return self._request(url, json_payload, http_verb='PATCH',
39 | api_key=api_key, username=username)
40 |
41 | def put(self, url, json_payload='{}', api_key=None, username=None):
42 | return self._request(url, json_payload, http_verb='PUT',
43 | api_key=api_key, username=username)
44 |
45 | def delete(self, url, params=None, api_key=None, username=None):
46 | return self._request(url, params=params, http_verb='DELETE',
47 | api_key=api_key, username=username)
48 |
49 | def _request(self, url, json_payload='{}', http_verb='GET', params=None,
50 | api_key=None, username=None, event=None, signature=None):
51 |
52 | json_payload = json.dumps(json_payload)
53 |
54 | if api_key:
55 | self.headers['api-key'] = api_key
56 | if username:
57 | self.headers['username'] = username
58 | if event:
59 | self.headers['event'] = event
60 | if signature:
61 | self.headers['pywebhooks-signature'] = signature
62 |
63 | if http_verb == "PUT":
64 | req = self._session.put(
65 | url=url,
66 | verify=self.verify_ssl,
67 | headers=self.headers,
68 | timeout=self.request_timeout,
69 | data=json_payload)
70 | elif http_verb == 'POST':
71 | req = self._session.post(
72 | url=url,
73 | verify=self.verify_ssl,
74 | headers=self.headers,
75 | timeout=self.request_timeout,
76 | data=json_payload)
77 | elif http_verb == 'PATCH':
78 | req = self._session.patch(
79 | url=url,
80 | verify=self.verify_ssl,
81 | headers=self.headers,
82 | timeout=self.request_timeout,
83 | data=json_payload)
84 | elif http_verb == 'DELETE':
85 | req = self._session.delete(
86 | url=url,
87 | verify=self.verify_ssl,
88 | headers=self.headers,
89 | timeout=self.request_timeout,
90 | params=params)
91 | else: # Default to GET
92 | req = self._session.get(
93 | url=url,
94 | verify=self.verify_ssl,
95 | headers=self.headers,
96 | timeout=self.request_timeout,
97 | params=params)
98 |
99 | return req.json(), req.status_code
100 |
--------------------------------------------------------------------------------
/pywebhooks/utils/rethinkdb_helper.py:
--------------------------------------------------------------------------------
1 | import rethinkdb as rethink
2 |
3 | from pywebhooks import DEFAULT_DB_NAME, RETHINK_PORT, \
4 | RETHINK_HOST, RETHINK_AUTH_KEY
5 |
6 |
7 | def get_connection():
8 | return rethink.connect(
9 | host=RETHINK_HOST,
10 | port=RETHINK_PORT,
11 | auth_key=RETHINK_AUTH_KEY,
12 | db=DEFAULT_DB_NAME
13 | )
14 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | celery
2 | flask
3 | flask-restful
4 | redis
5 | requests
6 | requests-mock
7 | rethinkdb
8 | werkzeug
9 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [nosetests]
2 | where=tests
3 | nocapture=1
4 | cover-package=pywebhooks
5 | cover-erase=1
6 | [metadata]
7 | description-file=README.rst
8 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | PyWebHooks Setup
3 | """
4 |
5 | try:
6 | from setuptools import setup, find_packages
7 | except ImportError:
8 | from ez_setup import use_setuptools
9 | use_setuptools()
10 | from setuptools import setup, find_packages
11 |
12 |
13 | def read(relative):
14 | """
15 | Read file contents and return a list of lines.
16 | ie, read the VERSION file
17 | """
18 | contents = open(relative, 'r').read()
19 | return [l for l in contents.split('\n') if l != '']
20 |
21 |
22 | with open('README.rst', 'r') as f:
23 | readme = f.read()
24 |
25 | setup(
26 | name='pywebhooks',
27 | url='https://github.com/chadlung/pywebhooks',
28 | keywords=['WebHooks'],
29 | long_description=readme,
30 | version=read('VERSION')[0],
31 | description='WebHooks Service',
32 | author='Chad Lung',
33 | author_email='chad.lung@gmail.com',
34 | tests_require=read('./test-requirements.txt'),
35 | install_requires=read('./requirements.txt'),
36 | test_suite='nose.collector',
37 | zip_safe=False,
38 | include_package_data=True,
39 | classifiers=[
40 | 'Programming Language :: Python :: 3.6',
41 | 'Development Status :: 4 - Beta',
42 | 'Environment :: Web Environment',
43 | 'Intended Audience :: Developers',
44 | 'License :: OSI Approved :: Apache Software License',
45 | 'Operating System :: MacOS :: MacOS X',
46 | 'Operating System :: POSIX :: Linux'
47 | ],
48 | packages=find_packages(exclude=['ez_setup']),
49 | entry_points={
50 | 'console_scripts': [
51 | 'pywebhooks = pywebhooks.app:main'
52 | ]
53 | }
54 | )
55 |
--------------------------------------------------------------------------------
/test-requirements.txt:
--------------------------------------------------------------------------------
1 | coverage
2 | flake8==3.5.0
3 | nose
4 | pep8==1.7.1
5 | pycodestyle==2.3.1
6 | pyflakes==1.6.0
7 | pylint==2.1.1
8 | testtools
9 | tox
10 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/__init__.py
--------------------------------------------------------------------------------
/tests/functional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/functional/__init__.py
--------------------------------------------------------------------------------
/tests/functional/http_interactions.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 |
3 | # Standard lib imports
4 | # None
5 |
6 | # Third-party imports
7 | # None
8 |
9 | # Project-level imports
10 | from pywebhooks.database.rethinkdb.initialize import create_database
11 | from pywebhooks.database.rethinkdb.drop import drop_database
12 | from pywebhooks.database.rethinkdb.bootstrap_admin import create_admin_account
13 | from pywebhooks.utils.request_handler import RequestHandler
14 |
15 |
16 | BASE_URL = 'http://127.0.0.1:8081/v1/{0}'
17 |
18 |
19 | def initdb():
20 | drop_database()
21 | create_database()
22 | return create_admin_account()
23 |
24 |
25 | def create_account_records(request_handler):
26 | url = BASE_URL.format('account')
27 | user_keys = {}
28 |
29 | accounts = [{'username': 'johndoe',
30 | 'endpoint': 'http://127.0.0.1:9090/account/endpoint'},
31 | {'username': 'janedoe',
32 | 'endpoint': 'http://127.0.0.1:9090/account/endpoint'},
33 | {'username': 'samjones',
34 | 'endpoint': 'http://127.0.0.1:9090/account/endpoint'},
35 | {'username': 'leahrichards',
36 | 'endpoint': 'http://127.0.0.1:9090/account/endpoint'}]
37 |
38 | for account in accounts:
39 | data, status_code = request_handler.post(url, account)
40 | # Store the generated api and secret keys
41 | user_keys[account['username']] = {'api_key': data['api_key'],
42 | 'secret_key': data['secret_key'],
43 | 'id': data['id']}
44 | return user_keys
45 |
46 |
47 | def get_accounts_records(request_handler, username=None, api_key=None):
48 | url = BASE_URL.format('accounts')
49 |
50 | data, status_code = request_handler.get(
51 | url, params={'start': 0, 'limit': 20},
52 | api_key=api_key, username=username)
53 | return data, status_code
54 |
55 |
56 | def reset_key(request_handler, username=None, api_key=None, key_type='api_key'):
57 | if key_type == 'api_key':
58 | url = BASE_URL.format('account/reset/apikey')
59 | else:
60 | url = BASE_URL.format('account/reset/secretkey')
61 |
62 | data, status_code = request_handler.post(
63 | url, username=username, api_key=api_key)
64 | return data, status_code
65 |
66 |
67 | def update_account_record(request_handler, username=None, api_key=None,
68 | endpoint=None):
69 | url = BASE_URL.format('account')
70 |
71 | data, status_code = request_handler.patch(
72 | url, json_payload={'endpoint': endpoint},
73 | username=username,
74 | api_key=api_key)
75 | return data, status_code
76 |
77 |
78 | def get_account_record(request_handler, username=None, api_key=None,
79 | account_id=None):
80 | account_url = 'account/{0}'.format(account_id)
81 | url = BASE_URL.format(account_url)
82 |
83 | data, status_code = request_handler.get(url, username=username, api_key=api_key)
84 | return data, status_code
85 |
86 |
87 | def delete_account_record(request_handler, username=None, api_key=None,
88 | account_id=None):
89 | account_url = 'account/{0}'.format(account_id)
90 | url = BASE_URL.format(account_url)
91 |
92 | data, status_code = request_handler.delete(
93 | url, username=username, api_key=api_key)
94 | return data, status_code
95 |
96 |
97 | def delete_accounts_records(request_handler, username=None, api_key=None):
98 | url = BASE_URL.format('accounts')
99 |
100 | data, status_code = request_handler.delete(
101 | url, username=username, api_key=api_key)
102 | return data, status_code
103 |
104 |
105 | def insert_registration_record(request_handler, username=None, api_key=None,
106 | json_payload={}):
107 | url = BASE_URL.format('webhook/registration')
108 |
109 | data, status_code = request_handler.post(
110 | url, username=username, api_key=api_key, json_payload=json_payload)
111 | return data, status_code
112 |
113 |
114 | def get_registration_records(request_handler, username=None, api_key=None):
115 | url = BASE_URL.format('webhook/registration')
116 |
117 | data, status_code = request_handler.get(
118 | url, params={'start': 0, 'limit': 20},
119 | username=username, api_key=api_key)
120 | return data, status_code
121 |
122 |
123 | def get_registrations_records(request_handler, username=None, api_key=None):
124 | url = BASE_URL.format('webhook/registrations')
125 |
126 | data, status_code = request_handler.get(
127 | url, params={'start': 0, 'limit': 20},
128 | username=username, api_key=api_key)
129 | return data, status_code
130 |
131 |
132 | def update_registration_record(request_handler, username=None, api_key=None,
133 | registration_id=None, json_payload={}):
134 | registration_url = 'webhook/registration/{0}'.format(registration_id)
135 | url = BASE_URL.format(registration_url)
136 |
137 | data, status_code = request_handler.patch(
138 | url, json_payload=json_payload, username=username, api_key=api_key)
139 | return data, status_code
140 |
141 |
142 | def delete_registration_record(request_handler, username=None, api_key=None,
143 | registration_id=None):
144 | registration_url = 'webhook/registration/{0}'.format(registration_id)
145 | url = BASE_URL.format(registration_url)
146 |
147 | data, status_code = request_handler.delete(
148 | url, username=username, api_key=api_key)
149 | return data, status_code
150 |
151 |
152 | def delete_registrations_records(request_handler, username=None, api_key=None):
153 | url = BASE_URL.format('webhook/registrations')
154 |
155 | data, status_code = request_handler.delete(
156 | url, username=username, api_key=api_key)
157 | return data, status_code
158 |
159 |
160 | def get_subscription_records(request_handler, username=None, api_key=None):
161 | url = BASE_URL.format('webhook/subscription')
162 |
163 | data, status_code = request_handler.get(
164 | url,params={'start': 0, 'limit': 20},
165 | username=username, api_key=api_key)
166 | return data, status_code
167 |
168 |
169 | def insert_subscription_record(request_handler, username=None, api_key=None,
170 | registration_id=None):
171 | registration_url = 'webhook/subscription/{0}'.format(registration_id)
172 | url = BASE_URL.format(registration_url)
173 |
174 | data, status_code = request_handler.post(
175 | url, username=username, api_key=api_key)
176 | return data, status_code
177 |
178 |
179 | def get_subscriptions_records(request_handler, username=None, api_key=None):
180 | url = BASE_URL.format('webhook/subscriptions')
181 |
182 | try:
183 | data, status_code = request_handler.get(
184 | url,params={'start': 0, 'limit': 20},
185 | username=username, api_key=api_key)
186 | except ValueError:
187 | # No records are left
188 | return None, 204
189 |
190 | return data, status_code
191 |
192 |
193 | def delete_subscriptions_records(request_handler, username=None, api_key=None):
194 | url = BASE_URL.format('webhook/subscriptions')
195 |
196 | data, status_code = request_handler.delete(
197 | url, username=username, api_key=api_key)
198 | return data, status_code
199 |
200 |
201 | def delete_subscription_record(request_handler, username=None, api_key=None,
202 | subscription_id=None):
203 | subscription_url = 'webhook/subscription/{0}'.format(subscription_id)
204 | url = BASE_URL.format(subscription_url)
205 |
206 | data, status_code = request_handler.delete(
207 | url, username=username, api_key=api_key)
208 | return data, status_code
209 |
210 |
211 | def insert_triggered_record(request_handler, username=None, api_key=None,
212 | registration_id=None):
213 | registration_url = 'webhook/triggered/{0}'.format(registration_id)
214 | url = BASE_URL.format(registration_url)
215 |
216 | data, status_code = request_handler.post(
217 | url, username=username, api_key=api_key)
218 | return data, status_code
219 |
220 |
221 | def get_triggered_records(request_handler, username=None, api_key=None):
222 | url = BASE_URL.format('webhook/triggered')
223 |
224 | data, status_code = request_handler.get(url,
225 | params={'start': 0, 'limit': 20},
226 | username=username,
227 | api_key=api_key)
228 | return data, status_code
229 |
230 |
231 | def validate_account_actions(request_handler, users_and_keys=None,
232 | admin_username=None, admin_api_key=None):
233 | john_doe_info = users_and_keys['johndoe']
234 | jane_doe_info = users_and_keys['janedoe']
235 | sam_jones_info = users_and_keys['samjones']
236 |
237 | # Regular (non-admin) users should not be able to list accounts
238 | _, status = get_accounts_records(request_handler, username='johndoe',
239 | api_key=john_doe_info['api_key'])
240 | assert status == 401
241 |
242 | # Reset a secret key
243 | _, status = reset_key(request_handler, username='samjones',
244 | api_key=sam_jones_info['api_key'],
245 | key_type='secret_key')
246 | assert status == 200
247 |
248 | # Reset an API key
249 | _, status = reset_key(request_handler, username='samjones',
250 | key_type='api_key')
251 | assert status == 200
252 |
253 | # Update an endpoint
254 | json_data, status = update_account_record(
255 | request_handler, 'johndoe', api_key=john_doe_info['api_key'],
256 | endpoint='http://127.0.0.1:9090/account/alternate/endpoint')
257 |
258 | assert status == 200
259 | assert json_data['replaced'] == 1
260 |
261 | # Get a single account record (johndoe)
262 | json_data, status = get_account_record(
263 | request_handler, username='johndoe',
264 | api_key=john_doe_info['api_key'],
265 | account_id=john_doe_info['id'])
266 |
267 | assert status == 200
268 | assert json_data['username'] == 'johndoe'
269 |
270 | # Users should not be able to get someone else's account record
271 | json_data, status = get_account_record(
272 | request_handler, username='johndoe',
273 | api_key=john_doe_info['api_key'],
274 | account_id=jane_doe_info['id'])
275 |
276 | assert status == 401
277 |
278 | # User's should not be able to delete another user's account record
279 | _, status = delete_account_record(
280 | request_handler,
281 | username='johndoe',
282 | api_key=john_doe_info['api_key'],
283 | account_id=sam_jones_info['id'])
284 | assert status == 401
285 |
286 | # Admin can delete any account record
287 | json_data, status = delete_account_record(
288 | request_handler,
289 | username=admin_username,
290 | api_key=admin_api_key,
291 | account_id=sam_jones_info['id'])
292 | assert status == 200
293 | assert json_data['deleted'] == 1
294 |
295 |
296 | def validate_misc_actions(request_handler, users_and_keys=None):
297 | sam_jones_info = users_and_keys['samjones']
298 | leah_richards_info = users_and_keys['leahrichards']
299 |
300 | # Insert new leah richards registration record
301 | json_data, status = insert_registration_record(
302 | request_handler, username='leahrichards',
303 | api_key=leah_richards_info['api_key'],
304 | json_payload={'event': 'leahrichards.event',
305 | 'description': 'leah richards registered webhook',
306 | 'event_data': {'message': 'Leah Richards'}})
307 | assert status == 201
308 | leah_richards_registration_id = json_data['id']
309 |
310 | # User's should not be able to update another user's registration
311 | json_data, status = update_registration_record(
312 | request_handler, username='leahrichards',
313 | api_key=sam_jones_info['api_key'],
314 | registration_id=leah_richards_registration_id,
315 | json_payload={'description': 'leah new'})
316 | assert status == 401
317 |
318 | # User's should be able to delete their own account
319 | json_data, status = delete_account_record(
320 | request_handler,
321 | username='samjones',
322 | api_key=sam_jones_info['api_key'],
323 | account_id=sam_jones_info['id'])
324 | assert status == 200
325 | assert json_data['deleted'] == 1
326 |
327 |
328 | def validate_registration_actions(request_handler, users_and_keys=None):
329 | john_doe_info = users_and_keys['johndoe']
330 | jane_doe_info = users_and_keys['janedoe']
331 | leah_richards_info = users_and_keys['leahrichards']
332 |
333 | # Insert new janedoe registration record
334 | json_data, status = insert_registration_record(
335 | request_handler, username='janedoe',
336 | api_key=jane_doe_info['api_key'],
337 | json_payload={'event': 'janedoe.event',
338 | 'description': 'jane doe registered webhook',
339 | 'event_data': {'message': 'Jane Doe'}})
340 |
341 | assert status == 201
342 | jane_doe_registration_id = json_data['id']
343 |
344 | # Get the user's registration record(s)
345 | json_data, status = get_registration_records(request_handler,
346 | 'janedoe',
347 | jane_doe_info['api_key'])
348 |
349 | assert 'registrations' in json_data
350 | assert len(json_data['registrations']) == 1
351 |
352 | # Insert new johndoe registration record
353 | json_data, status = insert_registration_record(
354 | request_handler, username='johndoe',
355 | api_key=john_doe_info['api_key'],
356 | json_payload={'event': 'johndoe.event',
357 | 'description': 'john doe registered webhook',
358 | 'event_data': {'message': 'John Doe'}})
359 |
360 | assert status == 201
361 | john_doe_registration_id = json_data['id']
362 |
363 | # Get the user's registration record(s)
364 | json_data, status = get_registration_records(request_handler,
365 | 'johndoe',
366 | john_doe_info['api_key'])
367 |
368 | assert 'registrations' in json_data
369 | assert len(json_data['registrations']) == 1
370 |
371 | # Insert new leahrichards registration record
372 | json_data, status = insert_registration_record(
373 | request_handler, username='leahrichards',
374 | api_key=leah_richards_info['api_key'],
375 | json_payload={'event': 'leahrichards.event',
376 | 'description': 'leah richards registered webhook',
377 | 'event_data': {'message': 'Leah Richards'}})
378 |
379 | assert status == 201
380 | leah_richards_registration_id = json_data['id']
381 |
382 | # Get all registrations (should be 3 of them)
383 | json_data, status = get_registrations_records(
384 | request_handler, 'leahrichards', leah_richards_info['api_key'])
385 |
386 | assert 'registrations' in json_data
387 | assert len(json_data['registrations']) == 3
388 |
389 | # Update a registration record (description)
390 | json_data, status = update_registration_record(
391 | request_handler, username='leahrichards',
392 | api_key=leah_richards_info['api_key'],
393 | registration_id=leah_richards_registration_id,
394 | json_payload={'description': 'leah new'})
395 |
396 | assert status == 200
397 | assert json_data['replaced'] == 1
398 |
399 | json_data, status = get_registration_records(
400 | request_handler, 'leahrichards', leah_richards_info['api_key'])
401 |
402 | assert len(json_data['registrations']) == 1
403 | assert json_data['registrations'][0]['description'] == 'leah new'
404 |
405 | # Delete registration record
406 | json_data, status = delete_registration_record(
407 | request_handler, username='leahrichards',
408 | api_key=leah_richards_info['api_key'],
409 | registration_id=leah_richards_registration_id)
410 |
411 | assert status == 200
412 | assert json_data['deleted'] == 1
413 |
414 | # Another user should not be able to delete someone else's
415 | # registration record
416 | _, status = delete_registration_record(
417 | request_handler, username='johndoe',
418 | api_key=john_doe_info['api_key'],
419 | registration_id=leah_richards_registration_id)
420 |
421 | assert status == 401
422 |
423 | # Get all registrations (should be 2 of them)
424 | json_data, status = get_registrations_records(
425 | request_handler, 'johndoe', john_doe_info['api_key'])
426 |
427 | assert len(json_data['registrations']) == 2
428 |
429 | return {
430 | 'janedoe': jane_doe_registration_id,
431 | 'johndoe': john_doe_registration_id,
432 | 'leahrichards': leah_richards_registration_id
433 | }
434 |
435 |
436 | def validate_subscription_actions(request_handler, users_and_keys=None,
437 | registration_ids=None,
438 | admin_username=None, admin_api_key=None):
439 | john_doe_info = users_and_keys['johndoe']
440 | jane_doe_info = users_and_keys['janedoe']
441 | leah_richards_info = users_and_keys['leahrichards']
442 |
443 | # You should not be able to create a new subscription with a
444 | # registration id that doesn't exist
445 | json_data, status = insert_subscription_record(
446 | request_handler, username='johndoe',
447 | api_key=john_doe_info['api_key'],
448 | registration_id='01d248ae-babb-4802-8060-47820c3bd018')
449 |
450 | assert status == 404
451 |
452 | # Create a new subscription
453 | json_data, status = insert_subscription_record(
454 | request_handler, username='johndoe',
455 | api_key=john_doe_info['api_key'],
456 | registration_id=registration_ids['janedoe'])
457 |
458 | # John Doe has subscribed to Jane Doe's webhook
459 | assert status == 201
460 | assert json_data['registration_id'] == registration_ids['janedoe']
461 | assert json_data['account_id'] == john_doe_info['id']
462 | john_doe_subscription_id = json_data['id']
463 |
464 | # Another user should not be able to delete someone else's
465 | # registration record. Jane Doe should not be able to delete
466 | # John Doe's subscription
467 | _, status = delete_subscription_record(
468 | request_handler, username='janedoe',
469 | api_key=jane_doe_info['api_key'],
470 | subscription_id=john_doe_subscription_id)
471 |
472 | assert status == 401
473 |
474 | # Insert new leahrichards registration record
475 | json_data, status = insert_registration_record(
476 | request_handler, username='leahrichards',
477 | api_key=leah_richards_info['api_key'],
478 | json_payload={'event': 'leahrichards.event',
479 | 'description': 'leah richards registered webhook',
480 | 'event_data': {'message': 'Leah Richards'}})
481 |
482 | assert status == 201
483 |
484 | # Create a new subscription
485 | json_data, status = insert_subscription_record(
486 | request_handler, username='johndoe',
487 | api_key=john_doe_info['api_key'],
488 | registration_id=registration_ids['janedoe'])
489 |
490 | # John Doe has subscribed to Leah Richard's webhook
491 | assert status == 201
492 | assert json_data['registration_id'] == registration_ids['janedoe']
493 | assert json_data['account_id'] == john_doe_info['id']
494 |
495 | # Get john doe's subscriptions (should be two, leah's and janes')
496 | json_data, status = get_subscription_records(
497 | request_handler, username='johndoe',
498 | api_key=john_doe_info['api_key'])
499 |
500 | assert status == 200
501 | assert 'subscriptions' in json_data
502 | assert len(json_data['subscriptions']) == 2
503 |
504 | # If we delete john doe's account record the subscriptions and
505 | # registrations should be gone as well
506 | # First, get a count of all registrations (should be 3)
507 | json_data, _ = get_registrations_records(
508 | request_handler, username='janedoe',
509 | api_key=jane_doe_info['api_key'])
510 |
511 | assert len(json_data['registrations']) == 3
512 |
513 | # Get a count of all subscription records (should be 2)
514 | json_data, status = get_subscriptions_records(
515 | request_handler, username='janedoe',
516 | api_key=jane_doe_info['api_key'])
517 |
518 | assert len(json_data['subscriptions']) == 2
519 |
520 | json_data, status = delete_account_record(
521 | request_handler, username=admin_username, api_key=admin_api_key,
522 | account_id=john_doe_info['id'])
523 |
524 | assert status == 200
525 | assert json_data['deleted'] == 1
526 |
527 | # Get a count of all registrations (should be 2)
528 | json_data, _ = get_registrations_records(
529 | request_handler, username='janedoe',
530 | api_key=jane_doe_info['api_key'])
531 |
532 | assert len(json_data['registrations']) == 2
533 |
534 | # Get a count of all subscription records
535 | json_data, status = get_subscriptions_records(
536 | request_handler, username='janedoe',
537 | api_key=jane_doe_info['api_key'])
538 |
539 | assert json_data is None
540 |
541 | # Create a new subscription (for delete check later)
542 | _, status = insert_subscription_record(
543 | request_handler, username='leahrichards',
544 | api_key=leah_richards_info['api_key'],
545 | registration_id=registration_ids['janedoe'])
546 |
547 | assert status == 201
548 |
549 |
550 | def validate_accounts_creation(request_handler, username=None, api_key=None):
551 | # List accounts, there should be 5 accounts, the admin and 4 users
552 | json_data, status = get_accounts_records(request_handler,
553 | username=username,
554 | api_key=api_key)
555 |
556 | assert len(json_data['accounts']) == 5
557 | assert status == 200
558 |
559 |
560 | def validate_chain_scenario_one(request_handler, users_and_keys=None):
561 | john_doe_info = users_and_keys['johndoe']
562 | jane_doe_info = users_and_keys['janedoe']
563 | sam_jones_info = users_and_keys['samjones']
564 | leah_richards_info = users_and_keys['leahrichards']
565 |
566 | # John Doe will create a registration and everyone will subscribe it it
567 | json_data, status = insert_registration_record(
568 | request_handler, username='johndoe',
569 | api_key=john_doe_info['api_key'],
570 | json_payload={'event': 'johndoe.event',
571 | 'description': 'john doe registered webhook',
572 | 'event_data': {'message': 'John Doe'}})
573 |
574 | assert status == 201
575 | john_doe_registration_id = json_data['id']
576 |
577 | # Everyone subscribes including John Doe
578 | # JOHN DOE:
579 | json_data, status = insert_subscription_record(
580 | request_handler, username='johndoe',
581 | api_key=john_doe_info['api_key'],
582 | registration_id=john_doe_registration_id)
583 | assert status == 201
584 | # JANE DOE:
585 | json_data, status = insert_subscription_record(
586 | request_handler, username='janedoe',
587 | api_key=jane_doe_info['api_key'],
588 | registration_id=john_doe_registration_id)
589 | assert status == 201
590 | # SAM JONES:
591 | json_data, status = insert_subscription_record(
592 | request_handler, username='samjones',
593 | api_key=sam_jones_info['api_key'],
594 | registration_id=john_doe_registration_id)
595 | assert status == 201
596 | # LEAH RICHARDS:
597 | json_data, status = insert_subscription_record(
598 | request_handler, username='leahrichards',
599 | api_key=leah_richards_info['api_key'],
600 | registration_id=john_doe_registration_id)
601 | assert status == 201
602 |
603 | # Everyone has subscribed, now add one more registration that is not
604 | # john doe's and have leah richards subscribe to it, this subscription
605 | # should not be deleted
606 | # Jane Doe's new registration
607 | json_data, status = insert_registration_record(
608 | request_handler, username='janedoe',
609 | api_key=jane_doe_info['api_key'],
610 | json_payload={'event': 'janedoe.event',
611 | 'description': 'jane doe registered webhook',
612 | 'event_data': {'message': 'Jane Doe'}})
613 |
614 | assert status == 201
615 | jane_doe_registration_id = json_data['id']
616 |
617 | # Leah Richards subscribes to Jane Doe's webhook:
618 | json_data, status = insert_subscription_record(
619 | request_handler, username='leahrichards',
620 | api_key=leah_richards_info['api_key'],
621 | registration_id=jane_doe_registration_id)
622 | assert status == 201
623 |
624 | # There should now be 2 registrations
625 | json_data, _ = get_registrations_records(
626 | request_handler, username='janedoe',
627 | api_key=jane_doe_info['api_key'])
628 |
629 | assert len(json_data['registrations']) == 2
630 |
631 | # There should now be 5 subscriptions
632 | json_data, _ = get_subscriptions_records(
633 | request_handler, username='janedoe',
634 | api_key=jane_doe_info['api_key'])
635 |
636 | assert len(json_data['subscriptions']) == 5
637 |
638 | # Delete John Doe's registration
639 | json_data, status = delete_registration_record(
640 | request_handler, username='johndoe',
641 | api_key=john_doe_info['api_key'],
642 | registration_id=john_doe_registration_id)
643 |
644 | assert status == 200
645 | assert json_data['deleted'] == 1
646 |
647 | # There should now be 1 registration
648 | json_data, _ = get_registrations_records(
649 | request_handler, username='janedoe',
650 | api_key=jane_doe_info['api_key'])
651 |
652 | assert len(json_data['registrations']) == 1
653 | # That last subscription should belong to Jane Doe
654 | assert json_data['registrations'][0]['account_id'] == \
655 | jane_doe_info['id']
656 |
657 | # There should now be 1 subscription
658 | json_data, _ = get_subscriptions_records(
659 | request_handler, username='janedoe',
660 | api_key=jane_doe_info['api_key'])
661 |
662 | assert len(json_data['subscriptions']) == 1
663 | # That last subscription should belong to Leah Richard
664 | assert json_data['subscriptions'][0]['account_id'] == \
665 | leah_richards_info['id']
666 |
667 |
668 | def validate_chain_scenario_two(request_handler, users_and_keys=None,
669 | admin_username=None, admin_api_key=None):
670 | john_doe_info = users_and_keys['johndoe']
671 | jane_doe_info = users_and_keys['janedoe']
672 | sam_jones_info = users_and_keys['samjones']
673 | leah_richards_info = users_and_keys['leahrichards']
674 |
675 | # Like scenario #1, John Doe will create a registration and everyone
676 | # will subscribe it it
677 | json_data, status = insert_registration_record(
678 | request_handler, username='johndoe',
679 | api_key=john_doe_info['api_key'],
680 | json_payload={'event': 'johndoe.event',
681 | 'description': 'john doe registered webhook',
682 | 'event_data': {'message': 'John Doe'}})
683 |
684 | assert status == 201
685 | john_doe_registration_id = json_data['id']
686 |
687 | # Everyone subscribes including John Doe
688 | # JOHN DOE:
689 | json_data, status = insert_subscription_record(
690 | request_handler, username='johndoe',
691 | api_key=john_doe_info['api_key'],
692 | registration_id=john_doe_registration_id)
693 | assert status == 201
694 | # JANE DOE:
695 | json_data, status = insert_subscription_record(
696 | request_handler, username='janedoe',
697 | api_key=jane_doe_info['api_key'],
698 | registration_id=john_doe_registration_id)
699 | assert status == 201
700 | # SAM JONES:
701 | json_data, status = insert_subscription_record(
702 | request_handler, username='samjones',
703 | api_key=sam_jones_info['api_key'],
704 | registration_id=john_doe_registration_id)
705 | assert status == 201
706 | # LEAH RICHARDS:
707 | json_data, status = insert_subscription_record(
708 | request_handler, username='leahrichards',
709 | api_key=leah_richards_info['api_key'],
710 | registration_id=john_doe_registration_id)
711 | assert status == 201
712 |
713 | # Everyone has subscribed, now add one more registration that is not
714 | # john doe's and have leah richards subscribe to it, this subscription
715 | # should not be deleted
716 | # Jane Doe's new registration
717 | json_data, status = insert_registration_record(
718 | request_handler, username='janedoe',
719 | api_key=jane_doe_info['api_key'],
720 | json_payload={'event': 'janedoe.event',
721 | 'description': 'jane doe registered webhook',
722 | 'event_data': {'message': 'Jane Doe'}})
723 |
724 | assert status == 201
725 | jane_doe_registration_id = json_data['id']
726 |
727 | # Leah Richards subscribes to Jane Doe's webhook:
728 | json_data, status = insert_subscription_record(
729 | request_handler, username='leahrichards',
730 | api_key=leah_richards_info['api_key'],
731 | registration_id=jane_doe_registration_id)
732 | assert status == 201
733 |
734 | # There should now be 2 registrations
735 | json_data, _ = get_registrations_records(
736 | request_handler, username='janedoe',
737 | api_key=jane_doe_info['api_key'])
738 |
739 | assert len(json_data['registrations']) == 2
740 |
741 | # There should now be 5 subscriptions
742 | json_data, _ = get_subscriptions_records(
743 | request_handler, username='janedoe',
744 | api_key=jane_doe_info['api_key'])
745 |
746 | assert len(json_data['subscriptions']) == 5
747 |
748 | # Delete John Doe's account
749 | json_data, status = delete_account_record(
750 | request_handler, username=admin_username,
751 | api_key=admin_api_key,
752 | account_id=john_doe_info['id'])
753 |
754 | assert status == 200
755 | assert json_data['deleted'] == 1
756 |
757 | # There should now be 1 registration
758 | json_data, _ = get_registrations_records(
759 | request_handler, username='janedoe',
760 | api_key=jane_doe_info['api_key'])
761 |
762 | assert len(json_data['registrations']) == 1
763 | # That last subscription should belong to Jane Doe
764 | assert json_data['registrations'][0]['account_id'] == \
765 | jane_doe_info['id']
766 |
767 | # There should now be 1 subscription
768 | json_data, _ = get_subscriptions_records(
769 | request_handler, username='janedoe',
770 | api_key=jane_doe_info['api_key'])
771 |
772 | assert len(json_data['subscriptions']) == 1
773 | # That last subscription should belong to Leah Richard
774 | assert json_data['subscriptions'][0]['account_id'] == \
775 | leah_richards_info['id']
776 |
777 |
778 | def validate_webhook_actions(request_handler, users_and_keys=None):
779 | john_doe_info = users_and_keys['johndoe']
780 | jane_doe_info = users_and_keys['janedoe']
781 | sam_jones_info = users_and_keys['samjones']
782 | leah_richards_info = users_and_keys['leahrichards']
783 |
784 | # John Doe will create a registration and everyone
785 | # will subscribe it it
786 | json_data, status = insert_registration_record(
787 | request_handler, username='johndoe',
788 | api_key=john_doe_info['api_key'],
789 | json_payload={'event': 'johndoe.event',
790 | 'description': 'john doe registered webhook',
791 | 'event_data': {'message': 'John Doe'}})
792 |
793 | assert status == 201
794 | john_doe_registration_id = json_data['id']
795 |
796 | # Everyone subscribes except John Doe
797 | # JANE DOE:
798 | json_data, status = insert_subscription_record(
799 | request_handler, username='janedoe',
800 | api_key=jane_doe_info['api_key'],
801 | registration_id=john_doe_registration_id)
802 | assert status == 201
803 | # SAM JONES:
804 | json_data, status = insert_subscription_record(
805 | request_handler, username='samjones',
806 | api_key=sam_jones_info['api_key'],
807 | registration_id=john_doe_registration_id)
808 | assert status == 201
809 | # LEAH RICHARDS:
810 | json_data, status = insert_subscription_record(
811 | request_handler, username='leahrichards',
812 | api_key=leah_richards_info['api_key'],
813 | registration_id=john_doe_registration_id)
814 | assert status == 201
815 |
816 | # John Doe will trigger his registered webhook
817 | json_data, status = insert_triggered_record(
818 | request_handler,
819 | username='johndoe',
820 | api_key=john_doe_info['api_key'],
821 | registration_id=john_doe_registration_id)
822 | assert status == 201
823 | assert json_data['registration_id'] == john_doe_registration_id
824 |
825 | # There should be one triggered webhook record
826 | json_data, status = get_triggered_records(
827 | request_handler,
828 | username='johndoe',
829 | api_key=john_doe_info['api_key']
830 | )
831 | assert status == 200
832 | assert 'triggered_webhooks' in json_data
833 | assert json_data['triggered_webhooks'][0]['registration_id'] == \
834 | john_doe_registration_id
835 |
836 | # Other people cannot trigger webhooks other than their own
837 | json_data, status = insert_triggered_record(
838 | request_handler,
839 | username='leahrichards',
840 | api_key=leah_richards_info['api_key'],
841 | registration_id=john_doe_registration_id)
842 | assert status == 401
843 |
844 |
845 | def validate_table_deletions(request_handler, admin_username=None,
846 | admin_api_key=None):
847 | json_data, status = delete_accounts_records(
848 | request_handler, username=admin_username, api_key=admin_api_key)
849 | assert status == 200
850 | assert json_data['deleted'] == 2
851 |
852 | json_data, status = delete_subscriptions_records(
853 | request_handler, username=admin_username, api_key=admin_api_key)
854 | assert status == 200
855 | assert json_data['deleted'] == 1
856 |
857 | json_data, status = delete_registrations_records(
858 | request_handler, username=admin_username, api_key=admin_api_key)
859 | assert status == 200
860 | assert json_data['deleted'] == 2
861 |
862 |
863 | def perform_table_deletions(request_handler, admin_username=None,
864 | admin_api_key=None):
865 | json_data, status = delete_accounts_records(
866 | request_handler, username=admin_username, api_key=admin_api_key)
867 | assert status == 200
868 |
869 | json_data, status = delete_subscriptions_records(
870 | request_handler, username=admin_username, api_key=admin_api_key)
871 | assert status == 200
872 |
873 | json_data, status = delete_registrations_records(
874 | request_handler, username=admin_username, api_key=admin_api_key)
875 | assert status == 200
876 |
877 |
878 | def run_tests():
879 | """
880 | Running this code requires you have RethinkDB setup and running as well
881 | as Redis and the Celery worker. These tests should run through all the
882 | possible scenarios.
883 | """
884 | request_handler = RequestHandler(verify_ssl=False,
885 | request_timeout=10.0)
886 |
887 | print('Functional Testing Starting...')
888 |
889 | # Start with a clean database
890 | admin_account = initdb()
891 |
892 | admin_username = 'admin'
893 | admin_api_key = admin_account['api_key']
894 |
895 | # Create four user accounts
896 | users_and_keys = create_account_records(request_handler)
897 |
898 | # **********************************
899 | # *** Validate Account Creations ***
900 | # **********************************
901 | validate_accounts_creation(request_handler,
902 | username=admin_username,
903 | api_key=admin_api_key)
904 |
905 | # ********************************
906 | # *** Validate Account Actions ***
907 | # ********************************
908 | validate_account_actions(
909 | request_handler, users_and_keys=users_and_keys,
910 | admin_username=admin_username, admin_api_key=admin_api_key)
911 |
912 | # *************************************
913 | # *** Validate Registration Actions ***
914 | # *************************************
915 | user_registration_ids = validate_registration_actions(
916 | request_handler, users_and_keys=users_and_keys)
917 |
918 | # *************************************
919 | # *** Validate Subscription Actions ***
920 | # *************************************
921 | validate_subscription_actions(
922 | request_handler,
923 | users_and_keys=users_and_keys,
924 | registration_ids=user_registration_ids,
925 | admin_username=admin_username,
926 | admin_api_key=admin_api_key)
927 |
928 | # ***************************************
929 | # *** Validate Table Deletion Actions ***
930 | # ***************************************
931 | validate_table_deletions(
932 | request_handler,
933 | admin_username=admin_username,
934 | admin_api_key=admin_api_key)
935 |
936 | # *****************************************
937 | # *** Validate Chained Deletion Actions ***
938 | # *****************************************
939 | #
940 | # The database should be empty so run the more complex tests:
941 | #
942 | # 1.) When deleting a registration record it should also remove the records
943 | # for that registration_id in the subscription table
944 | #
945 | # 2.) When deleting an account it should delete that user's account,
946 | # registrations and subscriptions. It should also delete other user's
947 | # subscriptions to those registrations that were deleted
948 | #
949 | # Re-populate the users and test scenario #1
950 | validate_chain_scenario_one(
951 | request_handler,
952 | users_and_keys=create_account_records(request_handler))
953 |
954 | # Clean the tables out again (no need to count the deletes as this has been
955 | # tested prior
956 | perform_table_deletions(
957 | request_handler,
958 | admin_username=admin_username,
959 | admin_api_key=admin_api_key)
960 | # Re-populate the users and test scenario #2
961 | validate_chain_scenario_two(
962 | request_handler,
963 | users_and_keys=create_account_records(request_handler),
964 | admin_username=admin_username,
965 | admin_api_key=admin_api_key)
966 |
967 | # *******************************
968 | # *** Validate Misc. Actions ***
969 | # *******************************
970 |
971 | # Clean the tables out again (no need to count the deletes as this has been
972 | # tested prior
973 | perform_table_deletions(
974 | request_handler,
975 | admin_username=admin_username,
976 | admin_api_key=admin_api_key)
977 | # These are tests that don't work with the flow prior so they are on
978 | # their own
979 | validate_misc_actions(
980 | request_handler,
981 | users_and_keys=create_account_records(request_handler))
982 |
983 | # ********************************
984 | # *** Validate WebHook Actions ***
985 | # ********************************
986 |
987 | # Clean the tables out again (no need to count the deletes as this has been
988 | # tested prior
989 | perform_table_deletions(
990 | request_handler,
991 | admin_username=admin_username,
992 | admin_api_key=admin_api_key)
993 |
994 | validate_webhook_actions(
995 | request_handler,
996 | users_and_keys=create_account_records(request_handler))
997 |
998 | print('Functional Testing Complete')
999 |
1000 |
1001 | if __name__ == "__main__":
1002 | run_tests()
1003 |
--------------------------------------------------------------------------------
/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/__init__.py
--------------------------------------------------------------------------------
/tests/unit/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/api/__init__.py
--------------------------------------------------------------------------------
/tests/unit/api/decorators/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/api/decorators/__init__.py
--------------------------------------------------------------------------------
/tests/unit/api/decorators/test_authorization.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | import unittest
4 | from unittest.mock import patch
5 |
6 | # Third party imports
7 | # None
8 |
9 | # Project level imports
10 | from pywebhooks.app import create_wsgi_app
11 | from pywebhooks import DEFAULT_ACCOUNTS_TABLE
12 | from pywebhooks.api.decorators.authorization import api_key_restricted_resource
13 | from pywebhooks.database.rethinkdb.interactions import Interactions
14 |
15 |
16 | def suite():
17 | test_suite = unittest.TestSuite()
18 | test_suite.addTest(WhenTestingAuthorization())
19 | return test_suite
20 |
21 |
22 | class WhenTestingAuthorization(unittest.TestCase):
23 |
24 | def setUp(self):
25 | self.app = create_wsgi_app()
26 | self.app.config['TESTING'] = True
27 | self.test_headers = [('api-key', '12345'), ('username', 'johndoe')]
28 |
29 | def test_validate_id_params_unauthorized(self):
30 | with patch.object(Interactions, 'query', return_value=False) as \
31 | query_method:
32 | @api_key_restricted_resource(verify_admin=False)
33 | def test_func():
34 | pass
35 |
36 | missing_api_key_header = [('test', 'test')]
37 |
38 | with self.app.test_request_context(headers=missing_api_key_header):
39 | response = test_func()
40 | self.assertEqual(response.status_code, client.UNAUTHORIZED)
41 |
42 | missing_username_header = [('api-key', '12345')]
43 |
44 | with self.app.test_request_context(headers=missing_username_header):
45 | response = test_func()
46 | self.assertEqual(response.status_code, client.UNAUTHORIZED)
47 |
48 | with self.app.test_request_context(headers=self.test_headers):
49 | response = test_func()
50 | self.assertEqual(response.status_code, client.UNAUTHORIZED)
51 |
52 | query_method.assert_called_with(
53 | DEFAULT_ACCOUNTS_TABLE,
54 | filters={'username': 'johndoe'}
55 | )
56 |
57 | def test_validate_id_params_unauthorized_invalid_api_key(self):
58 | with patch.object(Interactions, 'query',
59 | return_value=[{'api_key': '12345'}]):
60 |
61 | @api_key_restricted_resource(verify_admin=False)
62 | def test_func():
63 | pass
64 |
65 | with self.app.test_request_context(headers=self.test_headers):
66 | response = test_func()
67 | self.assertEqual(
68 | response.data,
69 | b'{"Error":"Invalid API key"}\n'
70 | )
71 | self.assertEqual(response.status_code, client.UNAUTHORIZED)
72 |
--------------------------------------------------------------------------------
/tests/unit/api/decorators/test_validation.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | import unittest
4 | from unittest.mock import patch
5 |
6 | # Third party imports
7 | # None
8 |
9 | # Project level imports
10 | from pywebhooks.app import create_wsgi_app
11 | from pywebhooks import DEFAULT_ACCOUNTS_TABLE
12 | from pywebhooks.api.decorators.validation import validate_id_params,\
13 | validate_username_in_header, validate_pagination_params
14 | from pywebhooks.database.rethinkdb.interactions import Interactions
15 |
16 |
17 | def suite():
18 | test_suite = unittest.TestSuite()
19 | test_suite.addTest(WhenTestingValidation())
20 | return test_suite
21 |
22 |
23 | class WhenTestingValidation(unittest.TestCase):
24 |
25 | def setUp(self):
26 | self.app = create_wsgi_app()
27 | self.app.config['TESTING'] = True
28 |
29 | def test_validate_id_params_bad_request(self):
30 |
31 | @validate_id_params(None)
32 | def test_func():
33 | pass
34 |
35 | with self.app.test_request_context():
36 | response = test_func()
37 | self.assertEqual(response.status_code, client.BAD_REQUEST)
38 |
39 | def test_validate_username_in_header_bad_request(self):
40 |
41 | @validate_username_in_header()
42 | def test_func():
43 | pass
44 |
45 | with self.app.test_request_context():
46 | response = test_func()
47 | self.assertEqual(response.status_code, client.BAD_REQUEST)
48 |
49 | def test_validate_username_in_header_not_found(self):
50 | with patch.object(Interactions, 'query', return_value=False):
51 | @validate_username_in_header()
52 | def test_func():
53 | pass
54 |
55 | test_header = [('username', 'johndoe')]
56 |
57 | with self.app.test_request_context(headers=test_header):
58 | response = test_func()
59 | self.assertEqual(response.status_code, client.NOT_FOUND)
60 |
61 | def test_validate_username_in_header(self):
62 | with patch.object(Interactions, 'query', return_value=True) as \
63 | query_method:
64 | @validate_username_in_header()
65 | def test_func():
66 | pass
67 |
68 | test_header = [('username', 'johndoe')]
69 |
70 | with self.app.test_request_context(headers=test_header):
71 | test_func()
72 | query_method.assert_called_with(
73 | DEFAULT_ACCOUNTS_TABLE,
74 | filters={'username': 'johndoe'}
75 | )
76 |
77 | def test_validate_pagination_params_invalid_start(self):
78 | @validate_pagination_params()
79 | def test_func():
80 | pass
81 |
82 | with self.app.test_request_context('/?limit=10&start=-1'):
83 | response = test_func()
84 | self.assertEqual(response.status_code, client.BAD_REQUEST)
85 |
86 | with self.app.test_request_context('/?limit=10'
87 | '&start=9999999999999991'):
88 | response = test_func()
89 | self.assertEqual(response.status_code, client.BAD_REQUEST)
90 |
91 | def test_validate_pagination_params_invalid_limit(self):
92 | @validate_pagination_params()
93 | def test_func():
94 | pass
95 |
96 | with self.app.test_request_context('/?limit=-1&start=0'):
97 | response = test_func()
98 | self.assertEqual(response.status_code, client.BAD_REQUEST)
99 |
100 | with self.app.test_request_context('/?limit=101&start=0'):
101 | response = test_func()
102 | self.assertEqual(response.status_code, client.BAD_REQUEST)
103 |
104 | def test_validate_pagination_params(self):
105 | @validate_pagination_params()
106 | def test_func():
107 | pass
108 |
109 | with self.app.test_request_context('/?limit=1&start=0'):
110 | self.assertIsNone(test_func())
111 |
--------------------------------------------------------------------------------
/tests/unit/api/handlers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/api/handlers/__init__.py
--------------------------------------------------------------------------------
/tests/unit/api/handlers/test_pagination_handler.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | import unittest
4 | from unittest.mock import patch
5 |
6 | # Third party imports
7 | from flask import request
8 |
9 | # Project level imports
10 | from pywebhooks import DEFAULT_ACCOUNTS_TABLE
11 | from pywebhooks.api.handlers.pagination_handler import paginate
12 | from pywebhooks.app import create_wsgi_app
13 | from pywebhooks.database.rethinkdb.interactions import Interactions
14 |
15 |
16 | def suite():
17 | test_suite = unittest.TestSuite()
18 | test_suite.addTest(WhenTestingpaginationHanlder())
19 | return test_suite
20 |
21 |
22 | class WhenTestingpaginationHanlder(unittest.TestCase):
23 |
24 | def setUp(self):
25 | self.app = create_wsgi_app()
26 | self.app.config['TESTING'] = True
27 |
28 | self.returned_records = \
29 | [{'description': 'jane doe registered webhook',
30 | 'event_data': {'message': 'Jane Doe'},
31 | 'epoch': 1441563242.268688,
32 | 'account_id': '04ee97a8-2f77-4117-bc96-fe8a33497c36',
33 | 'id': '069bce36-b2bf-4771-96c5-468eb37665d5',
34 | 'event': 'janedoe.event'},
35 | {'description': 'john doe registered webhook',
36 | 'event_data': {'message': 'John Doe'},
37 | 'epoch': 1441563242.300409,
38 | 'account_id': 'd382e86c-913d-4a06-abdc-232b963a8f8f',
39 | 'id': '047e549a-24a1-4194-8a41-c56b525cb815',
40 | 'event': 'johndoe.event'},
41 | {
42 | 'description': 'leah richards registered webhook',
43 | 'event_data': {'message': 'Leah Richards'},
44 | 'epoch': 1441563242.331244,
45 | 'account_id': '45012169-902e-4d24-80ba-d2f2061baef3',
46 | 'id': '50e2148f-7b43-4b85-a524-4dc64fc521fc',
47 | 'event': 'leahrichards.event'}]
48 |
49 | def test_validate_pagination_params_no_content(self):
50 | with self.app.test_request_context('/?limit=10&start=0', method='GET'):
51 | with patch.object(Interactions, 'list', return_value=[]):
52 | response = paginate(
53 | request, DEFAULT_ACCOUNTS_TABLE, 'accounts'
54 | )
55 | self.assertEqual(response.status_code, client.NO_CONTENT)
56 |
57 | def test_validate_pagination_params(self):
58 | with self.app.test_request_context('/?limit=10&start=0', method='GET'):
59 | with patch.object(Interactions, 'list',
60 | return_value=self.returned_records):
61 | response = paginate(
62 | request, DEFAULT_ACCOUNTS_TABLE, 'accounts', filters=None
63 | )
64 | self.assertEqual(response.status_code, client.OK)
65 | self.assertTrue('next_start' not in str(response.data))
66 |
67 | def test_validate_pagination_params_wth_next_marker(self):
68 | with self.app.test_request_context('/?limit=1&start=0', method='GET'):
69 | with patch.object(Interactions, 'list',
70 | return_value=self.returned_records):
71 | response = paginate(
72 | request, DEFAULT_ACCOUNTS_TABLE, 'accounts'
73 | )
74 | self.assertTrue('next_start' in str(response.data))
75 | self.assertEqual(response.status_code, client.OK)
76 |
--------------------------------------------------------------------------------
/tests/unit/api/handlers/test_resources_handler.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | import unittest
4 | from unittest.mock import Mock
5 | from unittest.mock import patch
6 |
7 | # Third party imports
8 | import rethinkdb as rethink
9 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
10 | import requests_mock
11 |
12 | # Project level imports
13 | from pywebhooks.app import create_wsgi_app
14 | from pywebhooks import DEFAULT_REGISTRATIONS_TABLE, \
15 | DEFAULT_SUBSCRIPTIONS_TABLE, DEFAULT_ACCOUNTS_TABLE
16 | from pywebhooks.database.rethinkdb.interactions import Interactions
17 | from pywebhooks.api.handlers.resources_handler import \
18 | registration_id_exists, lookup_subscription_id, lookup_registration_id, \
19 | lookup_account_id, validate_access, update, query, delete_all, \
20 | delete_accounts_except_admins, delete_registration, delete, insert, \
21 | insert_account, delete_account, client_reset_key, client_echo_valid, \
22 | reset_key
23 |
24 |
25 | def suite():
26 | test_suite = unittest.TestSuite()
27 | test_suite.addTest(WhenTestingResourcesHandler())
28 | return test_suite
29 |
30 |
31 | class WhenTestingResourcesHandler(unittest.TestCase):
32 |
33 | def setUp(self):
34 | self.app = create_wsgi_app()
35 | self.app.config['TESTING'] = True
36 |
37 | self.param_kwargs = {
38 | 'username': 'johndoe',
39 | 'api_key': '123456789abcdef'
40 | }
41 |
42 | def test_registration_id_exists(self):
43 | with patch.object(Interactions, 'query', return_value=True) as \
44 | query_method:
45 |
46 | self.assertTrue(registration_id_exists('123'))
47 |
48 | query_method.assert_called_with(
49 | DEFAULT_REGISTRATIONS_TABLE,
50 | filters={'id': '123'}
51 | )
52 |
53 | with patch.object(Interactions, 'query', return_value=False) as \
54 | query_method:
55 |
56 | self.assertFalse(registration_id_exists('321'))
57 |
58 | query_method.assert_called_with(
59 | DEFAULT_REGISTRATIONS_TABLE,
60 | filters={'id': '321'}
61 | )
62 |
63 | def test_lookup_subscription_id(self):
64 | filters = {'account_id': '12345', 'id': '55555'}
65 |
66 | with patch.object(Interactions, 'query', return_value=None) as \
67 | query_method:
68 |
69 | lookup_subscription_id('12345', '55555')
70 |
71 | query_method.assert_called_with(
72 | DEFAULT_SUBSCRIPTIONS_TABLE,
73 | filters=filters
74 | )
75 |
76 | def test_lookup_registration_id(self):
77 | filters = {'account_id': '4545', 'id': '5353'}
78 |
79 | with patch.object(Interactions, 'query', return_value=None) as \
80 | query_method:
81 |
82 | lookup_registration_id('4545', '5353')
83 |
84 | query_method.assert_called_with(
85 | DEFAULT_REGISTRATIONS_TABLE,
86 | filters=filters
87 | )
88 |
89 | def test_lookup_account_id(self):
90 | return_value = [
91 | {
92 | 'id': '123'
93 | }
94 | ]
95 |
96 | filters = {'username': 'johndoe'}
97 |
98 | with patch.object(Interactions, 'query',
99 | return_value=return_value) as query_method:
100 |
101 | ret = lookup_account_id('johndoe')
102 |
103 | self.assertEqual(ret, '123')
104 |
105 | query_method.assert_called_with(
106 | DEFAULT_ACCOUNTS_TABLE,
107 | filters=filters
108 | )
109 |
110 | def test_validate_access_admin(self):
111 | self.assertIsNone(validate_access('admin'))
112 |
113 | @patch('pywebhooks.api.handlers.resources_handler.lookup_account_id')
114 | @patch('pywebhooks.api.handlers.resources_handler.lookup_registration_id')
115 | def test_validate_access_registration_id(self,
116 | lookup_registration_id_method,
117 | lookup_account_id_method,):
118 | with self.app.test_request_context():
119 | account_id = '555'
120 | registration_id = '444'
121 |
122 | lookup_account_id_method.return_value = account_id
123 | lookup_registration_id_method.return_value = True
124 | return_value = validate_access('fred', registration_id='444')
125 |
126 | self.assertIsNone(return_value)
127 | lookup_account_id_method.assert_called_with('fred')
128 | lookup_registration_id_method.assert_called_with(
129 | account_id, registration_id)
130 | lookup_registration_id_method.return_value = False
131 | response = validate_access('fred', registration_id='444')
132 |
133 | self.assertEqual(response.status_code, client.UNAUTHORIZED)
134 |
135 | @patch('pywebhooks.api.handlers.resources_handler.lookup_account_id')
136 | @patch('pywebhooks.api.handlers.resources_handler.lookup_subscription_id')
137 | def test_validate_access_subscription_id(self,
138 | lookup_subscription_id_method,
139 | lookup_account_id_method,):
140 | with self.app.test_request_context():
141 | account_id = '123'
142 | subscription_id = '775'
143 |
144 | lookup_account_id_method.return_value = account_id
145 | lookup_subscription_id_method.return_value = True
146 | return_value = validate_access('fred', subscription_id='775')
147 |
148 | self.assertIsNone(return_value)
149 | lookup_account_id_method.assert_called_with('fred')
150 |
151 | lookup_subscription_id_method.assert_called_with(
152 | account_id, subscription_id)
153 | lookup_subscription_id_method.return_value = False
154 | response = validate_access('fred', subscription_id='775')
155 |
156 | self.assertEqual(response.status_code, client.UNAUTHORIZED)
157 |
158 | @patch('pywebhooks.api.handlers.resources_handler.lookup_account_id')
159 | def test_validate_access_incoming_account_id(self,
160 | lookup_account_id_method):
161 |
162 | with self.app.test_request_context():
163 | account_id = '111222'
164 | lookup_account_id_method.return_value = account_id
165 |
166 | response = validate_access(
167 | 'fred', incoming_account_id='333444')
168 |
169 | lookup_account_id_method.assert_called_with('fred')
170 | self.assertEqual(response.status_code, client.UNAUTHORIZED)
171 |
172 | response = validate_access(
173 | 'fred', incoming_account_id='111222')
174 | self.assertIsNone(response)
175 |
176 | @patch('pywebhooks.database.rethinkdb.interactions.get_connection')
177 | def test_update_bad_request(self, connection_method):
178 | connection_method.return_value = Mock(__enter__=Mock, __exit__=Mock())
179 | with self.app.test_request_context():
180 | with patch.object(rethink, 'table', return_value=Mock()) as \
181 | table_method:
182 | response = update(DEFAULT_REGISTRATIONS_TABLE,
183 | record_id='123',
184 | username=None,
185 | updates={})
186 | self.assertEqual(response.status_code, client.BAD_REQUEST)
187 | table_method.assert_called_once_with(
188 | DEFAULT_REGISTRATIONS_TABLE
189 | )
190 |
191 | def test_update_record_id(self):
192 | with self.app.test_request_context():
193 | with patch.object(Interactions, 'update', return_value={}):
194 | response = update(DEFAULT_REGISTRATIONS_TABLE,
195 | record_id='123',
196 | username=None,
197 | updates={})
198 | self.assertEqual(response.status_code, client.OK)
199 |
200 | def test_update_rql_runtime_error(self):
201 | with self.app.test_request_context():
202 | with patch.object(Interactions, 'update',
203 | side_effect=RqlRuntimeError(None, None, None)):
204 | response = update(DEFAULT_REGISTRATIONS_TABLE,
205 | record_id='123', username=None, updates={})
206 | self.assertRaises(RqlRuntimeError)
207 | self.assertEqual(response.status_code,
208 | client.INTERNAL_SERVER_ERROR)
209 |
210 | def test_update_rql_driver_error(self):
211 | with self.app.test_request_context():
212 | with patch.object(Interactions, 'update',
213 | side_effect=RqlDriverError(None)):
214 | update(DEFAULT_REGISTRATIONS_TABLE,
215 | record_id='123',
216 | username=None,
217 | updates={})
218 | self.assertRaises(RqlDriverError)
219 |
220 | def test_update_no_record_id(self):
221 | with self.app.test_request_context():
222 | with patch.object(Interactions, 'update', return_value={}):
223 | response = update(DEFAULT_REGISTRATIONS_TABLE,
224 | record_id=None,
225 | username='johndoe',
226 | updates={})
227 | self.assertEqual(response.status_code, client.OK)
228 |
229 | def test_query(self):
230 | with self.app.test_request_context():
231 | with patch.object(Interactions, 'get', return_value={}):
232 | response = query(DEFAULT_REGISTRATIONS_TABLE, record_id='123')
233 | self.assertEqual(response.status_code, client.OK)
234 |
235 | def test_query_rql_runtime_error(self):
236 | with self.app.test_request_context():
237 | with patch.object(Interactions, 'get',
238 | side_effect=RqlRuntimeError(None, None, None)):
239 | response = query(DEFAULT_REGISTRATIONS_TABLE, record_id='123')
240 | self.assertRaises(RqlRuntimeError)
241 | self.assertEqual(response.status_code,
242 | client.INTERNAL_SERVER_ERROR)
243 |
244 | def test_query_rql_driver_error(self):
245 | with self.app.test_request_context():
246 | with patch.object(Interactions, 'get',
247 | side_effect=RqlDriverError(None)):
248 | response = query(DEFAULT_REGISTRATIONS_TABLE, record_id='123')
249 | self.assertRaises(RqlDriverError)
250 | self.assertEqual(response.status_code,
251 | client.INTERNAL_SERVER_ERROR)
252 |
253 | def test_delete_all(self):
254 | with self.app.test_request_context():
255 | with patch.object(Interactions, 'delete_all', return_value={}):
256 | response = delete_all(DEFAULT_REGISTRATIONS_TABLE)
257 | self.assertEqual(response.status_code, client.OK)
258 |
259 | def test_delete_all_rql_runtime_error(self):
260 | with self.app.test_request_context():
261 | with patch.object(Interactions, 'delete_all',
262 | side_effect=RqlRuntimeError(None, None, None)):
263 | response = delete_all(DEFAULT_REGISTRATIONS_TABLE)
264 | self.assertRaises(RqlRuntimeError)
265 | self.assertEqual(response.status_code,
266 | client.INTERNAL_SERVER_ERROR)
267 |
268 | def test_delete_all_rql_driver_error(self):
269 | with self.app.test_request_context():
270 | with patch.object(Interactions, 'delete_all',
271 | side_effect=RqlDriverError(None)):
272 | response = delete_all(DEFAULT_REGISTRATIONS_TABLE)
273 | self.assertRaises(RqlDriverError)
274 | self.assertEqual(response.status_code,
275 | client.INTERNAL_SERVER_ERROR)
276 |
277 | def test_delete_accounts_except_admins(self):
278 | with self.app.test_request_context():
279 | with patch.object(Interactions, 'delete_specific',
280 | return_value={}):
281 | response = delete_accounts_except_admins()
282 | self.assertEqual(response.status_code, client.OK)
283 |
284 | def test_delete_accounts_except_admins_rql_runtime_error(self):
285 | with self.app.test_request_context():
286 | with patch.object(Interactions, 'delete_specific',
287 | side_effect=RqlRuntimeError(None, None, None)):
288 | response = delete_accounts_except_admins()
289 | self.assertRaises(RqlRuntimeError)
290 | self.assertEqual(response.status_code,
291 | client.INTERNAL_SERVER_ERROR)
292 |
293 | def test_delete_accounts_except_admins_rql_driver_error(self):
294 | with self.app.test_request_context():
295 | with patch.object(Interactions, 'delete_specific',
296 | side_effect=RqlDriverError(None)):
297 | response = delete_accounts_except_admins()
298 | self.assertRaises(RqlDriverError)
299 | self.assertEqual(response.status_code,
300 | client.INTERNAL_SERVER_ERROR)
301 |
302 | def test_delete_registration(self):
303 | with self.app.test_request_context():
304 | with patch.object(Interactions, 'delete_specific',
305 | return_value={}):
306 | response = delete_registration('123')
307 | self.assertEqual(response.status_code, client.OK)
308 |
309 | def test_delete_registration_rql_runtime_error(self):
310 | with self.app.test_request_context():
311 | with patch.object(Interactions, 'delete_specific',
312 | side_effect=RqlRuntimeError(None, None, None)):
313 | response = delete_registration('123')
314 | self.assertRaises(RqlRuntimeError)
315 | self.assertEqual(response.status_code,
316 | client.INTERNAL_SERVER_ERROR)
317 |
318 | def test_delete_registration_rql_driver_error(self):
319 | with self.app.test_request_context():
320 | with patch.object(Interactions, 'delete_specific',
321 | side_effect=RqlDriverError(None)):
322 | response = delete_registration('123')
323 | self.assertRaises(RqlDriverError)
324 | self.assertEqual(response.status_code,
325 | client.INTERNAL_SERVER_ERROR)
326 |
327 | def test_delete_registration_type_error(self):
328 | with self.app.test_request_context():
329 | with patch.object(Interactions, 'delete_specific',
330 | side_effect=TypeError):
331 | response = delete_registration('123')
332 | self.assertRaises(TypeError)
333 | self.assertEqual(response.status_code,
334 | client.BAD_REQUEST)
335 |
336 | def test_delete(self):
337 | with self.app.test_request_context():
338 | with patch.object(Interactions, 'delete',
339 | return_value={}):
340 | response = delete(DEFAULT_SUBSCRIPTIONS_TABLE, '123')
341 | self.assertEqual(response.status_code, client.OK)
342 |
343 | def test_delete_rql_runtime_error(self):
344 | with self.app.test_request_context():
345 | with patch.object(Interactions, 'delete',
346 | side_effect=RqlRuntimeError(None, None, None)):
347 | response = delete(DEFAULT_SUBSCRIPTIONS_TABLE, '123')
348 | self.assertRaises(RqlRuntimeError)
349 | self.assertEqual(response.status_code,
350 | client.INTERNAL_SERVER_ERROR)
351 |
352 | def test_delete_rql_driver_error(self):
353 | with self.app.test_request_context():
354 | with patch.object(Interactions, 'delete',
355 | side_effect=RqlDriverError(None)):
356 | response = delete(DEFAULT_SUBSCRIPTIONS_TABLE, '123')
357 | self.assertRaises(RqlDriverError)
358 | self.assertEqual(response.status_code,
359 | client.INTERNAL_SERVER_ERROR)
360 |
361 | def test_delete_type_error(self):
362 | with self.app.test_request_context():
363 | with patch.object(Interactions, 'delete',
364 | side_effect=TypeError):
365 | response = delete(DEFAULT_SUBSCRIPTIONS_TABLE, '123')
366 | self.assertRaises(TypeError)
367 | self.assertEqual(response.status_code,
368 | client.BAD_REQUEST)
369 |
370 | def test_insert(self):
371 | with self.app.test_request_context():
372 | with patch.object(Interactions, 'insert',
373 | return_value={}):
374 | response = insert(DEFAULT_SUBSCRIPTIONS_TABLE,
375 | **self.param_kwargs)
376 | self.assertEqual(response.status_code, client.CREATED)
377 |
378 | def test_insert_rql_runtime_error(self):
379 | with self.app.test_request_context():
380 | with patch.object(Interactions, 'insert',
381 | side_effect=RqlRuntimeError(None, None, None)):
382 | response = insert(DEFAULT_SUBSCRIPTIONS_TABLE,
383 | **self.param_kwargs)
384 | self.assertRaises(RqlRuntimeError)
385 | self.assertEqual(response.status_code,
386 | client.INTERNAL_SERVER_ERROR)
387 |
388 | def test_insert_rql_driver_error(self):
389 | with self.app.test_request_context():
390 | with patch.object(Interactions, 'insert',
391 | side_effect=RqlDriverError(None)):
392 | response = insert(DEFAULT_SUBSCRIPTIONS_TABLE,
393 | **self.param_kwargs)
394 | self.assertRaises(RqlDriverError)
395 | self.assertEqual(response.status_code,
396 | client.INTERNAL_SERVER_ERROR)
397 |
398 | def test_insert_type_error(self):
399 | with self.app.test_request_context():
400 | with patch.object(Interactions, 'insert',
401 | side_effect=TypeError):
402 | response = insert(DEFAULT_SUBSCRIPTIONS_TABLE,
403 | **self.param_kwargs)
404 | self.assertRaises(TypeError)
405 | self.assertEqual(response.status_code,
406 | client.BAD_REQUEST)
407 |
408 | def test_insert_account(self):
409 | with self.app.test_request_context():
410 | with patch.object(Interactions, 'query',
411 | return_value={}):
412 | with patch.object(Interactions, 'insert',
413 | return_value={'id': '123'}):
414 | response = insert_account(DEFAULT_SUBSCRIPTIONS_TABLE,
415 | **self.param_kwargs)
416 | self.assertEqual(response.status_code, client.CREATED)
417 |
418 | def test_insert_account_conflict(self):
419 | with self.app.test_request_context():
420 | with patch.object(Interactions, 'query',
421 | return_value={'username': 'johndoe'}):
422 | response = insert_account(DEFAULT_SUBSCRIPTIONS_TABLE,
423 | **self.param_kwargs)
424 | self.assertEqual(response.status_code, client.CONFLICT)
425 |
426 | def test_insert_account_rql_runtime_error(self):
427 | with self.app.test_request_context():
428 | with patch.object(Interactions, 'query',
429 | side_effect=RqlRuntimeError(None, None, None)):
430 | response = insert_account(DEFAULT_SUBSCRIPTIONS_TABLE,
431 | **self.param_kwargs)
432 | self.assertRaises(RqlRuntimeError)
433 | self.assertEqual(response.status_code,
434 | client.INTERNAL_SERVER_ERROR)
435 |
436 | def test_insert_account_rql_driver_error(self):
437 | with self.app.test_request_context():
438 | with patch.object(Interactions, 'query',
439 | side_effect=RqlDriverError(None)):
440 | response = insert_account(DEFAULT_SUBSCRIPTIONS_TABLE,
441 | **self.param_kwargs)
442 | self.assertRaises(RqlDriverError)
443 | self.assertEqual(response.status_code,
444 | client.INTERNAL_SERVER_ERROR)
445 |
446 | def test_insert_account_type_error(self):
447 | with self.app.test_request_context():
448 | with patch.object(Interactions, 'query',
449 | side_effect=TypeError):
450 | response = insert_account(DEFAULT_SUBSCRIPTIONS_TABLE,
451 | **self.param_kwargs)
452 | self.assertRaises(TypeError)
453 | self.assertEqual(response.status_code,
454 | client.BAD_REQUEST)
455 |
456 | @patch('pywebhooks.api.handlers.resources_handler.delete_registration')
457 | def test_delete_account(self, delete_registration_method):
458 | delete_registration_method.return_value = None
459 |
460 | with self.app.test_request_context():
461 | with patch.object(Interactions, 'delete_specific',
462 | return_value=[{'id': '123'}]):
463 | with patch.object(Interactions, 'query',
464 | return_value={}):
465 | with patch.object(Interactions, 'delete',
466 | return_value={}):
467 | response = delete_account('123')
468 | self.assertEqual(response.status_code, client.OK)
469 |
470 | def test_delete_account_rql_runtime_error(self):
471 | with self.app.test_request_context():
472 | with patch.object(Interactions, 'query',
473 | side_effect=RqlRuntimeError(None, None, None)):
474 | response = insert_account(DEFAULT_SUBSCRIPTIONS_TABLE,
475 | **self.param_kwargs)
476 | self.assertRaises(RqlRuntimeError)
477 | self.assertEqual(response.status_code,
478 | client.INTERNAL_SERVER_ERROR)
479 |
480 | def test_delete_account_rql_driver_error(self):
481 | with self.app.test_request_context():
482 | with patch.object(Interactions, 'query',
483 | side_effect=RqlDriverError(None)):
484 | response = insert_account(DEFAULT_SUBSCRIPTIONS_TABLE,
485 | **self.param_kwargs)
486 | self.assertRaises(RqlDriverError)
487 | self.assertEqual(response.status_code,
488 | client.INTERNAL_SERVER_ERROR)
489 |
490 | def test_delete_account_type_error(self):
491 | with self.app.test_request_context():
492 | with patch.object(Interactions, 'query',
493 | side_effect=TypeError):
494 | response = insert_account(DEFAULT_SUBSCRIPTIONS_TABLE,
495 | **self.param_kwargs)
496 | self.assertRaises(TypeError)
497 | self.assertEqual(response.status_code,
498 | client.BAD_REQUEST)
499 |
500 | def test_client_reset_key_success(self):
501 | with requests_mock.Mocker() as mocker:
502 | mocker.register_uri('GET', 'http://localhost/endpoint',
503 | json={'api_key': '54321'},
504 | status_code=client.OK)
505 |
506 | return_value = client_reset_key('http://localhost/endpoint',
507 | 'api_key', '12345')
508 |
509 | self.assertTrue(return_value)
510 |
511 | def test_client_reset_key_fail(self):
512 | with requests_mock.Mocker() as mocker:
513 | mocker.register_uri('GET', 'http://localhost/endpoint/hello',
514 | json={'api_key': '54321'},
515 | status_code=client.OK)
516 |
517 | return_value = client_reset_key('http://localhost/endpoint',
518 | 'api_key', '12345')
519 |
520 | self.assertFalse(return_value)
521 |
522 | @patch('pywebhooks.utils.common.generate_key')
523 | def test_client_echo_valid_success(self, generate_key_method):
524 | generate_key_method.return_value = '12345GENKEY'
525 |
526 | with requests_mock.Mocker() as mocker:
527 | mocker.register_uri('GET', 'http://localhost/endpoint',
528 | json={'echo': '12345GENKEY'},
529 | status_code=client.OK)
530 |
531 | self.assertTrue(client_echo_valid('http://localhost/endpoint'))
532 |
533 | def test_client_echo_valid_fail_wrong_key(self):
534 | with requests_mock.Mocker() as mocker:
535 | mocker.register_uri('GET', 'http://localhost/endpoint',
536 | json={'echo': '12345GENKEY'},
537 | status_code=client.OK)
538 |
539 | self.assertFalse(client_echo_valid('http://localhost/endpoint'))
540 |
541 | def test_client_echo_valid_fail_wrong_status(self):
542 | with requests_mock.Mocker() as mocker:
543 | mocker.register_uri('GET', 'http://localhost/endpoint',
544 | json={'echo': '12345GENKEY'},
545 | status_code=client.INTERNAL_SERVER_ERROR)
546 |
547 | self.assertFalse(client_echo_valid('http://localhost/endpoint'))
548 |
549 | def test_reset_key_endpoint_not_found(self):
550 | records = [{'endpoint': ''}]
551 |
552 | with self.app.test_request_context():
553 | with patch.object(Interactions, 'query', return_value=records):
554 | response = reset_key('johndoe', 'api_key')
555 | self.assertEqual(response.status_code,
556 | client.NOT_FOUND)
557 |
558 | def test_reset_key_endpoint_call_fail(self):
559 | records = [{'endpoint': 'http://localhost/endpoint'}]
560 |
561 | with self.app.test_request_context():
562 | with patch.object(Interactions, 'query', return_value=records):
563 | response = reset_key('johndoe', 'api_key')
564 | self.assertEqual(response.status_code,
565 | client.BAD_REQUEST)
566 |
567 | @patch('pywebhooks.api.handlers.resources_handler.client_reset_key')
568 | def test_reset_key_api_key(self, client_reset_key_method):
569 | records = [{'endpoint': 'http://localhost/endpoint'}]
570 | client_reset_key_method.return_value = True
571 |
572 | with self.app.test_request_context():
573 | with patch.object(Interactions, 'query', return_value=records):
574 | with patch.object(Interactions, 'update', return_value=None):
575 | response = reset_key('johndoe', 'api_key')
576 | self.assertEqual(response.status_code, client.OK)
577 |
578 | @patch('pywebhooks.api.handlers.resources_handler.client_reset_key')
579 | def test_reset_key_secret_key(self, client_reset_key_method):
580 | records = [{'endpoint': 'http://localhost/endpoint'}]
581 | client_reset_key_method.return_value = True
582 |
583 | with self.app.test_request_context():
584 | with patch.object(Interactions, 'query', return_value=records):
585 | with patch.object(Interactions, 'update', return_value=None):
586 | response = reset_key('johndoe', 'secret_key')
587 | self.assertEqual(response.status_code, client.OK)
588 |
589 | def test_reset_key_rql_runtime_error(self):
590 | with self.app.test_request_context():
591 | with patch.object(Interactions, 'query',
592 | side_effect=RqlRuntimeError(None, None, None)):
593 | response = reset_key('johndoe', 'api_key')
594 | self.assertRaises(RqlRuntimeError)
595 | self.assertEqual(response.status_code,
596 | client.INTERNAL_SERVER_ERROR)
597 |
598 | def test_reset_key_rql_driver_error(self):
599 | with self.app.test_request_context():
600 | with patch.object(Interactions, 'query',
601 | side_effect=RqlDriverError(None)):
602 | response = reset_key('johndoe', 'api_key')
603 | self.assertRaises(RqlDriverError)
604 | self.assertEqual(response.status_code,
605 | client.INTERNAL_SERVER_ERROR)
606 |
--------------------------------------------------------------------------------
/tests/unit/api/resources/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/api/resources/__init__.py
--------------------------------------------------------------------------------
/tests/unit/api/resources/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/api/resources/v1/__init__.py
--------------------------------------------------------------------------------
/tests/unit/api/resources/v1/test_account_api.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | import unittest
4 | from unittest.mock import patch
5 |
6 | # Third party imports
7 | # None
8 |
9 | # Project level imports
10 | from pywebhooks.app import app
11 | from pywebhooks.api.resources.v1.account import account_api
12 | from pywebhooks.database.rethinkdb.interactions import Interactions
13 | from pywebhooks.api.decorators import authorization
14 |
15 |
16 | def suite():
17 | test_suite = unittest.TestSuite()
18 | test_suite.addTest(WhenTestingAccountAPI())
19 | return test_suite
20 |
21 |
22 | class WhenTestingAccountAPI(unittest.TestCase):
23 |
24 | def setUp(self):
25 | app.config['TESTING'] = True
26 | self.test_headers = [
27 | ('api-key', '12345'),
28 | ('username', 'johndoe')
29 | ]
30 | self.client = app.test_client()
31 |
32 | def test_get_should_return_unsupported_media_type(self):
33 | resp = self.client.get('/v1/account/')
34 | self.assertEqual(resp.status_code, client.UNSUPPORTED_MEDIA_TYPE)
35 |
36 | def test_get_should_return_bad_request(self):
37 | resp = self.client.get('/v1/account/', content_type='application/json')
38 | self.assertEqual(resp.status_code, client.BAD_REQUEST)
39 |
40 | def test_get_should_return_unauthorized(self):
41 | with patch.object(Interactions, 'query', return_value=False):
42 | resp = self.client.get(
43 | '/v1/account/45712a61-a1b3-41a4-aa89-9593b909ae3d',
44 | content_type='application/json',
45 | headers=self.test_headers
46 | )
47 | self.assertEqual(resp.status_code, client.UNAUTHORIZED)
48 |
49 | def test_get_should_return_authorized(self):
50 | account_id = '45712a61-a1b3-41a4-aa89-9593b909ae3d'
51 | record = [
52 | {
53 | 'api_key': '12345'
54 | }
55 | ]
56 |
57 | with patch.object(authorization, 'check_password_hash',
58 | return_value=True):
59 | with patch.object(account_api, 'lookup_account_id',
60 | return_value=account_id):
61 | with patch.object(Interactions, 'query', return_value=record):
62 | with patch.object(account_api, 'query', return_value={}):
63 | resp = self.client.get(
64 | '/v1/account/{0}'.format(account_id),
65 | content_type='application/json',
66 | headers=self.test_headers
67 | )
68 | self.assertEqual(resp.status_code, client.OK)
69 |
--------------------------------------------------------------------------------
/tests/unit/database/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/database/__init__.py
--------------------------------------------------------------------------------
/tests/unit/database/rethinkdb/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/database/rethinkdb/__init__.py
--------------------------------------------------------------------------------
/tests/unit/database/rethinkdb/test_bootstrap_admin.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | import unittest
3 | from unittest.mock import patch
4 |
5 | # Third party imports
6 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
7 |
8 | # Project level imports
9 | from pywebhooks.database.rethinkdb.bootstrap_admin import create_admin_account
10 | from pywebhooks.database.rethinkdb.interactions import Interactions
11 |
12 |
13 | def suite():
14 | test_suite = unittest.TestSuite()
15 | test_suite.addTest(WhenTestingBootstrapAdminFunctions())
16 | return test_suite
17 |
18 |
19 | class WhenTestingBootstrapAdminFunctions(unittest.TestCase):
20 |
21 | def setUp(self):
22 | pass
23 |
24 | def test_create_admin_account(self):
25 | with patch.object(Interactions, 'insert', return_value=None) as \
26 | insert_method:
27 |
28 | return_data = create_admin_account()
29 |
30 | self.assertTrue(insert_method.called)
31 |
32 | self.assertTrue('api_key' in return_data)
33 | self.assertTrue('secret_key' in return_data)
34 |
35 | self.assertEqual(len(return_data['api_key']), 40)
36 | self.assertEqual(len(return_data['secret_key']), 40)
37 |
38 | def test_create_admin_account_throws_rql_runtime_error(self):
39 | with patch.object(Interactions, 'insert',
40 | side_effect=RqlRuntimeError(None, None, None)):
41 | with self.assertRaises(RqlRuntimeError) as cm:
42 | create_admin_account()
43 | self.assertEqual(cm.exception,
44 | RqlRuntimeError(None, None, None))
45 |
46 | def test_create_admin_account_throws_rql_driver_error(self):
47 | with patch.object(Interactions, 'insert',
48 | side_effect=RqlDriverError(None)):
49 | with self.assertRaises(RqlDriverError) as cm:
50 | create_admin_account()
51 | self.assertEqual(cm.exception,
52 | RqlDriverError(None))
53 |
--------------------------------------------------------------------------------
/tests/unit/database/rethinkdb/test_drop.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | import unittest
3 | from unittest.mock import Mock
4 | from unittest.mock import patch
5 |
6 | # Third party imports
7 | import rethinkdb as rethink
8 |
9 | # Project level imports
10 | from pywebhooks import DEFAULT_DB_NAME
11 | from pywebhooks.database.rethinkdb.drop import drop_database
12 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
13 |
14 |
15 | def suite():
16 | test_suite = unittest.TestSuite()
17 | test_suite.addTest(WhenTestingDrop())
18 | return test_suite
19 |
20 |
21 | class WhenTestingDrop(unittest.TestCase):
22 |
23 | def setUp(self):
24 | pass
25 |
26 | @patch('pywebhooks.database.rethinkdb.drop.get_connection')
27 | def test_drop_database(self, connection_method):
28 | connection_method.return_value = Mock(__enter__=Mock, __exit__=Mock())
29 |
30 | with patch.object(rethink, 'db_drop', return_value=Mock()) as \
31 | db_drop_method:
32 |
33 | drop_database()
34 |
35 | db_drop_method.assert_called_once_with(DEFAULT_DB_NAME)
36 |
37 | @patch('pywebhooks.database.rethinkdb.drop.get_connection',
38 | side_effect=RqlDriverError(None))
39 | def test_drop_database_throws_rql_driver_error(self, _):
40 | with self.assertRaises(RqlDriverError) as cm:
41 | drop_database()
42 | self.assertEqual(cm.exception, RqlDriverError(None))
43 |
44 | @patch('pywebhooks.database.rethinkdb.drop.get_connection',
45 | side_effect=RqlRuntimeError(None, None, None))
46 | def test_drop_database_throws_rql_runtime_error(self, _):
47 | with self.assertRaises(RqlRuntimeError) as cm:
48 | drop_database()
49 | self.assertEqual(cm.exception, RqlRuntimeError(None, None, None))
50 |
--------------------------------------------------------------------------------
/tests/unit/database/rethinkdb/test_initialize.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | import unittest
3 | from unittest.mock import Mock
4 | from unittest.mock import patch
5 |
6 | # Third party imports
7 | import rethinkdb as rethink
8 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
9 |
10 | # Project level imports
11 | from pywebhooks.database.rethinkdb.initialize import create_database
12 |
13 |
14 | def suite():
15 | test_suite = unittest.TestSuite()
16 | test_suite.addTest(WhenTestingInitialize())
17 | return test_suite
18 |
19 |
20 | class WhenTestingInitialize(unittest.TestCase):
21 |
22 | def setUp(self):
23 | pass
24 |
25 | @patch('pywebhooks.database.rethinkdb.initialize.get_connection')
26 | def test_create_database(self, connection_method):
27 | connection_method.return_value = Mock(__enter__=Mock, __exit__=Mock())
28 |
29 | with patch.object(rethink, 'db_list') as db_list_method:
30 | db_list_method.return_value.run.return_value = ['rethinkdb']
31 |
32 | with patch.object(rethink, 'db_create') as db_create_method:
33 | db_create_method.return_value.run.return_value = None
34 |
35 | with patch.object(rethink, 'db') as db_method:
36 | db_method.return_value.table_list.return_value. \
37 | run.return_value = []
38 |
39 | create_database()
40 |
41 | self.assertTrue(db_list_method.called)
42 | self.assertTrue(db_create_method.called)
43 | self.assertTrue(db_method.called)
44 |
45 | @patch('pywebhooks.database.rethinkdb.initialize.get_connection',
46 | side_effect=RqlDriverError(None))
47 | def test_create_database_throws_rql_driver_error(self, _):
48 | with self.assertRaises(RqlDriverError) as cm:
49 | create_database()
50 | self.assertEqual(cm.exception, RqlDriverError(None))
51 |
52 | @patch('pywebhooks.database.rethinkdb.initialize.get_connection',
53 | side_effect=RqlRuntimeError(None, None, None))
54 | def test_create_database_throws_rql_runtime_error(self, _):
55 | with self.assertRaises(RqlRuntimeError) as cm:
56 | create_database()
57 | self.assertEqual(cm.exception, RqlRuntimeError(None, None, None))
58 |
--------------------------------------------------------------------------------
/tests/unit/tasks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/tasks/__init__.py
--------------------------------------------------------------------------------
/tests/unit/tasks/test_webhook_notification.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | import unittest
4 | from unittest.mock import patch
5 |
6 | # Third party imports
7 | from rethinkdb.errors import RqlRuntimeError, RqlDriverError
8 |
9 | # Project level imports
10 | from pywebhooks import DEFAULT_ACCOUNTS_TABLE
11 | from pywebhooks.database.rethinkdb.interactions import Interactions
12 | from pywebhooks.tasks.webhook_notification import update_failed_count, \
13 | notify_subscribed_accounts
14 | from pywebhooks.utils.request_handler import RequestHandler
15 |
16 |
17 | def suite():
18 | test_suite = unittest.TestSuite()
19 | test_suite.addTest(WhenTestingWebHookNotifications())
20 | return test_suite
21 |
22 |
23 | class WhenTestingWebHookNotifications(unittest.TestCase):
24 |
25 | def setUp(self):
26 | pass
27 |
28 | def test_update_failed_count_exceptions(self):
29 | with patch.object(Interactions, 'get',
30 | side_effect=RqlRuntimeError(None, None, None)):
31 | self.assertIsNone(update_failed_count(account_id='123'))
32 |
33 | with patch.object(Interactions, 'get',
34 | side_effect=RqlDriverError(None)):
35 | self.assertIsNone(update_failed_count(account_id='123'))
36 |
37 | def test_update_failed_count(self):
38 | account_record = {
39 | 'failed_count': 0
40 | }
41 |
42 | with patch.object(Interactions, 'get',
43 | return_value=account_record) as \
44 | get_method:
45 | with patch.object(Interactions, 'update', return_value=None) as \
46 | update_method:
47 |
48 | update_failed_count(
49 | account_id='123',
50 | increment_failed_count=True
51 | )
52 |
53 | get_method.assert_called_with(
54 | DEFAULT_ACCOUNTS_TABLE,
55 | record_id='123'
56 | )
57 |
58 | update_method.assert_called_with(
59 | DEFAULT_ACCOUNTS_TABLE,
60 | record_id='123',
61 | updates={'failed_count': 1}
62 | )
63 |
64 | update_failed_count(
65 | account_id='123',
66 | increment_failed_count=False
67 | )
68 |
69 | update_method.assert_called_with(
70 | DEFAULT_ACCOUNTS_TABLE,
71 | record_id='123',
72 | updates={'failed_count': 0}
73 | )
74 |
75 | @patch('pywebhooks.tasks.webhook_notification.update_failed_count')
76 | def test_notify_subscribed_accounts(self, update_failed_count_method):
77 |
78 | account_id = '123'
79 | update_failed_count_method.return_value = None
80 | request_handler_return = None, client.OK
81 |
82 | with patch.object(RequestHandler, 'post',
83 | return_value=request_handler_return):
84 |
85 | notify_subscribed_accounts(event=None, event_data=None,
86 | secret_key=None, endpoint=None,
87 | account_id=account_id)
88 |
89 | update_failed_count_method.assert_called_with(
90 | account_id,
91 | increment_failed_count=False
92 | )
93 |
94 | @patch('pywebhooks.tasks.webhook_notification.update_failed_count')
95 | def test_notify_subscribed_accounts_endppoint_issue(
96 | self, update_failed_count_method):
97 |
98 | update_failed_count_method.return_value = None
99 | request_handler_return = None, client.INTERNAL_SERVER_ERROR
100 |
101 | with patch.object(RequestHandler, 'post',
102 | return_value=request_handler_return):
103 | # Catch the raise
104 | try:
105 | notify_subscribed_accounts(event=None, event_data=None,
106 | secret_key=None, endpoint=None,
107 | account_id=None)
108 | except Exception as exc:
109 | self.assertEqual('Endpoint returning non HTTP 200 status. '
110 | 'Actual code returned: 500', exc.args[0])
111 | self.assertRaises(Exception)
112 |
--------------------------------------------------------------------------------
/tests/unit/test_app.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | import unittest
4 | from unittest.mock import patch
5 |
6 | # Third party imports
7 | # None
8 |
9 | # Project level imports
10 | from pywebhooks.app import before_request, create_wsgi_app
11 |
12 |
13 | def suite():
14 | test_suite = unittest.TestSuite()
15 | test_suite.addTest(WhenTestingAppFunctions())
16 | return test_suite
17 |
18 |
19 | class WhenTestingAppFunctions(unittest.TestCase):
20 |
21 | def setUp(self):
22 | self.app = create_wsgi_app()
23 | self.app.config['TESTING'] = True
24 |
25 | @patch('pywebhooks.app.before_request')
26 | def test_before_request(self, before_request_decorator):
27 | before_request_decorator.return_value = None
28 |
29 | with self.app.test_request_context():
30 | msg, status = before_request()
31 |
32 | self.assertEqual(status, client.UNSUPPORTED_MEDIA_TYPE)
33 | self.assertEqual('Unsupported Media Type', msg)
34 |
--------------------------------------------------------------------------------
/tests/unit/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chadlung/pywebhooks/4b5f41be7c3c498a31cb0225cbde8e63c48ce999/tests/unit/utils/__init__.py
--------------------------------------------------------------------------------
/tests/unit/utils/test_common.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | import hashlib
3 | import hmac
4 | import json
5 | import unittest
6 |
7 | # Third party imports
8 | # None
9 |
10 | # Project level imports
11 | from pywebhooks.utils.common import create_signature, generate_key
12 |
13 |
14 | def suite():
15 | test_suite = unittest.TestSuite()
16 | test_suite.addTest(WhenTestingCommonFunctions())
17 | return test_suite
18 |
19 |
20 | class WhenTestingCommonFunctions(unittest.TestCase):
21 |
22 | def setUp(self):
23 | self.secret_key = 'secret-key'
24 | self.json_data = "{'message': 'hello world'}"
25 |
26 | self.signature = hmac.new(
27 | str(self.secret_key).encode('utf-8'),
28 | str(json.dumps(self.json_data)).encode('utf-8'),
29 | digestmod=hashlib.sha1
30 | ).digest()
31 |
32 | def test_bad_secret_key(self):
33 | test_signature = hmac.new(
34 | str('bad-secret-key').encode('utf-8'),
35 | str(json.dumps(self.json_data)).encode('utf-8'),
36 | digestmod=hashlib.sha1
37 | ).hexdigest()
38 |
39 | self.assertNotEqual(
40 | test_signature,
41 | create_signature(self.secret_key, self.json_data)
42 | )
43 |
44 | def test_good_secret_key(self):
45 | test_signature = hmac.new(
46 | str(self.secret_key).encode('utf-8'),
47 | str(json.dumps(self.json_data)).encode('utf-8'),
48 | digestmod=hashlib.sha1
49 | ).hexdigest()
50 |
51 | self.assertEqual(
52 | test_signature,
53 | create_signature(self.secret_key, self.json_data)
54 | )
55 |
56 | def test_generate_key(self):
57 | self.assertTrue(generate_key())
58 | self.assertEqual(len(generate_key()), 40)
59 |
--------------------------------------------------------------------------------
/tests/unit/utils/test_request_handler.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | from http import client
3 | import unittest
4 |
5 | # Third party imports
6 | import requests_mock
7 |
8 | # Project level imports
9 | from pywebhooks.utils.request_handler import RequestHandler
10 |
11 |
12 | def suite():
13 | test_suite = unittest.TestSuite()
14 | test_suite.addTest(WhenTestingRequestHandler())
15 | return test_suite
16 |
17 |
18 | class WhenTestingRequestHandler(unittest.TestCase):
19 |
20 | def setUp(self):
21 | pass
22 |
23 | def test_get(self):
24 |
25 | with requests_mock.Mocker() as mocker:
26 | mocker.register_uri('GET', 'http://localhost?test=123',
27 | json={'test': 'value'},
28 | status_code=200)
29 |
30 | request_handler = RequestHandler()
31 | data, status = request_handler.get(
32 | 'http://localhost',
33 | params={'test': 123},
34 | api_key='12345',
35 | username='johndoe'
36 | )
37 | self.assertEqual(status, client.OK)
38 | self.assertEqual({'test': 'value'}, data)
39 | self.assertEqual(request_handler.headers['username'], 'johndoe')
40 | self.assertEqual(request_handler.headers['api-key'], '12345')
41 | self.assertEqual(
42 | request_handler.headers['Content-Type'], 'application/json')
43 | self.assertEqual(
44 | request_handler.headers['Accept'], 'application/json')
45 |
46 | def test_put(self):
47 |
48 | with requests_mock.Mocker() as mocker:
49 | mocker.register_uri('PUT', 'http://localhost',
50 | json={'test': 'value'},
51 | status_code=200)
52 |
53 | request_handler = RequestHandler()
54 | data, status = request_handler.put(
55 | 'http://localhost',
56 | json_payload={'hello': 'world'},
57 | api_key='555',
58 | username='janedoe'
59 | )
60 | self.assertEqual(status, client.OK)
61 | self.assertEqual({'test': 'value'}, data)
62 | self.assertEqual(request_handler.headers['username'], 'janedoe')
63 | self.assertEqual(request_handler.headers['api-key'], '555')
64 | self.assertEqual(
65 | request_handler.headers['Content-Type'], 'application/json')
66 | self.assertEqual(
67 | request_handler.headers['Accept'], 'application/json')
68 |
69 | def test_post(self):
70 |
71 | with requests_mock.Mocker() as mocker:
72 | mocker.register_uri('POST', 'http://localhost',
73 | json={'test': 'value'},
74 | status_code=201)
75 |
76 | request_handler = RequestHandler()
77 | data, status = request_handler.post(
78 | 'http://localhost',
79 | json_payload={'hello': 'world'},
80 | api_key='8900',
81 | username='samjones',
82 | event='myevent',
83 | signature='mysignature'
84 | )
85 | self.assertEqual(status, client.CREATED)
86 | self.assertEqual({'test': 'value'}, data)
87 | self.assertEqual(request_handler.headers['username'], 'samjones')
88 | self.assertEqual(request_handler.headers['api-key'], '8900')
89 | self.assertEqual(request_handler.headers['event'], 'myevent')
90 | self.assertEqual(
91 | request_handler.headers['pywebhooks-signature'], 'mysignature')
92 | self.assertEqual(
93 | request_handler.headers['Content-Type'], 'application/json')
94 | self.assertEqual(
95 | request_handler.headers['Accept'], 'application/json')
96 |
97 | def test_patch(self):
98 |
99 | with requests_mock.Mocker() as mocker:
100 | mocker.register_uri('PATCH', 'http://localhost',
101 | json={'test': 'value'},
102 | status_code=200)
103 |
104 | request_handler = RequestHandler()
105 | data, status = request_handler.patch(
106 | 'http://localhost',
107 | json_payload={'hello': 'world'},
108 | api_key='01245',
109 | username='natml'
110 | )
111 | self.assertEqual(status, client.OK)
112 | self.assertEqual({'test': 'value'}, data)
113 | self.assertEqual(request_handler.headers['username'], 'natml')
114 | self.assertEqual(request_handler.headers['api-key'], '01245')
115 | self.assertEqual(
116 | request_handler.headers['Content-Type'], 'application/json')
117 | self.assertEqual(
118 | request_handler.headers['Accept'], 'application/json')
119 |
120 | def test_delete(self):
121 |
122 | with requests_mock.Mocker() as mocker:
123 | mocker.register_uri('DELETE', 'http://localhost/45678',
124 | json={'test': 'value'},
125 | status_code=200)
126 |
127 | request_handler = RequestHandler()
128 | data, status = request_handler.delete(
129 | 'http://localhost/45678',
130 | api_key='765434',
131 | username='birk'
132 | )
133 | self.assertEqual(status, client.OK)
134 | self.assertEqual({'test': 'value'}, data)
135 | self.assertEqual(request_handler.headers['username'], 'birk')
136 | self.assertEqual(request_handler.headers['api-key'], '765434')
137 | self.assertEqual(
138 | request_handler.headers['Content-Type'], 'application/json')
139 | self.assertEqual(
140 | request_handler.headers['Accept'], 'application/json')
141 |
--------------------------------------------------------------------------------
/tests/unit/utils/test_rethinkdb_helper.py:
--------------------------------------------------------------------------------
1 | # Standard lib imports
2 | import unittest
3 | from unittest.mock import patch
4 |
5 | # Third party imports
6 | import rethinkdb as rethink
7 |
8 | # Project level imports
9 | from pywebhooks import DEFAULT_DB_NAME
10 | from pywebhooks.utils.rethinkdb_helper import get_connection
11 |
12 |
13 | def suite():
14 | test_suite = unittest.TestSuite()
15 | test_suite.addTest(WhenTestingRethinkDBHelper())
16 | return test_suite
17 |
18 |
19 | class WhenTestingRethinkDBHelper(unittest.TestCase):
20 |
21 | def setUp(self):
22 | pass
23 |
24 | def test_get_connection(self):
25 | with patch.object(rethink, 'connect', return_value=None) as \
26 | connect_method:
27 |
28 | get_connection()
29 |
30 | connect_method.assert_called_once_with(host='rethinkdb',
31 | port=28015,
32 | auth_key='',
33 | db=DEFAULT_DB_NAME)
34 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = flake8, py36
3 |
4 | [testenv]
5 | deps = -r{toxinidir}/test-requirements.txt
6 |
7 | commands =
8 | coverage erase
9 | nosetests --with-coverage
10 | coverage report -m
11 |
12 | [testenv:flake8]
13 | exclude =
14 | .tox,
15 | .git,
16 | __pycache__,
17 | docs/source/conf.py,
18 | build,
19 | dist,
20 | tests/fixtures/*,
21 | *.pyc,
22 | *.egg-info,
23 | .cache,
24 | .eggs,
25 | .venv
26 |
--------------------------------------------------------------------------------