├── 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 |
59 | Call
60 |
61 |
62 |
63 | Hangup
64 |
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 |
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 | []
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 |
--------------------------------------------------------------------------------