├── .gitignore ├── .travis.yml ├── Makefile ├── Procfile ├── README.md ├── app.json ├── app.py ├── config.json ├── requirements-test.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── static └── util.js ├── templates ├── demo.html ├── results.html └── success.html └── test ├── fixtures ├── GitHub_emojis.json ├── global_preserve_exact_body_bytes.json ├── history_failure.json ├── history_success.json ├── me_failure.json ├── me_success.json ├── preserve_exact_bytes.json ├── price_estimates_failure.json ├── price_estimates_success.json ├── products_failure.json ├── products_success.json ├── submit_failure.json ├── time_estimates_failure.json └── time_estimates_success.json └── test_endpoints.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | .coverage 4 | env/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: make bootstrap 3 | script: make 4 | branches: 5 | except: 6 | - /^v[0-9]/ 7 | after_success: coveralls 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: bootstrap clean lint test 2 | .DEFAULT_GOAL := test 3 | 4 | test: clean lint 5 | @py.test test/ --cov app.py -s 6 | 7 | lint: 8 | @flake8 . 9 | 10 | clean: 11 | @find . -type f -name '*.pyc' -delete 12 | 13 | bootstrap: 14 | @pip install -r requirements.txt 15 | @pip install -r requirements-test.txt 16 | @python setup.py develop 17 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app --log-file=- 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Example Uber app for developers 2 | ============================== 3 | 4 | [![TravisCI](https://travis-ci.org/uber/Python-Sample-Application.svg?branch=master)](https://travis-ci.org/uber/Python-Sample-Application) 5 | [![Coverage Status](https://coveralls.io/repos/uber/Python-Sample-Application/badge.png)](https://coveralls.io/r/uber/Python-Sample-Application) 6 | 7 | https://developer.uber.com/ 8 | 9 | What Is This? 10 | ------------- 11 | 12 | This is a simple Python/Flask application intended to provide a working example of Uber's external API. The goal of these endpoints is to be simple, well-documented and to provide a base for developers to develop other applications off of. 13 | 14 | 15 | How To Use This 16 | --------------- 17 | 18 | 1. Navigate over to https://developer.uber.com/, and sign up for an Uber developer account. 19 | 2. Register a new Uber application and make your Redirect URI `http://localhost:7000/submit` - ensure that both the `profile` and `history` OAuth scopes are checked. 20 | 3. Fill in the relevant information in the `config.json` file in the root folder and add your client id and secret as the environment variables `UBER_CLIENT_ID` and `UBER_CLIENT_SECRET`. 21 | 4. Run `export UBER_CLIENT_ID="`*{your client id}*`"&&export UBER_CLIENT_SECRET="`*{your client secret}*`"` 22 | 5. Run `pip install -r requirements.txt` to install dependencies 23 | 6. Run `python app.py` 24 | 7. Navigate to http://localhost:7000 in your browser 25 | 26 | 27 | Testing 28 | ------- 29 | 30 | 1. Install the dependencies with `make bootstrap` 31 | 2. Run the command `make test` 32 | 3. If you delete the fixtures, or decide to add some of your own, you’ll have to re-generate them, and the way this is done is by running the app, getting an auth_token from the main page of the app. Paste that token in place of the `test_auth_token` at the top of the `test_endpoints.py` file, then run the tests. 33 | 34 | 35 | Development 36 | ----------- 37 | 38 | If you want to work on this application we’d love your pull requests and tickets on GitHub! 39 | 40 | 1. If you open up a ticket, please make sure it describes the problem or feature request fully. 41 | 2. If you send us a pull request, make sure you add a test for what you added, and make sure the full test suite runs with `make test`. 42 | 43 | Deploy to Heroku 44 | ---------------- 45 | 46 | Click the button below to set up this sample app on Heroku: 47 | 48 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 49 | 50 | After creating your app on Heroku, you have to configure the redirect URL for your Uber OAuth app. Use a `https://`*{your-app-name}*`.herokuapp.com/submit` URL. 51 | You will also want to configure the heroku environment variable FLASK_DEBUG=False in order to properly serve SSL traffic. 52 | 53 | Making Requests 54 | --------------- 55 | 56 | The base for all requests is https://api.uber.com/v1/, to find a list of all available endpoints, please visit: https://developer.uber.com/v1/endpoints/ 57 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Uber API Python/Flask sample", 3 | "logo": "http://blogcdn.uber.com/wp-content/uploads/2011/12/New-Logo-Vertical-Dark.jpg", 4 | "repository": "https://github.com/uber/Python-Sample-Application", 5 | "keywords": ["uber", "python", "flask"], 6 | "env": { 7 | "UBER_CLIENT_ID": { 8 | "description": "Your Uber API client id" 9 | }, 10 | "UBER_CLIENT_SECRET": { 11 | "description": "Your Uber API client secret" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # urllib is built-in. No need to have it in the requirements.txt 2 | # python3 app.py 3 | 4 | from __future__ import absolute_import 5 | 6 | import json 7 | import os 8 | from urllib.parse import urlparse 9 | 10 | 11 | from flask import Flask, render_template, request, redirect, session 12 | from flask_sslify import SSLify 13 | from rauth import OAuth2Service 14 | import requests 15 | 16 | app = Flask(__name__, static_folder='static', static_url_path='') 17 | app.requests_session = requests.Session() 18 | app.secret_key = os.urandom(24) 19 | 20 | sslify = SSLify(app) 21 | 22 | with open('config.json') as f: 23 | config = json.load(f) 24 | 25 | 26 | def generate_oauth_service(): 27 | """Prepare the OAuth2Service that is used to make requests later.""" 28 | return OAuth2Service( 29 | client_id=os.environ.get('UBER_CLIENT_ID'), 30 | client_secret=os.environ.get('UBER_CLIENT_SECRET'), 31 | name=config.get('name'), 32 | authorize_url=config.get('authorize_url'), 33 | access_token_url=config.get('access_token_url'), 34 | base_url=config.get('base_url'), 35 | ) 36 | 37 | 38 | def generate_ride_headers(token): 39 | """Generate the header object that is used to make api requests.""" 40 | return { 41 | 'Authorization': 'bearer %s' % token, 42 | 'Content-Type': 'application/json', 43 | } 44 | 45 | 46 | @app.route('/health', methods=['GET']) 47 | def health(): 48 | """Check the status of this application.""" 49 | return ';-)' 50 | 51 | 52 | @app.route('/', methods=['GET']) 53 | def signup(): 54 | """The first step in the three-legged OAuth handshake. 55 | 56 | You should navigate here first. It will redirect to login.uber.com. 57 | """ 58 | params = { 59 | 'response_type': 'code', 60 | 'redirect_uri': get_redirect_uri(request), 61 | 'scopes': ','.join(config.get('scopes')), 62 | } 63 | url = generate_oauth_service().get_authorize_url(**params) 64 | return redirect(url) 65 | 66 | 67 | @app.route('/submit', methods=['GET']) 68 | def submit(): 69 | """The other two steps in the three-legged Oauth handshake. 70 | 71 | Your redirect uri will redirect you here, where you will exchange 72 | a code that can be used to obtain an access token for the logged-in use. 73 | """ 74 | params = { 75 | 'redirect_uri': get_redirect_uri(request), 76 | 'code': request.args.get('code'), 77 | 'grant_type': 'authorization_code' 78 | } 79 | response = app.requests_session.post( 80 | config.get('access_token_url'), 81 | auth=( 82 | os.environ.get('UBER_CLIENT_ID'), 83 | os.environ.get('UBER_CLIENT_SECRET') 84 | ), 85 | data=params, 86 | ) 87 | session['access_token'] = response.json().get('access_token') 88 | 89 | return render_template( 90 | 'success.html', 91 | token=response.json().get('access_token') 92 | ) 93 | 94 | 95 | @app.route('/demo', methods=['GET']) 96 | def demo(): 97 | """Demo.html is a template that calls the other routes in this example.""" 98 | return render_template('demo.html', token=session.get('access_token')) 99 | 100 | 101 | @app.route('/products', methods=['GET']) 102 | def products(): 103 | """Example call to the products endpoint. 104 | 105 | Returns all the products currently available in San Francisco. 106 | """ 107 | url = config.get('base_uber_url') + 'products' 108 | params = { 109 | 'latitude': config.get('start_latitude'), 110 | 'longitude': config.get('start_longitude'), 111 | } 112 | 113 | response = app.requests_session.get( 114 | url, 115 | headers=generate_ride_headers(session.get('access_token')), 116 | params=params, 117 | ) 118 | 119 | if response.status_code != 200: 120 | return 'There was an error', response.status_code 121 | return render_template( 122 | 'results.html', 123 | endpoint='products', 124 | data=response.text, 125 | ) 126 | 127 | 128 | @app.route('/time', methods=['GET']) 129 | def time(): 130 | """Example call to the time estimates endpoint. 131 | 132 | Returns the time estimates from the given lat/lng given below. 133 | """ 134 | url = config.get('base_uber_url') + 'estimates/time' 135 | params = { 136 | 'start_latitude': config.get('start_latitude'), 137 | 'start_longitude': config.get('start_longitude'), 138 | } 139 | 140 | response = app.requests_session.get( 141 | url, 142 | headers=generate_ride_headers(session.get('access_token')), 143 | params=params, 144 | ) 145 | 146 | if response.status_code != 200: 147 | return 'There was an error', response.status_code 148 | return render_template( 149 | 'results.html', 150 | endpoint='time', 151 | data=response.text, 152 | ) 153 | 154 | 155 | @app.route('/price', methods=['GET']) 156 | def price(): 157 | """Example call to the price estimates endpoint. 158 | 159 | Returns the time estimates from the given lat/lng given below. 160 | """ 161 | url = config.get('base_uber_url') + 'estimates/price' 162 | params = { 163 | 'start_latitude': config.get('start_latitude'), 164 | 'start_longitude': config.get('start_longitude'), 165 | 'end_latitude': config.get('end_latitude'), 166 | 'end_longitude': config.get('end_longitude'), 167 | } 168 | 169 | response = app.requests_session.get( 170 | url, 171 | headers=generate_ride_headers(session.get('access_token')), 172 | params=params, 173 | ) 174 | 175 | if response.status_code != 200: 176 | return 'There was an error', response.status_code 177 | return render_template( 178 | 'results.html', 179 | endpoint='price', 180 | data=response.text, 181 | ) 182 | 183 | 184 | @app.route('/history', methods=['GET']) 185 | def history(): 186 | """Return the last 5 trips made by the logged in user.""" 187 | url = config.get('base_uber_url_v1_1') + 'history' 188 | params = { 189 | 'offset': 0, 190 | 'limit': 5, 191 | } 192 | 193 | response = app.requests_session.get( 194 | url, 195 | headers=generate_ride_headers(session.get('access_token')), 196 | params=params, 197 | ) 198 | 199 | if response.status_code != 200: 200 | return 'There was an error', response.status_code 201 | return render_template( 202 | 'results.html', 203 | endpoint='history', 204 | data=response.text, 205 | ) 206 | 207 | 208 | @app.route('/me', methods=['GET']) 209 | def me(): 210 | """Return user information including name, picture and email.""" 211 | url = config.get('base_uber_url') + 'me' 212 | response = app.requests_session.get( 213 | url, 214 | headers=generate_ride_headers(session.get('access_token')), 215 | ) 216 | 217 | if response.status_code != 200: 218 | return 'There was an error', response.status_code 219 | return render_template( 220 | 'results.html', 221 | endpoint='me', 222 | data=response.text, 223 | ) 224 | 225 | 226 | def get_redirect_uri(request): 227 | """Return OAuth redirect URI.""" 228 | parsed_url = urlparse(request.url) 229 | if parsed_url.hostname == 'localhost': 230 | return 'http://{hostname}:{port}/submit'.format( 231 | hostname=parsed_url.hostname, port=parsed_url.port 232 | ) 233 | return 'https://{hostname}/submit'.format(hostname=parsed_url.hostname) 234 | 235 | if __name__ == '__main__': 236 | app.debug = os.environ.get('FLASK_DEBUG', True) 237 | app.run(host='127.0.0.1', port=5000) 238 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token_url": "https://login.uber.com/oauth/token", 3 | "authorize_url": "https://login.uber.com/oauth/authorize", 4 | "base_url": "https://login.uber.com/", 5 | "scopes": ["profile", "history_lite"], 6 | "name": "Sample app", 7 | "base_uber_url": "https://api.uber.com/v1/", 8 | "base_uber_url_v1_1" : "https://api.uber.com/v1.1/", 9 | "start_latitude": "37.781955", 10 | "start_longitude": "-122.402367", 11 | "end_latitude": "37.744352", 12 | "end_longitude": "-122.416743" 13 | } 14 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | # Test harness 2 | pytest==2.5.2 3 | # Coverage 4 | pytest-cov==1.6 5 | # HTTP Fixtures 6 | betamax==0.4.0 7 | # LINT OR DIE 8 | flake8==2.1.0 9 | pep8==1.5.6 10 | pyflakes==0.8.1 11 | # Coveralls 12 | coveralls==0.4.2 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Jinja2 3 | MarkupSafe 4 | Werkzeug 5 | gnureadline 6 | itsdangerous 7 | rauth 8 | requests 9 | 10 | gunicorn 11 | Flask-SSLify 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | exclude = env 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='Python-Sample-Application', 5 | version='0.0.1', 6 | author='Uber Engineering', 7 | author_email='developer@uber.com', 8 | packages=find_packages(), 9 | description='Python sample application', 10 | ) 11 | -------------------------------------------------------------------------------- /static/util.js: -------------------------------------------------------------------------------- 1 | function action(endpoint_name) { 2 | window.location.replace('/' + endpoint_name); 3 | } 4 | 5 | function redirect_to_demo(code) { 6 | window.location.replace('/demo'); 7 | } 8 | -------------------------------------------------------------------------------- /templates/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% if token %} 8 |

Congratulations! you have successfully authenticated and your token is: {{ token }}

9 | {% else %} 10 |

Something went wrong :(

11 | {% endif %} 12 | 13 |

Test the following functions of the api!

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

Welcome to the {{ endpoint }} endpoint!

10 | 11 |

Here is the result of a call to the {{ endpoint }}:

12 |

{{ data }}

13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/GitHub_emojis.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/global_preserve_exact_body_bytes.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"base64_string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://httpbin.org/get"}, "response": {"body": {"base64_string": "ewogICJhcmdzIjoge30sIAogICJoZWFkZXJzIjogewogICAgIkFjY2VwdCI6ICIqLyoiLCAKICAgICJBY2NlcHQtRW5jb2RpbmciOiAiZ3ppcCwgZGVmbGF0ZSIsIAogICAgIkNvbm5lY3Rpb24iOiAiY2xvc2UiLCAKICAgICJIb3N0IjogImh0dHBiaW4ub3JnIiwgCiAgICAiVXNlci1BZ2VudCI6ICJweXRob24tcmVxdWVzdHMvMi4zLjAgQ1B5dGhvbi8yLjcuNSBEYXJ3aW4vMTMuMi4wIiwgCiAgICAiWC1SZXF1ZXN0LUlkIjogIjE1ODlmMjgzLWRkYjMtNGU0YS1hYTMzLWVjM2I0NGVlY2JjMiIKICB9LCAKICAib3JpZ2luIjogIjguMjYuMTU3LjEyOCIsIAogICJ1cmwiOiAiaHR0cHM6Ly9odHRwYmluLm9yZy9nZXQiCn0=", "encoding": null}, "headers": {"content-length": ["353"], "server": ["gunicorn/18.0"], "connection": ["keep-alive"], "access-control-allow-credentials": ["true"], "date": ["Fri, 22 Aug 2014 18:11:35 GMT"], "access-control-allow-origin": ["*"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://httpbin.org/get"}, "recorded_at": "2014-08-22T18:11:35"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/history_failure.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer NOT_A_CODE"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.4.0"]}, "method": "GET", "uri": "https://api.uber.com/v1.1/history?limit=5&offset=0"}, "response": {"body": {"string": "{\"message\":\"Invalid OAuth 2.0 credentials provided.\",\"code\":\"unauthorized\"}", "encoding": null}, "headers": {"content-length": ["75"], "server": ["nginx"], "connection": ["keep-alive"], "date": ["Mon, 06 Oct 2014 16:58:57 GMT"], "x-uber-app": ["uberex-nonsandbox"], "content-type": ["application/json"]}, "status": {"message": "Unauthorized", "code": 401}, "url": "https://api.uber.com/v1.1/history?limit=5&offset=0"}, "recorded_at": "2014-10-06T16:58:57"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/history_success.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer MLth87eHvSAaCQ1vn7jTd0xA9Kapo5"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.4.0"]}, "method": "GET", "uri": "https://api.uber.com/v1.1/history?limit=5&offset=0"}, "response": {"body": {"base64_string": "H4sIAAAAAAAAA62TTW7bMBBG78K1aHB+yOH4KkURUBSJGojtxJYWReC7d5SFi9gGaqDRjuLokW++0Yerx+Uwu20UHNyx93OzRRjc626/W18P7tfuPB9Pv932x4c7z2Vezm5rX+3fXtvcJje4yQrKoTa3BdhIzFGFMw9uWXaTlbYWShtz9TFS8Jwq+kKZfW9x7KI1tKpGMcZpfpl3+5XDgFbNRIN7Ox2npc4vn6wC9lRjVcFPFvlsdR57nSahEDisN2qH6QuJE5ndqb0v7XxzBgFdhn+L4QZDRFXQv2LcU9fCwVcu3TPW4jV08hFUEiXRVPIjMYKwUr5BjDCG9FCMAos+IYYb0IyUrd3XwEJhqG0KPvVaPRcZvSaiNbreKcaecW3ybWDMwGiU//cyUmbr0H1gtkPIT3jxBpk0RYGrFkdoHdEGr49sMoheI3dfa5NWiGxm4YEWqVnJd2gZScFI91q2AyRPaOEmZEg5iOjVq/SeRonox1p0DSn4cQT10ssIQkUCyAMv1Bjo1mviMpbSxHeekmcwqoINgRifmh0dCxvr6/9lJEl2n3svtN5lvvy8/AHuOYdTZwQAAA==", "encoding": null}, "headers": {"x-rate-limit-remaining": ["999"], "content-language": ["en"], "content-encoding": ["gzip"], "transfer-encoding": ["chunked"], "strict-transport-security": ["max-age=2592000"], "server": ["nginx"], "connection": ["keep-alive"], "x-rate-limit-reset": ["1412618400"], "x-uber-app": ["uberex-nonsandbox"], "date": ["Mon, 06 Oct 2014 17:12:33 GMT"], "x-rate-limit-limit": ["1000"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.uber.com/v1.1/history?limit=5&offset=0"}, "recorded_at": "2014-10-06T17:12:33"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/me_failure.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer NOT_A_CODE"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://api.uber.com/v1/me"}, "response": {"body": {"string": "{\"message\":\"Invalid OAuth 2.0 credentials provided.\",\"code\":\"unauthorized\"}", "encoding": null}, "headers": {"content-length": ["75"], "server": ["nginx"], "connection": ["keep-alive"], "date": ["Fri, 22 Aug 2014 19:22:26 GMT"], "x-uber-app": ["uberex-nonsandbox"], "content-type": ["application/json"]}, "status": {"message": "Unauthorized", "code": 401}, "url": "https://api.uber.com/v1/me"}, "recorded_at": "2014-08-22T19:22:25"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/me_success.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer vX0ye7xeg42vNcBtWv59k9K0WjB5qH"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://api.uber.com/v1/me"}, "response": {"body": {"base64_string": "H4sIAAAAAAAAA0WMyw7CIBRE/4W1tLWlULpy78YPaNJcHhXsA0Ihxhj/XRoXLuecmXkjb2VMQaMemRj93g/lUKrzs/bOhiga2HkhF5fUFNwWi03HoawYkTXUDZ86RqhQZ8K64uH1HZ3QZMMexw3W4/EKaTewZLzAn94g2Nlk6INb3SidOrrKvAjPUK9gl5zn3/aShA6FdGtWKVmVTcsqAYwTTFnbYQICMFSa4pZIKRinilOOPl8CtsPG2gAAAA==", "encoding": null}, "headers": {"x-rate-limit-remaining": ["999"], "content-language": ["en"], "content-encoding": ["gzip"], "transfer-encoding": ["chunked"], "strict-transport-security": ["max-age=2592000"], "server": ["nginx"], "connection": ["keep-alive"], "x-rate-limit-reset": ["1408737600"], "x-uber-app": ["uberex-nonsandbox"], "date": ["Fri, 22 Aug 2014 19:22:26 GMT"], "x-rate-limit-limit": ["1000"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.uber.com/v1/me"}, "recorded_at": "2014-08-22T19:22:25"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/preserve_exact_bytes.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"base64_string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://httpbin.org/get"}, "response": {"body": {"base64_string": "ewogICJhcmdzIjoge30sIAogICJoZWFkZXJzIjogewogICAgIkFjY2VwdCI6ICIqLyoiLCAKICAgICJBY2NlcHQtRW5jb2RpbmciOiAiZ3ppcCwgZGVmbGF0ZSIsIAogICAgIkNvbm5lY3Rpb24iOiAiY2xvc2UiLCAKICAgICJIb3N0IjogImh0dHBiaW4ub3JnIiwgCiAgICAiVXNlci1BZ2VudCI6ICJweXRob24tcmVxdWVzdHMvMi4zLjAgQ1B5dGhvbi8yLjcuNSBEYXJ3aW4vMTMuMi4wIiwgCiAgICAiWC1SZXF1ZXN0LUlkIjogImFkYmJkMTUzLTk1MjYtNDk4Ni04NTI1LTZjNWUxMGNkNDA5ZSIKICB9LCAKICAib3JpZ2luIjogIjguMjYuMTU3LjEyOCIsIAogICJ1cmwiOiAiaHR0cHM6Ly9odHRwYmluLm9yZy9nZXQiCn0=", "encoding": null}, "headers": {"content-length": ["353"], "server": ["gunicorn/18.0"], "connection": ["keep-alive"], "access-control-allow-credentials": ["true"], "date": ["Fri, 22 Aug 2014 18:11:35 GMT"], "access-control-allow-origin": ["*"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://httpbin.org/get"}, "recorded_at": "2014-08-22T18:11:34"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/price_estimates_failure.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer NOT_A_CODE"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://api.uber.com/v1/estimates/price?start_longitude=-122.402367&end_longitude=-122.416743&start_latitude=37.781955&end_latitude=37.744352"}, "response": {"body": {"string": "{\"message\":\"No authentication provided.\",\"code\":\"unauthorized\"}", "encoding": null}, "headers": {"content-length": ["63"], "server": ["nginx"], "connection": ["keep-alive"], "date": ["Fri, 01 Aug 2014 20:19:37 GMT"], "x-uber-app": ["uberex-nonsandbox"], "content-type": ["application/json"]}, "status": {"message": "Unauthorized", "code": 401}, "url": "https://api.uber.com/v1/estimates/price?start_longitude=-122.402367&end_longitude=-122.416743&start_latitude=37.781955&end_latitude=37.744352"}, "recorded_at": "2014-08-01T20:19:37"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/price_estimates_success.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer 42Kq726Vv6lzJ0TMhXWsgUulVjRsxh"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://api.uber.com/v1/estimates/price?start_longitude=-122.402367&end_longitude=-122.416743&start_latitude=37.781955&end_latitude=37.744352"}, "response": {"body": {"base64_string": "H4sIAAAAAAAAA5WTXU+DMBSG/8uJl0AokOG4m3pjnFcTs8SYhrXN1lg+UlrdXPbfbYcmhOLQ29P3nL7P+ThCIzlhLWQvRxA1KQT/ZBRT3jaiOOCqKBlkkG+YvFkubh/AM6IPzFrFy0LZpygJwtCEL2Q0sqaaKMyp0WtTaiMK8mZyWi23DJdaKN4IziRkKAg96FW/ihI/tuV3fLvrfxuH3bdES8kqcsCkpmenqzs4eRMoq/zZBYlnv4J0egej1e9/g4hnfpKMQCTfvfs3hG3i02J971BUWghnFj21w6CKPR+F6Cr1RvHIFJOMuhidcsBggxfnYE2tHf8wD8LURAfb9CN27O9HvTtbNPdR5BoHhAKETHzgHfKpJTr7WZrMwTGgNLi2BcfsW7nrX5hoO30GKPWjMYIIBant1yjB6+kLI8sf5N4DAAA=", "encoding": null}, "headers": {"x-rate-limit-remaining": ["4993"], "content-language": ["en"], "content-encoding": ["gzip"], "transfer-encoding": ["chunked"], "strict-transport-security": ["max-age=2592000"], "server": ["nginx"], "connection": ["keep-alive"], "x-rate-limit-reset": ["1406926800"], "x-uber-app": ["uberex-nonsandbox"], "date": ["Fri, 01 Aug 2014 20:19:37 GMT"], "x-rate-limit-limit": ["5000"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.uber.com/v1/estimates/price?start_longitude=-122.402367&end_longitude=-122.416743&start_latitude=37.781955&end_latitude=37.744352"}, "recorded_at": "2014-08-01T20:19:38"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/products_failure.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer NOT_A_CODE"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://api.uber.com/v1/products?latitude=37.781955&longitude=-122.402367"}, "response": {"body": {"string": "{\"message\":\"No authentication provided.\",\"code\":\"unauthorized\"}", "encoding": null}, "headers": {"content-length": ["63"], "server": ["nginx"], "connection": ["keep-alive"], "date": ["Fri, 01 Aug 2014 20:19:37 GMT"], "x-uber-app": ["uberex-nonsandbox"], "content-type": ["application/json"]}, "status": {"message": "Unauthorized", "code": 401}, "url": "https://api.uber.com/v1/products?latitude=37.781955&longitude=-122.402367"}, "recorded_at": "2014-08-01T20:19:38"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/products_success.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer 42Kq726Vv6lzJ0TMhXWsgUulVjRsxh"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://api.uber.com/v1/products?latitude=37.781955&longitude=-122.402367"}, "response": {"body": {"base64_string": "H4sIAAAAAAAAA7XSQU/CMBQH8K/S9AxbVOJhN/Rk5KTDkDhDHl23NXZ9TfsGDMJ3t0NjjBuc4NJD//m//PLy9tw6zBtBnifvey7AglDU8mQy4qqGUvKEV0Q2yeIszm/grph4C7uds5NIaGzywqGhyEjKYgFuTK2VPotrNPj9jlcaxGdkTclHPFfeamiXBupu8Hwl3cNs+vgcoh/GUuUhaEJw7HUd6YVTlhSakKSVZOhUqQxo1vX5YfSXfX8ptm/Wp9Gv87chcuj0wC+INSvQMbmWrkUj/4EvtmeCrRoWd9tMp4unIXLX6pnT8Mk2iipsiFHYeAXe66vJO9/2NH0x5N720cGpcTMW6Omql3Hk6tsz4NmgWPfIv1ynwpUfr0SDKyUrHTbW88PH4Qugn78toAMAAA==", "encoding": null}, "headers": {"x-rate-limit-remaining": ["4992"], "content-language": ["en"], "content-encoding": ["gzip"], "transfer-encoding": ["chunked"], "strict-transport-security": ["max-age=2592000"], "server": ["nginx"], "connection": ["keep-alive"], "x-rate-limit-reset": ["1406926800"], "x-uber-app": ["uberex-nonsandbox"], "date": ["Fri, 01 Aug 2014 20:19:37 GMT"], "x-rate-limit-limit": ["5000"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.uber.com/v1/products?latitude=37.781955&longitude=-122.402367"}, "recorded_at": "2014-08-01T20:19:38"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/submit_failure.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "code=not_a_code&redirect_uri=http%3A%2F%2Flocalhost%3ANone%2Fsubmit&grant_type=authorization_code", "encoding": "utf-8"}, "headers": {"Content-Length": ["97"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.4.0"], "Content-Type": ["application/x-www-form-urlencoded"], "Authorization": ["Basic MW14R3hFR1JxcXU3Z1g5OTE2Rnc0azkwVmlHcTdLVzA6SV8wNW9tRFNIbjM5MHFISDJreE5zd04wb004bUZlc3pBQnREckQ0RQ=="]}, "method": "POST", "uri": "https://login.uber.com/oauth/token"}, "response": {"body": {"string": "{\"error\": \"invalid_grant\"}", "encoding": null}, "headers": {"content-length": ["26"], "server": ["nginx"], "connection": ["keep-alive"], "pragma": ["no-cache"], "cache-control": ["no-store"], "date": ["Mon, 06 Oct 2014 17:11:05 GMT"], "x-uber-app": ["login"], "content-type": ["application/json"]}, "status": {"message": "BAD REQUEST", "code": 400}, "url": "https://login.uber.com/oauth/token"}, "recorded_at": "2014-10-06T17:11:05"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/time_estimates_failure.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer NOT_A_CODE"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://api.uber.com/v1/estimates/time?start_longitude=-122.402367&start_latitude=37.781955"}, "response": {"body": {"string": "{\"message\":\"No authentication provided.\",\"code\":\"unauthorized\"}", "encoding": null}, "headers": {"content-length": ["63"], "server": ["nginx"], "connection": ["keep-alive"], "date": ["Fri, 01 Aug 2014 20:19:38 GMT"], "x-uber-app": ["uberex-nonsandbox"], "content-type": ["application/json"]}, "status": {"message": "Unauthorized", "code": 401}, "url": "https://api.uber.com/v1/estimates/time?start_longitude=-122.402367&start_latitude=37.781955"}, "recorded_at": "2014-08-01T20:19:38"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/fixtures/time_estimates_success.json: -------------------------------------------------------------------------------- 1 | {"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": ["*/*"], "Content-Type": ["application/json"], "Accept-Encoding": ["gzip, deflate"], "Authorization": ["bearer 42Kq726Vv6lzJ0TMhXWsgUulVjRsxh"], "User-Agent": ["python-requests/2.3.0 CPython/2.7.5 Darwin/13.2.0"]}, "method": "GET", "uri": "https://api.uber.com/v1/estimates/time?start_longitude=-122.402367&start_latitude=37.781955"}, "response": {"body": {"base64_string": "H4sIAAAAAAAAA6tWKsnMTS1WsoquVsrJT07MyaxKTYlPySwuyEmsjM9LzE1VslIKTUotcvJxdPZW0lFKLQZqSCwBChsZmOgo4VFZUJSfUppcEp+ZAjSiFGhEUk5icrZSrQ4Bm4JDw4iyB6IOw5bi0jL8doCcEuIY4YliibG5IYZnkBRi2FKSWJFJ2JoIFDuMTM2x2gFShWFBhVJtbC0AZXhg5Z4BAAA=", "encoding": null}, "headers": {"x-rate-limit-remaining": ["4991"], "content-language": ["en"], "content-encoding": ["gzip"], "transfer-encoding": ["chunked"], "strict-transport-security": ["max-age=2592000"], "server": ["nginx"], "connection": ["keep-alive"], "x-rate-limit-reset": ["1406926800"], "x-uber-app": ["uberex-nonsandbox"], "date": ["Fri, 01 Aug 2014 20:19:38 GMT"], "x-rate-limit-limit": ["5000"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.uber.com/v1/estimates/time?start_longitude=-122.402367&start_latitude=37.781955"}, "recorded_at": "2014-08-01T20:19:38"}], "recorded_with": "betamax/0.4.0"} -------------------------------------------------------------------------------- /test/test_endpoints.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from betamax import Betamax 4 | from app import app 5 | 6 | with Betamax.configure() as config: 7 | config.cassette_library_dir = 'test/fixtures' 8 | 9 | test_auth_token = 'MLth87eHvSAaCQ1vn7jTd0xA9Kapo5' 10 | 11 | 12 | class TestCases(unittest.TestCase): 13 | def setUp(self): 14 | # Necessary to disable SSLify 15 | app.debug = True 16 | self.test_app = app.test_client() 17 | self.session = app.requests_session 18 | 19 | def test_health_endpoint(self): 20 | """Assert that the health endpoint works.""" 21 | response = app.test_client().get('/health') 22 | self.assertEquals(response.data, ';-)') 23 | 24 | def test_root_endpoint(self): 25 | """Assert that the / endpoint correctly redirects to login.uber.com.""" 26 | response = app.test_client().get('/') 27 | self.assertIn('login.uber.com', response.data) 28 | 29 | def test_submit_endpoint_failure(self): 30 | """Assert that the submit endpoint returns no code in the response.""" 31 | with app.test_client() as client: 32 | with client.session_transaction() as session: 33 | session['access_token'] = test_auth_token 34 | with Betamax(app.requests_session).use_cassette('submit_failure'): 35 | response = client.get('/submit?code=not_a_code') 36 | self.assertIn('None', response.data) 37 | 38 | def test_products_endpoint_returns_success(self): 39 | """Assert that the products endpoint returns success. 40 | 41 | When a valid key is passed in. 42 | """ 43 | with app.test_client() as client: 44 | with client.session_transaction() as session: 45 | session['access_token'] = test_auth_token 46 | with Betamax(app.requests_session).use_cassette('products_success'): 47 | response = client.get('/products') 48 | self.assertIn('products', response.data) 49 | self.assertEquals(response.status_code, 200) 50 | 51 | def test_products_endpoint_returns_failure(self): 52 | """Assert that the products endpoint returns failure. 53 | 54 | When an invalid key is passed in. 55 | """ 56 | with app.test_client() as client: 57 | with client.session_transaction() as session: 58 | session['access_token'] = 'NOT_A_CODE' 59 | with Betamax(self.session).use_cassette('products_failure'): 60 | response = client.get('/products') 61 | self.assertEquals(response.status_code, 401) 62 | 63 | def test_time_estimates_endpoint_returns_success(self): 64 | """Assert that the time estimates endpoint returns success. 65 | 66 | When a valid key is passed in. 67 | """ 68 | with app.test_client() as client: 69 | with client.session_transaction() as session: 70 | session['access_token'] = test_auth_token 71 | with Betamax(app.requests_session).use_cassette('time_estimates_success'): 72 | response = client.get('/time') 73 | self.assertIn('times', response.data) 74 | self.assertEquals(response.status_code, 200) 75 | 76 | def test_time_estimates_endpoint_returns_failure(self): 77 | """Assert that the time estimates endpoint returns failure. 78 | 79 | When an invalid key is passed in. 80 | """ 81 | with app.test_client() as client: 82 | with client.session_transaction() as session: 83 | session['access_token'] = 'NOT_A_CODE' 84 | with Betamax(app.requests_session).use_cassette('time_estimates_failure'): 85 | response = client.get('/time') 86 | self.assertEquals(response.status_code, 401) 87 | 88 | def test_price_estimates_endpoint_returns_success(self): 89 | """Assert that the price estimates endpoint returns success. 90 | 91 | When a valid key is passed in. 92 | """ 93 | with app.test_client() as client: 94 | with client.session_transaction() as session: 95 | session['access_token'] = test_auth_token 96 | with Betamax(app.requests_session).use_cassette('price_estimates_success'): 97 | response = client.get('/price') 98 | self.assertIn('prices', response.data) 99 | self.assertEquals(response.status_code, 200) 100 | 101 | def test_price_estimates_endpoint_returns_failure(self): 102 | """Assert that the price estimates endpoint returns failure. 103 | 104 | When an invalid key is passed in. 105 | """ 106 | with app.test_client() as client: 107 | with client.session_transaction() as session: 108 | session['access_token'] = 'NOT_A_CODE' 109 | with Betamax(app.requests_session).use_cassette('price_estimates_failure'): 110 | response = client.get('/price') 111 | self.assertEquals(response.status_code, 401) 112 | 113 | def test_history_endpoint_returns_success(self): 114 | """Assert that the history endpoint returns success. 115 | 116 | When a valid key is passed in. 117 | """ 118 | with app.test_client() as client: 119 | with client.session_transaction() as session: 120 | session['access_token'] = test_auth_token 121 | with Betamax(app.requests_session).use_cassette('history_success'): 122 | response = client.get('/history') 123 | self.assertIn('history', response.data) 124 | self.assertEquals(response.status_code, 200) 125 | 126 | def test_history_endpoint_returns_failure(self): 127 | """Assert that the price estimates endpoint returns failure. 128 | 129 | When an invalid key is passed in. 130 | """ 131 | with app.test_client() as client: 132 | with client.session_transaction() as session: 133 | session['access_token'] = 'NOT_A_CODE' 134 | with Betamax(app.requests_session).use_cassette('history_failure'): 135 | response = client.get('/history') 136 | self.assertEquals(response.status_code, 401) 137 | 138 | def test_me_endpoint_returns_success(self): 139 | """Assert that the me endpoint returns success. 140 | 141 | When a valid key is passed in. 142 | """ 143 | with app.test_client() as client: 144 | with client.session_transaction() as session: 145 | session['access_token'] = test_auth_token 146 | with Betamax(app.requests_session).use_cassette('me_success'): 147 | response = client.get('/me') 148 | self.assertIn('picture', response.data) 149 | self.assertEquals(response.status_code, 200) 150 | 151 | def test_me_endpoint_returns_failure(self): 152 | """Assert that the me endpoint returns failure. 153 | 154 | When an invalid key is passed in. 155 | """ 156 | with app.test_client() as client: 157 | with client.session_transaction() as session: 158 | session['access_token'] = 'NOT_A_CODE' 159 | with Betamax(app.requests_session).use_cassette('me_failure'): 160 | response = client.get('/me') 161 | self.assertEquals(response.status_code, 401) 162 | --------------------------------------------------------------------------------