├── tests ├── __init__.py ├── test_assets │ ├── bad_git_config │ └── good_git_config ├── context.py ├── test_web.py ├── test_twilio.py └── test_configure.py ├── Procfile ├── requirements.txt ├── static ├── images │ └── favicon.ico └── styles │ └── index.css ├── .gitignore ├── Makefile ├── .travis.yml ├── local_settings.py ├── templates ├── index.html ├── client.html └── base.html ├── LICENSE ├── app.py ├── README.md └── configure.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python app.py 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=0.9 2 | twilio>=3.4.3 3 | mock>=0.8.0 4 | nose>=1.1.2 5 | URLObject>=2.1.0 6 | -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharyvoase/Twilio-Hackpack-for-Heroku-and-Flask/master/static/images/favicon.ico -------------------------------------------------------------------------------- /tests/test_assets/bad_git_config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | build 3 | include 4 | lib 5 | .Python 6 | *.pyc 7 | *.project 8 | *.pydevproject 9 | *.pyo 10 | *.swp 11 | *.coverage 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | pip install -r requirements.txt --use-mirrors 3 | 4 | test: 5 | nosetests -v tests 6 | 7 | configure: 8 | python configure.py 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | install: 6 | - pip install -r requirements.txt --use-mirrors 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.abspath('..')) 4 | 5 | from twilio.rest import resources 6 | 7 | import configure 8 | from app import app 9 | -------------------------------------------------------------------------------- /tests/test_assets/good_git_config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | [remote "heroku"] 7 | url = git@heroku.com:look-here-snacky-11211.git 8 | fetch = +refs/heads/*:refs/remotes/heroku/* 9 | -------------------------------------------------------------------------------- /tests/test_web.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .context import app 3 | 4 | class WebTest(unittest.TestCase): 5 | def setUp(self): 6 | self.app = app.test_client() 7 | 8 | 9 | class ExampleTests(WebTest): 10 | def test_index(self): 11 | response = self.app.get('/') 12 | self.assertEqual("200 OK", response.status) 13 | 14 | def test_client(self): 15 | response = self.app.get('/client') 16 | self.assertEqual("200 OK", response.status) 17 | -------------------------------------------------------------------------------- /local_settings.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Configuration Settings 3 | ''' 4 | 5 | ''' Uncomment to configure using the file. 6 | WARNING: Be careful not to post your account credentials on GitHub. 7 | 8 | TWILIO_ACCOUNT_SID = "ACxxxxxxxxxxxxx" 9 | TWILIO_AUTH_TOKEN = "yyyyyyyyyyyyyyyy" 10 | TWILIO_APP_SID = "APzzzzzzzzz" 11 | TWILIO_CALLER_ID = "+17778889999" 12 | ''' 13 | 14 | # Begin Heroku configuration - configured through environment variables. 15 | import os 16 | TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID', None) 17 | TWILIO_AUTH_TOKEN = os.environ.get('TWILIO_AUTH_TOKEN', None) 18 | TWILIO_CALLER_ID = os.environ.get('TWILIO_CALLER_ID', None) 19 | TWILIO_APP_SID = os.environ.get('TWILIO_APP_SID', None) 20 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Successfully Deployed{% endblock %} 4 | 5 | {% block header %}Success!{% endblock %} 6 | 7 | {% block message %}You deployed Twilio Hackpack for Heroku and Flask{% endblock %} 8 | 9 | {% block blurb %}Here are some helpful links to get you started:{% endblock 10 | %} 11 | 12 | {% block content %} 13 | Configure Twilio 15 | 16 |
17 | {% for key, value in params.iteritems() %} 18 |
{{key}}
19 |
{{value}}
20 | {% endfor %} 21 |
22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Twilio, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /templates/client.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Client{% endblock %} 4 | 5 | {% block head %} 6 | 8 | {% if not configuration_error %} 9 | 11 | 14 | 49 | {% endif %} 50 | {% endblock %} 51 | 52 | {% block header %}Nice!{% endblock %} 53 | 54 | {% block message %}Try using Twilio Client on your new Twilio app{% endblock %} 55 | 56 | {% block content %} 57 |
58 | 61 | 62 | 65 |
66 | {% if configuration_error %} 67 | {{ configuration_error }} 68 | {% else %} 69 | Loading pigeons... 70 | {% endif %} 71 |
72 |
73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block title %}{% endblock %} 5 | - Twilio Hackpack for Heroku and Flask 6 | {% block head %}{% endblock %} 7 | 9 | 11 | 13 | 14 | 16 | 18 | 20 | 21 | 22 |
23 |
24 | 31 |
32 |
33 |
34 | {% if configuration_error %} 35 |

We just need to set a few more 36 | configuration options.

37 |

Check your local_settings.py to set these missing 38 | options:

39 |
40 |
Error
41 |
{{ configuration_error }}
42 |
43 | {% else %} 44 |

{% block message %}{% endblock %}

45 |

{% block blurb %}{% endblock %}

46 | {% block content %}{% endblock %} 47 | {% endif %} 48 |
49 |
50 | 62 |
63 | {% block footer_js %}{% endblock %} 64 | 65 | 66 | -------------------------------------------------------------------------------- /tests/test_twilio.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from twilio.util import RequestValidator 3 | from .context import app 4 | 5 | 6 | app.config['SERVER_NAME'] = 'localhost' 7 | app.config['TWILIO_ACCOUNT_SID'] = 'ACxxxxxx' 8 | app.config['TWILIO_AUTH_TOKEN'] = 'yyyyyyyyy' 9 | app.config['TWILIO_CALLER_ID'] = '+15558675309' 10 | 11 | 12 | class TwiMLTest(unittest.TestCase): 13 | def setUp(self): 14 | self.app = app.test_client() 15 | self.validator = RequestValidator(app.config['TWILIO_AUTH_TOKEN']) 16 | 17 | def assertTwiML(self, response): 18 | self.assertTrue("" in response.data, "Did not find " \ 19 | ": %s" % response.data) 20 | self.assertTrue("" in response.data, "Did not find " \ 21 | ": %s" % response.data) 22 | self.assertEqual("200 OK", response.status) 23 | 24 | def sms(self, body, url='/sms', to=app.config['TWILIO_CALLER_ID'], 25 | from_='+15558675309', extra_params=None, signed=True): 26 | params = { 27 | 'SmsSid': 'SMtesting', 28 | 'AccountSid': app.config['TWILIO_ACCOUNT_SID'], 29 | 'To': to, 30 | 'From': from_, 31 | 'Body': body, 32 | 'FromCity': 'BROOKLYN', 33 | 'FromState': 'NY', 34 | 'FromCountry': 'US', 35 | 'FromZip': '55555'} 36 | if extra_params: 37 | params = dict(params.items() + extra_params.items()) 38 | if signed: 39 | abs_url = 'http://{0}{1}'.format(app.config['SERVER_NAME'], url) 40 | signature = self.validator.compute_signature(abs_url, params) 41 | return self.app.post(url, data=params, 42 | headers={'X-Twilio-Signature': signature}) 43 | return self.app.post(url, data=params) 44 | 45 | def call(self, url='/voice', to=app.config['TWILIO_CALLER_ID'], 46 | from_='+15558675309', digits=None, extra_params=None, signed=True): 47 | params = { 48 | 'CallSid': 'CAtesting', 49 | 'AccountSid': app.config['TWILIO_ACCOUNT_SID'], 50 | 'To': to, 51 | 'From': from_, 52 | 'CallStatus': 'ringing', 53 | 'Direction': 'inbound', 54 | 'FromCity': 'BROOKLYN', 55 | 'FromState': 'NY', 56 | 'FromCountry': 'US', 57 | 'FromZip': '55555'} 58 | if digits: 59 | params['Digits'] = digits 60 | if extra_params: 61 | params = dict(params.items() + extra_params.items()) 62 | if signed: 63 | abs_url = 'http://{0}{1}'.format(app.config['SERVER_NAME'], url) 64 | signature = self.validator.compute_signature(abs_url, params) 65 | return self.app.post(url, data=params, 66 | headers={'X-Twilio-Signature': signature}) 67 | return self.app.post(url, data=params) 68 | 69 | 70 | class ExampleTests(TwiMLTest): 71 | def test_sms(self): 72 | response = self.sms("Test") 73 | self.assertTwiML(response) 74 | 75 | def test_voice(self): 76 | response = self.call() 77 | self.assertTwiML(response) 78 | 79 | def test_unsigned_sms_fails(self): 80 | response = self.sms("Test", signed=False) 81 | self.assertEqual(response.status_code, 403) 82 | 83 | def test_unsigned_voice_fails(self): 84 | response = self.call(signed=False) 85 | self.assertEqual(response.status_code, 403) 86 | -------------------------------------------------------------------------------- /static/styles/index.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | background: #d80000; 3 | } 4 | 5 | #container { 6 | height: 800px; 7 | background: url("http://static0.twilio.com/packages/twilio-conference/img/twiliocon-2012-bg.png") no-repeat center top; 8 | } 9 | 10 | #header { 11 | width: 960px; 12 | margin: 0 auto; 13 | padding: 100px 0 0 0; 14 | } 15 | 16 | #header .section-heading { 17 | line-height: 68px; 18 | margin: 0; 19 | padding: 0 0 0 233px; 20 | font-family: 'Helvetica',sans-serif; 21 | font-size: 52px; 22 | font-weight: 100; 23 | color: #FFF; 24 | text-align: center; 25 | background: url("http://static0.twilio.com/packages/twilio-conference/img/twilio-logo-2012-header.png") no-repeat 148px center; 26 | } 27 | 28 | #content { 29 | width: 960px; 30 | height: 250px; 31 | margin: 0 auto; 32 | padding: 70px 0 0 0; 33 | } 34 | 35 | #content .section-heading { 36 | margin: 0 0 10px 0; 37 | font-family: 'Helvetica',sans-serif; 38 | font-size: 38px; 39 | font-weight: 100; 40 | color: #7C0000; 41 | text-align: center; 42 | } 43 | 44 | #content .blurb { 45 | line-height: 1.2em; 46 | margin: 5px 0 20px 85px; 47 | font-family: 'Helvetica',sans-serif; 48 | font-size: 17px; 49 | font-weight: 100; 50 | color: #DDD; 51 | } 52 | 53 | #url_list { 54 | margin: 10px 0 20px 85px; 55 | } 56 | 57 | #url_list dt { 58 | line-height: 1.2em; 59 | margin: 0px; 60 | font-family: 'Helvetica',sans-serif; 61 | font-size: 14px; 62 | font-weight: 100; 63 | color: #DDD; 64 | } 65 | 66 | #url_list dd, #url_list a { 67 | margin: 3px 0 5px 30px; 68 | font-family: 'Helvetica',sans-serif; 69 | font-size: 18px; 70 | font-weight: 100; 71 | color: #7C0000; 72 | } 73 | 74 | #url_list a:hover { 75 | color: #FFF; 76 | } 77 | 78 | #content a { 79 | text-decoration: none; 80 | } 81 | 82 | #content .button { 83 | width: 200px; 84 | float: right; 85 | margin: 30px 80px 0 0; 86 | padding: 10px; 87 | font-family: 'Helvetica',sans-serif; 88 | font-size: 26px; 89 | font-weight: 100; 90 | color: #454545; 91 | text-align: center; 92 | background-color: #EFEFEF; 93 | background: -webkit-gradient(linear, left top, left bottom, from(#efefef), to(#cccccc)); 94 | background: -moz-linear-gradient(top, #efefef, #cccccc); 95 | border-top: 2px solid #FFF; 96 | border-bottom: 1px solid #555; 97 | } 98 | 99 | #content .button:hover { 100 | color: #222; 101 | background-color: #FFF; 102 | background: -webkit-gradient(linear, left top, left bottom, from(white), to(#dddddd)); 103 | background: -moz-linear-gradient(top, white, #dddddd); 104 | border-top: 2px solid #FFF; 105 | } 106 | 107 | #content .button:active { 108 | color: #222; 109 | background-color: #FFF; 110 | background: -webkit-gradient(linear, left top, left bottom, from(#cccccc), to(#efefef)); 111 | background: -moz-linear-gradient(top, #cccccc, #efefef); 112 | border-top: 2px solid #888; 113 | border-bottom: 1px solid #FFF; 114 | } 115 | 116 | .footer { 117 | width: 960px; 118 | margin: 0 auto; 119 | padding: 10px 0 0 0; 120 | text-align: center; 121 | color: #7C0000; 122 | } 123 | 124 | .footer a { 125 | text-decoration: none; 126 | color: #7C0000; 127 | } 128 | 129 | .footer a:hover { 130 | color: #FFF; 131 | } 132 | 133 | #client-controls { 134 | margin: -300px 0 0 0; 135 | } 136 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import os 3 | 4 | from flask import Flask 5 | from flask import Response 6 | from flask import current_app 7 | from flask import render_template 8 | from flask import request 9 | from flask import url_for 10 | 11 | from twilio import twiml 12 | from twilio.util import RequestValidator 13 | from twilio.util import TwilioCapability 14 | from urlobject import URLObject 15 | 16 | 17 | # Declare and configure application 18 | app = Flask(__name__, static_url_path='/static') 19 | app.config.from_pyfile('local_settings.py') 20 | 21 | 22 | def validate_twilio_request(): 23 | """Ensure a request is coming from Twilio by checking the signature.""" 24 | validator = RequestValidator(current_app.config['TWILIO_AUTH_TOKEN']) 25 | if 'X-Twilio-Signature' not in request.headers: 26 | return False 27 | signature = request.headers['X-Twilio-Signature'] 28 | if 'CallSid' in request.form: 29 | # See: http://www.twilio.com/docs/security#notes 30 | url = URLObject(url_for('.voice', _external=True)).without_auth() 31 | if request.is_secure: 32 | url = url.without_port() 33 | elif 'SmsSid' in request.form: 34 | url = url_for('.sms', _external=True) 35 | else: 36 | return False 37 | return validator.validate(url, request.form, signature) 38 | 39 | 40 | def twilio_secure(func): 41 | """Wrap a view function to ensure that every request comes from Twilio.""" 42 | @wraps(func) 43 | def wrapper(*a, **kw): 44 | if validate_twilio_request(): 45 | return func(*a, **kw) 46 | return Response("Not a valid Twilio request", status=403) 47 | return wrapper 48 | 49 | 50 | # Voice Request URL 51 | @app.route('/voice', methods=['GET', 'POST']) 52 | @twilio_secure 53 | def voice(): 54 | response = twiml.Response() 55 | response.say("Congratulations! You deployed the Twilio Hackpack" \ 56 | " for Heroku and Flask.") 57 | return str(response) 58 | 59 | 60 | # SMS Request URL 61 | @app.route('/sms', methods=['GET', 'POST']) 62 | @twilio_secure 63 | def sms(): 64 | response = twiml.Response() 65 | response.sms("Congratulations! You deployed the Twilio Hackpack" \ 66 | " for Heroku and Flask.") 67 | return str(response) 68 | 69 | 70 | # Twilio Client demo template 71 | @app.route('/client') 72 | def client(): 73 | configuration_error = None 74 | for key in ('TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_APP_SID', 75 | 'TWILIO_CALLER_ID'): 76 | if not app.config[key]: 77 | configuration_error = "Missing from local_settings.py: " \ 78 | "%s" % key 79 | token = None 80 | if not configuration_error: 81 | capability = TwilioCapability(app.config['TWILIO_ACCOUNT_SID'], 82 | app.config['TWILIO_AUTH_TOKEN']) 83 | capability.allow_client_incoming("joey_ramone") 84 | capability.allow_client_outgoing(app.config['TWILIO_APP_SID']) 85 | token = capability.generate() 86 | params = {'token': token} 87 | return render_template('client.html', params=params, 88 | configuration_error=configuration_error) 89 | 90 | 91 | # Installation success page 92 | @app.route('/') 93 | def index(): 94 | params = { 95 | 'Voice Request URL': url_for('.voice', _external=True), 96 | 'SMS Request URL': url_for('.sms', _external=True), 97 | 'Client URL': url_for('.client', _external=True)} 98 | return render_template('index.html', params=params, 99 | configuration_error=None) 100 | 101 | 102 | # If PORT not specified by environment, assume development config. 103 | if __name__ == '__main__': 104 | port = int(os.environ.get("PORT", 5000)) 105 | if port == 5000: 106 | app.debug = True 107 | app.run(host='0.0.0.0', port=port) 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twilio Hackpack for Heroku and Flask 2 | 3 | An easy-to-use repo to kickstart your Twilio app using Flask and deploy onto 4 | Heroku. Easy to clone, easy to tweak, easy to deploy. 5 | 6 | [![Build 7 | Status](https://secure.travis-ci.org/RobSpectre/Twilio-Hackpack-for-Heroku-and-Flask.png)] 8 | (http://travis-ci.org/RobSpectre/Twilio-Hackpack-for-Heroku-and-Flask) 9 | 10 | 11 | ## Features 12 | 13 | Look at all these crazy features! 14 | 15 | * [Twilio Client](http://www.twilio.com/api/client) - This hackpack ships 16 | with a base Jinja2 template for Twilio Client already configured and ready to 17 | call. 18 | * Automagic Configuration - Just run `python configure.py --account_sid ACxxxx --auth_token yyyyy` 19 | and the hackpack configures Twilio and Heroku for you. 20 | * Production Ready - The [production branch](https://github.com/RobSpectre/Twilio-Hackpack-for-Heroku-and-Flask/tree/production) 21 | features a few more settings and dependencies to make the hackpack ready to 22 | put into live service. 23 | * Plug-and-Play - Procfile, requirements.txt and Makefile make installation 24 | and usage a breeze. 25 | * Boilerplate - All the Flask app boilerplate with example Voice and SMS 26 | Request URLs ready for use on Twilio. 27 | * Testing - Easy base class for unit testing with example tests, nose ready. 28 | * PEP8 - It's good for you! 29 | 30 | 31 | ## Usage 32 | 33 | This hackpack ships with two ready-to-go endpoints for your Twilio Voice and SMS 34 | apps. The two routes /voice and /sms contain two examples you can modify 35 | easily. 36 | 37 | For example, here is a quick Twilio Voice app that plays some Ramones. 38 | 39 | ```python 40 | @app.route('/voice', methods=['POST']) 41 | def voice(): 42 | response = twiml.Response() 43 | response.play("http://example.com/music/ramones.mp3") 44 | return str(response) 45 | ``` 46 | 47 | SMS apps are similarly easy. 48 | 49 | ```python 50 | @app.route('/sms', methods=['POST']) 51 | def sms(): 52 | response = twiml.Response() 53 | response.sms("The Ramones are great!") 54 | return str(response) 55 | ``` 56 | 57 | These apps can get interactive pretty quickly. For example, let's make an SMS 58 | app that responds with "Best band ever" when you text RAMONES. 59 | 60 | ```python 61 | @app.route('/sms', methods=['POST']) 62 | def sms(): 63 | response = twiml.Response() 64 | body = request.form['Body'] 65 | if "RAMONES" in body: 66 | response.sms("Best band ever.") 67 | else: 68 | response.sms("Not the best band ever.") 69 | return str(response) 70 | ``` 71 | 72 | You can apply this same concept to 73 | [Gathering](http://www.twilio.com/docs/api/twiml/gather) user input on Twilio 74 | Voice. Here we will Gather the user input with one route and then handle the 75 | user input with another. 76 | 77 | ```python 78 | @app.route('/voice', methods=['POST']) 79 | def voice(): 80 | response = twiml.Response() 81 | with response.gather(numDigits=1, action="/gather") as gather: 82 | gather.say("Press 1 to indicate The Ramones are the best band ever.") 83 | return str(response) 84 | 85 | @app.route('/gather', methods=['POST']) 86 | def gather(): 87 | response = twiml.Response() 88 | digits = request.form['Digits'] 89 | if digits == "1": 90 | response.say("You are correct. The Ramones are the best.") 91 | else: 92 | response.say("You are wrong. Never call me again.") 93 | return str(response) 94 | ``` 95 | 96 | ## Installation 97 | 98 | Step-by-step on how to deploy, configure and develop on this hackpack. 99 | 100 | ### Getting Started 101 | 102 | 1) Grab latest source 103 |
104 | git clone git://github.com/RobSpectre/Twilio-Hackpack-for-Heroku-and-Flask.git 
105 | 
106 | 107 | 2) Navigate to folder and create new Heroku Cedar app 108 |
109 | heroku create --stack cedar
110 | 
111 | 112 | 3) Deploy to Heroku 113 |
114 | git push heroku master
115 | 
116 | 117 | 4) Scale your dynos 118 |
119 | heroku scale web=1
120 | 
121 | 122 | 5) Visit the home page of your new Heroku app to see your newly configured app! 123 |
124 | heroku open
125 | 
126 | 127 | 128 | ### Configuration 129 | 130 | Want to use the built-in Twilio Client template? Configure your hackpack with 131 | three easy options. 132 | 133 | #### Automagic Configuration 134 | 135 | This hackpack ships with an auto-configure script that will create a new TwiML 136 | app, purchase a new phone number, and set your Heroku app's environment 137 | variables to use your new settings. Here's a quick step-by-step: 138 | 139 | 1) Make sure you have all dependencies installed 140 |
141 | make init
142 | 
143 | 144 | 2) Run configure script and follow instructions. 145 |
146 | python configure.py --account_sid ACxxxxxx --auth_token yyyyyyy
147 | 
148 | 149 | 3) For local development, copy/paste the environment variable commands the 150 | configurator provides to your shell. 151 |
152 | export TWILIO_ACCOUNT_SID=ACxxxxxx
153 | export TWILIO_AUTH_TOKEN=yyyyyyyyy
154 | export TWILIO_APP_SID=APzzzzzzzzzz
155 | export TWILIO_CALLER_ID=+15556667777
156 | 
157 | 158 | Automagic configuration comes with a number of features. 159 | `python configure.py --help` to see them all. 160 | 161 | 162 | #### local_settings.py 163 | 164 | local_settings.py is a file available in the hackpack route for you to configure 165 | your twilio account credentials manually. Be sure not to expose your Twilio 166 | account to a public repo though. 167 | 168 | ```python 169 | ACCOUNT_SID = "ACxxxxxxxxxxxxx" 170 | AUTH_TOKEN = "yyyyyyyyyyyyyyyy" 171 | TWILIO_APP_SID = "APzzzzzzzzz" 172 | TWILIO_CALLER_ID = "+17778889999" 173 | ``` 174 | 175 | #### Setting Your Own Environment Variables 176 | 177 | The configurator will automatically use your environment variables if you 178 | already have a TwiML app and phone number you would prefer to use. When these 179 | environment variables are present, it will configure the Twilio and Heroku apps 180 | all to use the hackpack. 181 | 182 | 1) Set environment variables locally. 183 |
184 | export TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxx
185 | export TWILIO_AUTH_TOKEN=yyyyyyyyyyyyyyyyy
186 | export TWILIO_APP_SID=APzzzzzzzzzzzzzzzzzz
187 | export TWILIO_CALLER_ID=+15556667777
188 | 
189 | 190 | 2) Run configurator 191 |
192 | python configure.py
193 | 
194 | 195 | 196 | ### Development 197 | 198 | Getting your local environment setup to work with this hackpack is similarly 199 | easy. After you configure your hackpack with the steps above, use this guide to 200 | get going locally: 201 | 202 | 1) Install the dependencies. 203 |
204 | make init
205 | 
206 | 207 | 2) Launch local development webserver 208 |
209 | foreman start
210 | 
211 | 212 | 3) Open browser to [http://localhost:5000](http://localhost:5000). 213 | 214 | 4) Tweak away on `app.py`. 215 | 216 | 217 | ## Testing 218 | 219 | This hackpack comes with a full testing suite ready for nose. 220 | 221 |
222 | make test
223 | 
224 | 225 | It also ships with an easy-to-use base class for testing your 226 | [TwiML](http://www.twilio.com/docs/api/twiml). For example, testing a basic SMS 227 | response is only two lines of code: 228 | 229 | ```python 230 | import test_twilio 231 | 232 | class ExampleTest(test_twilio.TwiMLTest): 233 | response = self.sms("Test") 234 | self.assertTwiML(response) 235 | ``` 236 | 237 | You can also test your [Gather 238 | verbs](http://www.twilio.com/docs/api/twiml/gather) for voice apps very easily. 239 | 240 | ```python 241 | import test_twilio 242 | 243 | class ExampleTest(test_twilio.TwiMLTest): 244 | response = self.call(digits="1") 245 | self.assertTwiML(response) 246 | ``` 247 | 248 | 249 | ## Branches 250 | 251 | Two configurations are available in different branches: 252 | 253 | * master - Default dev mode with minimum possible code to get going. 254 | * production - Intended for live use with more code and dependencies appropriate 255 | to a production environment. To deploy this branch instead, adjust your 256 | procedure for the production branch: 257 | 258 |
259 | git checkout production
260 | git push heroku production:master
261 | 
262 | 263 | 264 | ## Meta 265 | 266 | * No warranty expressed or implied. Software is as is. Diggity. 267 | * [MIT License](http://www.opensource.org/licenses/mit-license.html) 268 | * Lovingly crafted by [Twilio New 269 | York](http://www.meetup.com/Twilio/New-York-NY/) 270 | 271 | 272 | ## Community Contributors 273 | 274 | Here we recognize crack members of the Twilio community who worked on this 275 | hackpack. 276 | 277 | * [Timothée Boucher](http://www.timotheeboucher.com/) - idea for production 278 | branch 279 | * [Oscar](http://labcoder.com/) - Bug fix for user input 280 | -------------------------------------------------------------------------------- /configure.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Hackpack Configure 3 | A script to configure your TwiML apps and Twilio phone numbers to use your 4 | hackpack's Heroku app. 5 | 6 | Usage: 7 | 8 | Auto-configure using your local_settings.py: 9 | python configure.py 10 | 11 | Deploy to new Twilio number and App Sid: 12 | python configure.py --new 13 | 14 | Deploy to specific App Sid: 15 | python configure.py --app APxxxxxxxxxxxxxx 16 | 17 | Deploy to specific Twilio number: 18 | python configure.py --number +15556667777 19 | 20 | Deploy to custom domain: 21 | python configure.py --domain example.com 22 | ''' 23 | 24 | from optparse import OptionParser 25 | import sys 26 | import subprocess 27 | import logging 28 | 29 | from twilio.rest import TwilioRestClient 30 | from twilio import TwilioRestException 31 | 32 | import local_settings 33 | 34 | 35 | class Configure(object): 36 | def __init__(self, account_sid=local_settings.TWILIO_ACCOUNT_SID, 37 | auth_token=local_settings.TWILIO_AUTH_TOKEN, 38 | app_sid=local_settings.TWILIO_APP_SID, 39 | phone_number=local_settings.TWILIO_CALLER_ID, 40 | voice_url='/voice', 41 | sms_url='/sms', 42 | host=None): 43 | self.account_sid = account_sid 44 | self.auth_token = auth_token 45 | self.app_sid = app_sid 46 | self.phone_number = phone_number 47 | self.host = host 48 | self.voice_url = voice_url 49 | self.sms_url = sms_url 50 | self.friendly_phone_number = None 51 | 52 | def start(self): 53 | logging.info("Configuring your Twilio hackpack...") 54 | logging.debug("Checking if credentials are set...") 55 | if not self.account_sid: 56 | raise ConfigurationError("ACCOUNT_SID is not set in " \ 57 | "local_settings.") 58 | if not self.auth_token: 59 | raise ConfigurationError("AUTH_TOKEN is not set in " \ 60 | "local_settings.") 61 | 62 | logging.debug("Creating Twilio client...") 63 | self.client = TwilioRestClient(self.account_sid, self.auth_token) 64 | 65 | logging.debug("Checking if host is set.") 66 | if not self.host: 67 | logging.debug("Hostname is not set...") 68 | self.host = self.getHerokuHostname() 69 | 70 | # Check if urls are set. 71 | logging.debug("Checking if all urls are set.") 72 | if "http://" not in self.voice_url: 73 | self.voice_url = self.host + self.voice_url 74 | logging.debug("Setting voice_url with host: %s" % self.voice_url) 75 | if "http://" not in self.sms_url: 76 | self.sms_url = self.host + self.sms_url 77 | logging.debug("Setting sms_url with host: %s" % self.sms_url) 78 | 79 | if self.configureHackpack(self.voice_url, self.sms_url, 80 | self.app_sid, self.phone_number): 81 | 82 | # Configure Heroku environment variables. 83 | self.setHerokuEnvironmentVariables( 84 | TWILIO_ACCOUNT_SID=self.account_sid, 85 | TWILIO_AUTH_TOKEN=self.auth_token, 86 | TWILIO_APP_SID=self.app_sid, 87 | TWILIO_CALLER_ID=self.phone_number) 88 | 89 | # Ensure local environment variables are set. 90 | self.printLocalEnvironmentVariableCommands( 91 | TWILIO_ACCOUNT_SID=self.account_sid, 92 | TWILIO_AUTH_TOKEN=self.auth_token, 93 | TWILIO_APP_SID=self.app_sid, 94 | TWILIO_CALLER_ID=self.phone_number) 95 | 96 | logging.info("Hackpack is now configured. Call %s to test!" 97 | % self.friendly_phone_number) 98 | else: 99 | logging.error("There was an error configuring your hackpack. " \ 100 | "Weak sauce.") 101 | 102 | def configureHackpack(self, voice_url, sms_url, app_sid, 103 | phone_number, *args): 104 | 105 | # Check if app sid is configured and available. 106 | if not app_sid: 107 | app = self.createNewTwiMLApp(voice_url, sms_url) 108 | else: 109 | app = self.setAppRequestUrls(app_sid, voice_url, sms_url) 110 | 111 | # Check if phone_number is set. 112 | if not phone_number: 113 | number = self.purchasePhoneNumber() 114 | else: 115 | number = self.retrievePhoneNumber(phone_number) 116 | 117 | # Configure phone number to use App Sid. 118 | logging.info("Setting %s to use application sid: %s" % 119 | (number.friendly_name, app.sid)) 120 | try: 121 | self.client.phone_numbers.update(number.sid, 122 | voice_application_sid=app.sid, 123 | sms_application_sid=app.sid) 124 | logging.debug("Number set.") 125 | except TwilioRestException, e: 126 | raise ConfigurationError("An error occurred setting the " \ 127 | "application sid for %s: %s" % (number.friendly_name, 128 | e)) 129 | 130 | # We're done! 131 | if number: 132 | return number 133 | else: 134 | raise ConfigurationError("An unknown error occurred configuring " \ 135 | "request urls for this hackpack.") 136 | 137 | def createNewTwiMLApp(self, voice_url, sms_url): 138 | logging.debug("Asking user to create new app sid...") 139 | i = 0 140 | while True: 141 | i = i + 1 142 | choice = raw_input("Your APP_SID is not configured in your " \ 143 | "local_settings. Create a new one? [y/n]").lower() 144 | if choice == "y": 145 | try: 146 | logging.info("Creating new application...") 147 | app = self.client.applications.create(voice_url=voice_url, 148 | sms_url=sms_url, 149 | friendly_name="Hackpack for Heroku and Flask") 150 | break 151 | except TwilioRestException, e: 152 | raise ConfigurationError("Your Twilio app couldn't " \ 153 | "be created: %s" % e) 154 | elif choice == "n" or i >= 3: 155 | raise ConfigurationError("Your APP_SID setting must be " \ 156 | "set in local_settings.") 157 | else: 158 | logging.error("Please choose yes or no with a 'y' or 'n'") 159 | if app: 160 | logging.info("Application created: %s" % app.sid) 161 | self.app_sid = app.sid 162 | return app 163 | else: 164 | raise ConfigurationError("There was an unknown error " \ 165 | "creating your TwiML application.") 166 | 167 | def setAppRequestUrls(self, app_sid, voice_url, sms_url): 168 | logging.info("Setting request urls for application sid: %s" \ 169 | % app_sid) 170 | 171 | try: 172 | app = self.client.applications.update(app_sid, voice_url=voice_url, 173 | sms_url=sms_url, 174 | friendly_name="Hackpack for Heroku and Flask") 175 | except TwilioRestException, e: 176 | if "HTTP ERROR 404" in str(e): 177 | raise ConfigurationError("This application sid was not " \ 178 | "found: %s" % app_sid) 179 | else: 180 | raise ConfigurationError("An error setting the request URLs " \ 181 | "occured: %s" % e) 182 | if app: 183 | logging.debug("Updated application sid: %s " % app.sid) 184 | return app 185 | else: 186 | raise ConfigurationError("An unknown error occuring "\ 187 | "configuring request URLs for app sid.") 188 | 189 | def retrievePhoneNumber(self, phone_number): 190 | logging.debug("Retrieving phone number: %s" % phone_number) 191 | try: 192 | logging.debug("Getting sid for phone number: %s" % phone_number) 193 | number = self.client.phone_numbers.list( 194 | phone_number=phone_number) 195 | except TwilioRestException, e: 196 | raise ConfigurationError("An error setting the request URLs " \ 197 | "occured: %s" % e) 198 | if number: 199 | logging.debug("Retrieved sid: %s" % number[0].sid) 200 | self.friendly_phone_number = number[0].friendly_name 201 | return number[0] 202 | else: 203 | raise ConfigurationError("An unknown error occurred retrieving " \ 204 | "number: %s" % phone_number) 205 | 206 | def purchasePhoneNumber(self): 207 | logging.debug("Asking user to purchase phone number...") 208 | 209 | i = 0 210 | while True: 211 | i = i + 1 212 | # Find number to purchase 213 | choice = raw_input("Your CALLER_ID is not configured in your " \ 214 | "local_settings. Purchase a new one? [y/n]").lower() 215 | if choice == "y": 216 | break 217 | elif choice == "n" or i >= 3: 218 | raise ConfigurationError("To configure this " \ 219 | "hackpack CALLER_ID must set in local_settings or " \ 220 | "a phone number must be purchased.") 221 | else: 222 | logging.error("Please choose yes or no with a 'y' or 'n'") 223 | 224 | logging.debug("Confirming purchase...") 225 | i = 0 226 | while True: 227 | i = i + 1 228 | # Confirm phone number purchase. 229 | choice = raw_input("Are you sure you want to purchase? " \ 230 | "Your Twilio account will be charged $1. [y/n]").lower() 231 | if choice == "y": 232 | try: 233 | logging.debug("Purchasing phone number...") 234 | number = self.client.phone_numbers.purchase( 235 | area_code="646") 236 | logging.debug("Phone number purchased: %s" % 237 | number.friendly_name) 238 | break 239 | except TwilioRestException, e: 240 | raise ConfigurationError("Your Twilio app couldn't " \ 241 | "be created: %s" % e) 242 | elif choice == "n" or i >= 3: 243 | raise ConfigurationError("To configure this " \ 244 | "hackpack CALLER_ID must set in local_settings or " \ 245 | "a phone number must be purchased.") 246 | else: 247 | logging.error("Please choose yes or no with a 'y' or 'n'") 248 | 249 | # Return number or error out. 250 | if number: 251 | logging.debug("Returning phone number: %s " % number.friendly_name) 252 | self.phone_number = number.phone_number 253 | self.friendly_phone_number = number.friendly_name 254 | return number 255 | else: 256 | raise ConfigurationError("There was an unknown error purchasing " \ 257 | "your phone number.") 258 | 259 | def getHerokuHostname(self, git_config_path='./.git/config'): 260 | logging.debug("Getting hostname from git configuration file: %s" \ 261 | % git_config_path) 262 | # Load git configuration 263 | try: 264 | logging.debug("Loading git config...") 265 | git_config = file(git_config_path).readlines() 266 | except IOError, e: 267 | raise ConfigurationError("Could not find .git config. Does it " \ 268 | "still exist? Failed path: %s" % e) 269 | 270 | logging.debug("Finding Heroku remote in git configuration...") 271 | subdomain = None 272 | for line in git_config: 273 | if "git@heroku.com" in line: 274 | s = line.split(":") 275 | subdomain = s[1].replace('.git', '') 276 | logging.debug("Heroku remote found: %s" % subdomain) 277 | 278 | if subdomain: 279 | host = "http://%s.herokuapp.com" % subdomain.strip() 280 | logging.debug("Returning full host: %s" % host) 281 | return host 282 | else: 283 | raise ConfigurationError("Could not find Heroku remote in " \ 284 | "your .git config. Have you created the Heroku app?") 285 | 286 | def printLocalEnvironmentVariableCommands(self, **kwargs): 287 | logging.info("Copy/paste these commands to set your local " \ 288 | "environment to use this hackpack...") 289 | print "\n" 290 | for k, v in kwargs.iteritems(): 291 | if v: 292 | print "export %s=%s" % (k, v) 293 | print "\n" 294 | 295 | def setHerokuEnvironmentVariables(self, **kwargs): 296 | logging.info("Setting Heroku environment variables...") 297 | envvars = ["%s=%s" % (k, v) for k, v in kwargs.iteritems() if v] 298 | envvars.insert(0, "heroku") 299 | envvars.insert(1, "config:add") 300 | return subprocess.call(envvars) 301 | 302 | 303 | class ConfigurationError(Exception): 304 | def __init__(self, message): 305 | #Exception.__init__(self, message) 306 | logging.error(message) 307 | 308 | 309 | # Logging configuration 310 | logging.basicConfig(level=logging.INFO, format='%(message)s') 311 | 312 | # Parser configuration 313 | usage = "Twilio Hackpack Configurator - an easy way to configure " \ 314 | "configure your hackpack!\n%prog [options] arg1 arg2" 315 | parser = OptionParser(usage=usage, version="Twilio Hackpack Configurator 1.0") 316 | parser.add_option("-S", "--account_sid", default=None, 317 | help="Use a specific Twilio ACCOUNT_SID.") 318 | parser.add_option("-K", "--auth_token", default=None, 319 | help="Use a specific Twilio AUTH_TOKEN.") 320 | parser.add_option("-n", "--new", default=False, action="store_true", 321 | help="Purchase new Twilio phone number and configure app to use " \ 322 | "your hackpack.") 323 | parser.add_option("-N", "--new_app", default=False, action="store_true", 324 | help="Create a new TwiML application sid to use for your " \ 325 | "hackpack.") 326 | parser.add_option("-a", "--app_sid", default=None, 327 | help="Configure specific AppSid to use your hackpack.") 328 | parser.add_option("-#", "--phone-number", default=None, 329 | help="Configure specific Twilio number to use your hackpack.") 330 | parser.add_option("-v", "--voice_url", default=None, 331 | help="Set the route for your Voice Request URL: (e.g. '/voice').") 332 | parser.add_option("-s", "--sms_url", default=None, 333 | help="Set the route for your SMS Request URL: (e.g. '/sms').") 334 | parser.add_option("-d", "--domain", default=None, 335 | help="Set a custom domain.") 336 | parser.add_option("-D", "--debug", default=False, 337 | action="store_true", help="Turn on debug output.") 338 | 339 | 340 | def main(): 341 | (options, args) = parser.parse_args() 342 | 343 | # Configurator configuration :) 344 | configure = Configure() 345 | 346 | # Options tree 347 | if options.account_sid: 348 | configure.account_sid = options.account_sid 349 | if options.auth_token: 350 | configure.auth_token = options.auth_token 351 | if options.new: 352 | configure.phone_number = None 353 | if options.new_app: 354 | configure.app_sid = None 355 | if options.app_sid: 356 | configure.app_sid = options.app_sid 357 | if options.phone_number: 358 | configure.phone_number = options.phone_number 359 | if options.voice_url: 360 | configure.voice_url = options.voice_url 361 | if options.sms_url: 362 | configure.sms_url = options.sms_url 363 | if options.domain: 364 | configure.host = options.domain 365 | if options.debug: 366 | logging.basicConfig(level=logging.DEBUG, 367 | format='%(levelname)s - %(message)s') 368 | 369 | configure.start() 370 | 371 | if __name__ == "__main__": 372 | main() 373 | -------------------------------------------------------------------------------- /tests/test_configure.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mock import Mock 3 | from mock import patch 4 | import subprocess 5 | 6 | from twilio.rest import TwilioRestClient 7 | 8 | from .context import configure 9 | 10 | 11 | class ConfigureTest(unittest.TestCase): 12 | def setUp(self): 13 | self.configure = configure.Configure( 14 | account_sid="ACxxxxx", 15 | auth_token="yyyyyyyy", 16 | phone_number="+15555555555", 17 | app_sid="APzzzzzzzzz") 18 | self.configure.client = TwilioRestClient(self.configure.account_sid, 19 | self.configure.auth_token) 20 | 21 | 22 | class TwilioTest(ConfigureTest): 23 | @patch('twilio.rest.resources.Applications') 24 | @patch('twilio.rest.resources.Application') 25 | def test_createNewTwiMLApp(self, MockApp, MockApps): 26 | # Mock the Applications resource and its create method. 27 | self.configure.client.applications = MockApps.return_value 28 | self.configure.client.applications.create.return_value = \ 29 | MockApp.return_value 30 | 31 | # Mock our input. 32 | configure.raw_input = lambda _: 'y' 33 | 34 | # Test 35 | self.configure.createNewTwiMLApp(self.configure.voice_url, 36 | self.configure.sms_url) 37 | 38 | # Assert 39 | self.configure.client.applications.create.assert_called_once_with( 40 | voice_url=self.configure.voice_url, 41 | sms_url=self.configure.sms_url, 42 | friendly_name="Hackpack for Heroku and Flask") 43 | 44 | @patch('twilio.rest.resources.Applications') 45 | @patch('twilio.rest.resources.Application') 46 | def test_createNewTwiMLAppNegativeInput(self, MockApp, MockApps): 47 | # Mock the Applications resource and its create method. 48 | self.configure.client.applications = MockApps.return_value 49 | self.configure.client.applications.create.return_value = \ 50 | MockApp.return_value 51 | 52 | # Mock our input . 53 | configure.raw_input = lambda _: 'n' 54 | 55 | # Test / Assert 56 | self.assertRaises(configure.ConfigurationError, 57 | self.configure.createNewTwiMLApp, 58 | self.configure.voice_url, self.configure.sms_url) 59 | 60 | @patch('twilio.rest.resources.Applications') 61 | @patch('twilio.rest.resources.Application') 62 | def test_setAppSidRequestUrls(self, MockApp, MockApps): 63 | # Mock the Applications resource and its update method. 64 | self.configure.client.applications = MockApps.return_value 65 | self.configure.client.applications.update.return_value = \ 66 | MockApp.return_value 67 | 68 | # Test 69 | self.configure.setAppRequestUrls(self.configure.app_sid, 70 | self.configure.voice_url, 71 | self.configure.sms_url) 72 | 73 | # Assert 74 | self.configure.client.applications.update.assert_called_once_with( 75 | self.configure.app_sid, 76 | voice_url=self.configure.voice_url, 77 | sms_url=self.configure.sms_url, 78 | friendly_name='Hackpack for Heroku and Flask') 79 | 80 | @patch('twilio.rest.resources.PhoneNumbers') 81 | @patch('twilio.rest.resources.PhoneNumber') 82 | def test_retrievePhoneNumber(self, MockPhoneNumber, MockPhoneNumbers): 83 | # Mock the PhoneNumbers resource and its list method. 84 | mock_phone_number = MockPhoneNumber.return_value 85 | mock_phone_number.phone_number = self.configure.phone_number 86 | self.configure.client.phone_numbers = MockPhoneNumbers.return_value 87 | self.configure.client.phone_numbers.list.return_value = \ 88 | [mock_phone_number] 89 | 90 | # Test 91 | self.configure.retrievePhoneNumber(self.configure.phone_number) 92 | 93 | # Assert 94 | self.configure.client.phone_numbers.list.assert_called_once_with( 95 | phone_number=self.configure.phone_number) 96 | 97 | @patch('twilio.rest.resources.PhoneNumbers') 98 | @patch('twilio.rest.resources.PhoneNumber') 99 | def test_purchasePhoneNumber(self, MockPhoneNumber, MockPhoneNumbers): 100 | # Mock the PhoneNumbers resource and its search and purchase methods 101 | mock_phone_number = MockPhoneNumber.return_value 102 | mock_phone_number.phone_number = self.configure.phone_number 103 | self.configure.client.phone_numbers = MockPhoneNumbers.return_value 104 | self.configure.client.phone_numbers.purchase = \ 105 | mock_phone_number 106 | 107 | # Mock our input. 108 | configure.raw_input = lambda _: 'y' 109 | 110 | # Test 111 | self.configure.purchasePhoneNumber() 112 | 113 | # Assert 114 | self.configure.client.phone_numbers.purchase.assert_called_once_with( 115 | area_code="646") 116 | 117 | @patch('twilio.rest.resources.PhoneNumbers') 118 | @patch('twilio.rest.resources.PhoneNumber') 119 | def test_purchasePhoneNumberNegativeInput(self, MockPhoneNumbers, 120 | MockPhoneNumber): 121 | # Mock the PhoneNumbers resource and its search and purchase methods 122 | mock_phone_number = MockPhoneNumber.return_value 123 | mock_phone_number.phone_number = self.configure.phone_number 124 | self.configure.client.phone_numbers = MockPhoneNumbers.return_value 125 | self.configure.client.phone_numbers.purchase = \ 126 | mock_phone_number 127 | 128 | # Mock our input. 129 | configure.raw_input = lambda _: 'n' 130 | 131 | # Test / Assert 132 | self.assertRaises(configure.ConfigurationError, 133 | self.configure.purchasePhoneNumber) 134 | 135 | @patch('twilio.rest.resources.Applications') 136 | @patch('twilio.rest.resources.Application') 137 | @patch('twilio.rest.resources.PhoneNumbers') 138 | @patch('twilio.rest.resources.PhoneNumber') 139 | def test_configure(self, MockPhoneNumber, MockPhoneNumbers, MockApp, 140 | MockApps): 141 | # Mock the Applications resource and its update method. 142 | mock_app = MockApp.return_value 143 | mock_app.sid = self.configure.app_sid 144 | self.configure.client.applications = MockApps.return_value 145 | self.configure.client.applications.update.return_value = \ 146 | mock_app 147 | 148 | # Mock the PhoneNumbers resource and its list method. 149 | mock_phone_number = MockPhoneNumber.return_value 150 | mock_phone_number.sid = "PN123" 151 | mock_phone_number.friendly_name = "(555) 555-5555" 152 | mock_phone_number.phone_number = self.configure.phone_number 153 | self.configure.client.phone_numbers = MockPhoneNumbers.return_value 154 | self.configure.client.phone_numbers.list.return_value = \ 155 | [mock_phone_number] 156 | 157 | # Test 158 | self.configure.configureHackpack(self.configure.voice_url, 159 | self.configure.sms_url, 160 | self.configure.app_sid, 161 | self.configure.phone_number) 162 | 163 | # Assert 164 | self.configure.client.applications.update.assert_called_once_with( 165 | self.configure.app_sid, 166 | voice_url=self.configure.voice_url, 167 | sms_url=self.configure.sms_url, 168 | friendly_name='Hackpack for Heroku and Flask') 169 | 170 | self.configure.client.phone_numbers.update.assert_called_once_with( 171 | "PN123", 172 | voice_application_sid=self.configure.app_sid, 173 | sms_application_sid=self.configure.app_sid) 174 | 175 | @patch('twilio.rest.resources.Applications') 176 | @patch('twilio.rest.resources.Application') 177 | @patch('twilio.rest.resources.PhoneNumbers') 178 | @patch('twilio.rest.resources.PhoneNumber') 179 | def test_configureNoApp(self, MockPhoneNumber, MockPhoneNumbers, MockApp, 180 | MockApps): 181 | # Mock the Applications resource and its update method. 182 | mock_app = MockApp.return_value 183 | mock_app.sid = self.configure.app_sid 184 | self.configure.client.applications = MockApps.return_value 185 | self.configure.client.applications.create.return_value = \ 186 | mock_app 187 | 188 | # Mock the PhoneNumbers resource and its list method. 189 | mock_phone_number = MockPhoneNumber.return_value 190 | mock_phone_number.sid = "PN123" 191 | mock_phone_number.friendly_name = "(555) 555-5555" 192 | mock_phone_number.phone_number = self.configure.phone_number 193 | self.configure.client.phone_numbers = MockPhoneNumbers.return_value 194 | self.configure.client.phone_numbers.list.return_value = \ 195 | [mock_phone_number] 196 | 197 | # Set AppSid to None 198 | self.configure.app_sid = None 199 | 200 | # Mock our input. 201 | configure.raw_input = lambda _: 'y' 202 | 203 | # Test 204 | self.configure.configureHackpack(self.configure.voice_url, 205 | self.configure.sms_url, 206 | self.configure.app_sid, 207 | self.configure.phone_number) 208 | 209 | # Assert 210 | self.configure.client.applications.create.assert_called_once_with( 211 | voice_url=self.configure.voice_url, 212 | sms_url=self.configure.sms_url, 213 | friendly_name="Hackpack for Heroku and Flask") 214 | 215 | self.configure.client.phone_numbers.update.assert_called_once_with( 216 | "PN123", 217 | voice_application_sid=mock_app.sid, 218 | sms_application_sid=mock_app.sid) 219 | 220 | @patch('twilio.rest.resources.Applications') 221 | @patch('twilio.rest.resources.Application') 222 | @patch('twilio.rest.resources.PhoneNumbers') 223 | @patch('twilio.rest.resources.PhoneNumber') 224 | def test_configureNoPhoneNumber(self, MockPhoneNumber, MockPhoneNumbers, 225 | MockApp, MockApps): 226 | # Mock the Applications resource and its update method. 227 | mock_app = MockApp.return_value 228 | mock_app.sid = self.configure.app_sid 229 | self.configure.client.applications = MockApps.return_value 230 | self.configure.client.applications.update.return_value = \ 231 | mock_app 232 | 233 | # Mock the PhoneNumbers resource and its list method. 234 | mock_phone_number = MockPhoneNumber.return_value 235 | mock_phone_number.sid = "PN123" 236 | mock_phone_number.friendly_name = "(555) 555-5555" 237 | mock_phone_number.phone_number = self.configure.phone_number 238 | self.configure.client.phone_numbers = MockPhoneNumbers.return_value 239 | self.configure.client.phone_numbers.purchase.return_value = \ 240 | mock_phone_number 241 | 242 | # Set AppSid to None 243 | self.configure.phone_number = None 244 | 245 | # Mock our input. 246 | configure.raw_input = lambda _: 'y' 247 | 248 | # Test 249 | self.configure.configureHackpack(self.configure.voice_url, 250 | self.configure.sms_url, 251 | self.configure.app_sid, 252 | self.configure.phone_number) 253 | 254 | # Assert 255 | self.configure.client.applications.update.assert_called_once_with( 256 | self.configure.app_sid, 257 | voice_url=self.configure.voice_url, 258 | sms_url=self.configure.sms_url, 259 | friendly_name='Hackpack for Heroku and Flask') 260 | 261 | self.configure.client.phone_numbers.update.assert_called_once_with( 262 | "PN123", 263 | voice_application_sid=self.configure.app_sid, 264 | sms_application_sid=self.configure.app_sid) 265 | 266 | @patch.object(subprocess, 'call') 267 | @patch.object(configure.Configure, 'configureHackpack') 268 | def test_start(self, mock_configureHackpack, mock_call): 269 | mock_call.return_value = None 270 | self.configure.host = 'http://look-here-snacky-11211.herokuapp.com' 271 | self.configure.start() 272 | mock_configureHackpack.assert_called_once_with( 273 | 'http://look-here-snacky-11211.herokuapp.com/voice', 274 | 'http://look-here-snacky-11211.herokuapp.com/sms', 275 | self.configure.app_sid, 276 | self.configure.phone_number) 277 | 278 | @patch.object(subprocess, 'call') 279 | @patch.object(configure.Configure, 'configureHackpack') 280 | @patch.object(configure.Configure, 'getHerokuHostname') 281 | def test_startWithoutHostname(self, mock_getHerokuHostname, 282 | mock_configureHackpack, mock_call): 283 | mock_call.return_value = None 284 | mock_getHerokuHostname.return_value = \ 285 | 'http://look-here-snacky-11211.herokuapp.com' 286 | self.configure.start() 287 | mock_configureHackpack.assert_called_once_with( 288 | 'http://look-here-snacky-11211.herokuapp.com/voice', 289 | 'http://look-here-snacky-11211.herokuapp.com/sms', 290 | self.configure.app_sid, 291 | self.configure.phone_number) 292 | 293 | 294 | class HerokuTest(ConfigureTest): 295 | def test_getHerokuHostname(self): 296 | test = self.configure.getHerokuHostname( 297 | git_config_path='./tests/test_assets/good_git_config') 298 | self.assertEquals(test, 'http://look-here-snacky-11211.herokuapp.com') 299 | 300 | def test_getHerokuHostnameNoSuchFile(self): 301 | self.assertRaises(configure.ConfigurationError, 302 | self.configure.getHerokuHostname, 303 | git_config_path='/tmp') 304 | 305 | def test_getHerokuHostnameNoHerokuRemote(self): 306 | self.assertRaises(configure.ConfigurationError, 307 | self.configure.getHerokuHostname, 308 | git_config_path='./tests/test_assets/bad_git_config') 309 | 310 | @patch.object(subprocess, 'call') 311 | def test_setHerokuEnvironmentVariables(self, mock_call): 312 | mock_call.return_value = None 313 | self.configure.setHerokuEnvironmentVariables( 314 | TWILIO_ACCOUNT_SID=self.configure.account_sid, 315 | TWILIO_AUTH_TOKEN=self.configure.auth_token, 316 | TWILIO_APP_SID=self.configure.app_sid, 317 | TWILIO_CALLER_ID=self.configure.phone_number) 318 | mock_call.assert_called_once_with(["heroku", "config:add", 319 | '%s=%s' % ('TWILIO_ACCOUNT_SID', self.configure.account_sid), 320 | '%s=%s' % ('TWILIO_CALLER_ID', self.configure.phone_number), 321 | '%s=%s' % ('TWILIO_AUTH_TOKEN', self.configure.auth_token), 322 | '%s=%s' % ('TWILIO_APP_SID', self.configure.app_sid)]) 323 | 324 | 325 | class MiscellaneousTest(unittest.TestCase): 326 | def test_configureWithoutAccountSid(self): 327 | test = configure.Configure(account_sid=None, auth_token=None, 328 | phone_number=None, app_sid=None) 329 | self.assertRaises(configure.ConfigurationError, 330 | test.start) 331 | 332 | def test_configureWithoutAuthToken(self): 333 | test = configure.Configure(account_sid='ACxxxxxxx', auth_token=None, 334 | phone_number=None, app_sid=None) 335 | self.assertRaises(configure.ConfigurationError, 336 | test.start) 337 | 338 | 339 | class InputTest(ConfigureTest): 340 | @patch('twilio.rest.resources.Applications') 341 | @patch('twilio.rest.resources.Application') 342 | def test_createNewTwiMLAppWtfInput(self, MockApp, MockApps): 343 | # Mock the Applications resource and its create method. 344 | self.configure.client.applications = MockApps.return_value 345 | self.configure.client.applications.create.return_value = \ 346 | MockApp.return_value 347 | 348 | # Mock our input 349 | configure.raw_input = Mock() 350 | configure.raw_input.return_value = 'wtf' 351 | 352 | # Test / Assert 353 | self.assertRaises(configure.ConfigurationError, 354 | self.configure.createNewTwiMLApp, self.configure.voice_url, 355 | self.configure.sms_url) 356 | self.assertTrue(configure.raw_input.call_count == 3, "Prompt did " \ 357 | "not appear three times, instead: %i" % 358 | configure.raw_input.call_count) 359 | self.assertFalse(self.configure.client.applications.create.called, 360 | "Unexpected request to create AppSid made.") 361 | 362 | @patch('twilio.rest.resources.PhoneNumbers') 363 | @patch('twilio.rest.resources.PhoneNumber') 364 | def test_purchasePhoneNumberWtfInput(self, MockPhoneNumbers, 365 | MockPhoneNumber): 366 | # Mock the PhoneNumbers resource and its search and purchase methods 367 | mock_phone_number = MockPhoneNumber.return_value 368 | mock_phone_number.phone_number = self.configure.phone_number 369 | self.configure.client.phone_numbers = MockPhoneNumbers.return_value 370 | self.configure.client.phone_numbers.purchase = \ 371 | mock_phone_number 372 | 373 | # Mock our input. 374 | configure.raw_input = Mock() 375 | configure.raw_input.return_value = 'wtf' 376 | 377 | # Test / Assert 378 | self.assertRaises(configure.ConfigurationError, 379 | self.configure.purchasePhoneNumber) 380 | self.assertTrue(configure.raw_input.call_count == 3, "Prompt did " \ 381 | "not appear three times, instead: %i" % 382 | configure.raw_input.call_count) 383 | self.assertFalse(self.configure.client.phone_numbers.purchase.called, 384 | "Unexpected request to create AppSid made.") 385 | 386 | @patch('twilio.rest.resources.PhoneNumbers') 387 | @patch('twilio.rest.resources.PhoneNumber') 388 | def test_purchasePhoneNumberWtfInputConfirm(self, 389 | MockPhoneNumbers, MockPhoneNumber): 390 | # Mock the PhoneNumbers resource and its search and purchase methods 391 | mock_phone_number = MockPhoneNumber.return_value 392 | mock_phone_number.phone_number = self.configure.phone_number 393 | self.configure.client.phone_numbers = MockPhoneNumbers.return_value 394 | self.configure.client.phone_numbers.purchase = \ 395 | mock_phone_number 396 | 397 | # Mock our input. 398 | configure.raw_input = Mock() 399 | configure.raw_input.side_effect = ['y', 'wtf', 'wtf', 'wtf'] 400 | 401 | # Test / Assert 402 | self.assertRaises(configure.ConfigurationError, 403 | self.configure.purchasePhoneNumber) 404 | self.assertTrue(configure.raw_input.call_count == 4, "Prompt did " \ 405 | "not appear three times, instead: %i" % 406 | configure.raw_input.call_count) 407 | self.assertFalse(self.configure.client.phone_numbers.purchase.called, 408 | "Unexpectedly requested phone number purchase.") 409 | --------------------------------------------------------------------------------