Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Calls resource #4

Merged
merged 25 commits into from
Apr 1, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8c0dd00
Add validation for make_call fields
Mar 23, 2015
62b0ec2
Add support for ApplicationSid parameter verification
Mar 24, 2015
32f185e
Make format_json convert CamelCase to snake_case
Mar 24, 2015
5427953
Add session manager to vumi worker
Mar 25, 2015
dd50ff2
Add valid response for the making calls endpoint
Mar 25, 2015
1384b0e
Fix processing fields method
Mar 25, 2015
0b3ce9f
Add TwiML server to tests for worker to grab TwiML from
Mar 25, 2015
16aab92
Cleanup validation methods
Mar 25, 2015
0ea6c20
Change to correct response for root API request
Mar 25, 2015
ab45d34
Change tests to test for correct root response
Mar 25, 2015
9f3b7e4
Correct formatting for uris in response
Mar 25, 2015
91aedff
Update tests for correct URIs in response
Mar 25, 2015
dd931e4
Add mock requirement for tests
Mar 25, 2015
256abe8
Fix datetime formatting
Mar 25, 2015
7e9653e
Add tests for defaulting values of creating call
Mar 25, 2015
95d9c6c
Add tests for required parameters
Mar 26, 2015
8285595
flake8 cleanup
Mar 26, 2015
7ed9e04
Add tests for optional parameters
Mar 26, 2015
a8612a9
Add test for TwiMLServer
Mar 26, 2015
a10403b
Add assertRegexpMatches function for python 2.6
Mar 26, 2015
5c5e68a
Change to using UTC over local time
Mar 31, 2015
bd7c37d
Move camel_to_snake and convert_dict_keys functions to standalone fun…
Mar 31, 2015
12a4aaa
Separate out asserting missing parameter to helper method
Mar 31, 2015
529e318
Change to using popitem for formatting json
Mar 31, 2015
15567c8
flake8 cleanup
Mar 31, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements-dev.pip
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mock
twilio
twisted
treq
315 changes: 301 additions & 14 deletions vumi_twilio_api/tests/test_twilio_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from datetime import datetime
import json
from klein import Klein
from mock import Mock
import re
import treq
from twilio.rest import TwilioRestClient
from twilio.rest.exceptions import TwilioRestException
from twisted.internet.defer import inlineCallbacks
from twisted.internet.threads import deferToThread
from twisted.trial.unittest import TestCase
from vumi.application.tests.helpers import ApplicationHelper
from vumi.tests.helpers import VumiTestCase
Expand All @@ -9,22 +16,98 @@
from vumi_twilio_api.twilio_api import TwilioAPIServer, TwilioAPIWorker


class TwiMLServer(object):
app = Klein()

def __init__(self, responses={}):
self._responses = responses.copy()

def add_response(self, filename, response):
self._responses[filename] = response

@app.route('/<string:filename>')
def get_twiml(self, request, filename):
request.setHeader('Content-Type', 'application/xml')
return ET.tostring(self._responses[filename])


class TestTwiMLServer(VumiTestCase):

@inlineCallbacks
def setUp(self):
self.app_helper = self.add_helper(ApplicationHelper(
TwilioAPIWorker, transport_type='voice'))
self.worker = yield self.app_helper.get_application({
'web_path': '/api',
'web_port': 8080,
'api_version': 'v1',
})

self.twiml_server = TwiMLServer()
self.twiml_connection = self.worker.start_web_resources([
(self.twiml_server.app.resource(), '/twiml')], 8081)
self.add_cleanup(self.twiml_connection.loseConnection)
addr = self.twiml_connection.getHost()
self.url = 'http://%s:%s' % (addr.host, addr.port)

def _server_request(self, path='', method='GET', data={}):
url = '%s/twiml/%s' % (self.url, path)
return treq.request(method, url, persistent=False, data=data)

@inlineCallbacks
def test_getting_response(self):
response = ET.Element('Foo')
bar = ET.SubElement(response, 'Bar')
self.twiml_server.add_response('example.xml', response)

request = yield self._server_request('example.xml')
request = yield request.content()
root = ET.fromstring(request)
self.assertEqual(root.tag, response.tag)
[child] = list(root)
self.assertEqual(child.tag, bar.tag)


class TestTwilioAPIServer(VumiTestCase):

@inlineCallbacks
def setUp(self):
self.app_helper = self.add_helper(ApplicationHelper(
TwilioAPIWorker, transport_type='voice'))
self.worker = yield self.app_helper.get_application({
'web_path': '/api/v1',
'web_port': 8080
'web_path': '/api',
'web_port': 8080,
'api_version': 'v1',
})
addr = self.worker.webserver.getHost()
self.url = 'http://%s:%s%s' % (addr.host, addr.port, '/api')
self.client = TwilioRestClient(
'test_account', 'test_token', base=self.url, version='v1')
self.twiml_server = TwiMLServer()
self.twiml_connection = self.worker.start_web_resources([
(self.twiml_server.app.resource(), '/twiml')], 8081)
self.add_cleanup(self.twiml_connection.loseConnection)

def _server_request(self, path=''):
def _server_request(self, path='', method='GET', data={}):
url = '%s/v1/%s' % (self.url, path)
return treq.get(url, persistent=False)
return treq.request(method, url, persistent=False, data=data)

def _twilio_client_create_call(self, filename, *args, **kwargs):
addr = self.twiml_connection.getHost()
url = 'http://%s:%s%s%s' % (addr.host, addr.port, '/twiml/', filename)
return deferToThread(
self.client.calls.create, *args, url=url, **kwargs)

def assertRegexpMatches(self, text, regexp, msg=None):
self.assertTrue(re.search(regexp, text), msg=msg)

@inlineCallbacks
def assert_parameter_missing(self, url, method='GET', error={}, data={}):
response = yield self._server_request(
url, method=method, data=data)
self.assertEqual(response.code, 400)
response = yield response.json()
self.assertEqual(response, error)

@inlineCallbacks
def test_root_default(self):
Expand All @@ -36,7 +119,18 @@ def test_root_default(self):
content = yield response.content()
root = ET.fromstring(content)
self.assertEqual(root.tag, "TwilioResponse")
self.assertEqual(list(root), [])
[version] = list(root)
self.assertEqual(version.tag, 'Version')
[name, subresourceuris, uri] = sorted(
list(version), key=lambda i: i.tag)
self.assertEqual(name.tag, 'Name')
self.assertEqual(name.text, 'v1')
self.assertEqual(uri.tag, 'Uri')
self.assertEqual(uri.text, '/v1')
self.assertEqual(subresourceuris.tag, 'SubresourceUris')
[accounts] = sorted(list(subresourceuris), key=lambda i: i.tag)
self.assertEqual(accounts.tag, 'Accounts')
self.assertEqual(accounts.text, '/v1/Accounts')

@inlineCallbacks
def test_root_xml(self):
Expand All @@ -48,7 +142,18 @@ def test_root_xml(self):
content = yield response.content()
root = ET.fromstring(content)
self.assertEqual(root.tag, "TwilioResponse")
self.assertEqual(list(root), [])
[version] = list(root)
self.assertEqual(version.tag, 'Version')
[name, subresourceuris, uri] = sorted(
list(version), key=lambda i: i.tag)
self.assertEqual(name.tag, 'Name')
self.assertEqual(name.text, 'v1')
self.assertEqual(uri.tag, 'Uri')
self.assertEqual(uri.text, '/v1.xml')
self.assertEqual(subresourceuris.tag, 'SubresourceUris')
[accounts] = sorted(list(subresourceuris), key=lambda i: i.tag)
self.assertEqual(accounts.tag, 'Accounts')
self.assertEqual(accounts.text, '/v1/Accounts.xml')

@inlineCallbacks
def test_root_json(self):
Expand All @@ -58,7 +163,13 @@ def test_root_json(self):
['application/json'])
self.assertEqual(response.code, 200)
content = yield response.json()
self.assertEqual(content, {})
self.assertEqual(content, {
'name': 'v1',
'uri': '/v1.json',
'subresource_uris': {
'accounts': '/v1/Accounts.json'
}
})

@inlineCallbacks
def test_root_invalid_format(self):
Expand All @@ -69,13 +180,178 @@ def test_root_invalid_format(self):
self.assertEqual(response.code, 400)
content = yield response.content()
root = ET.fromstring(content)
[error_message, error_type] = sorted(root, key=lambda c: c.tag)
[error] = list(root)
[error_message, error_type] = sorted(error, key=lambda c: c.tag)
self.assertEqual(error_message.tag, 'error_message')
self.assertEqual(
error_message.text, "'foo' is not a valid request format")
self.assertEqual(error_type.tag, 'error_type')
self.assertEqual(error_type.text, 'UsageError')

@inlineCallbacks
def test_make_call_sid(self):
res = self.worker.server._get_sid()
self.assertTrue(isinstance(res, basestring))
self.assertTrue('-' not in res)

self.worker.server._get_sid = Mock(return_value='ab12cd34')
call = yield self._twilio_client_create_call(
'default.xml', from_='+12345', to='+54321')
self.assertEqual(call.sid, 'ab12cd34')
self.assertEqual(
call.subresource_uris['notifications'],
'/v1/Accounts/test_account/Calls/ab12cd34/Notifications.json')
self.assertEqual(
call.subresource_uris['recordings'],
'/v1/Accounts/test_account/Calls/ab12cd34/Recordings.json')
self.assertEqual(call.name, 'ab12cd34')

@inlineCallbacks
def test_make_call_timestamp(self):
res = self.worker.server._get_timestamp()
self.assertTrue(isinstance(res, basestring))
self.assertRegexpMatches(
res, '\w+, \d{2} \w+ \d{4} \d{2}:\d{2}:\d{2} (\+|-)\d{4}')

self.worker.server._get_timestamp = Mock(
return_value='Thu, 01 January 1970 00:00:00 +0000')
call = yield self._twilio_client_create_call(
'default.xml', from_='+12345', to='+54321')
self.assertEqual(call.date_created, datetime.utcfromtimestamp(0))
self.assertEqual(call.date_updated, datetime.utcfromtimestamp(0))

@inlineCallbacks
def test_make_call_response_defaults(self):
call = yield self._twilio_client_create_call(
'default.xml', from_='+12345', to='+54321')

self.assertEqual(call.to, '+54321')
self.assertEqual(call.formatted_to, '+54321')
self.assertEqual(call.from_, '+12345')
self.assertEqual(call.formatted_from, '+12345')
self.assertEqual(call.parent_call_sid, None)
self.assertEqual(call.phone_number_sid, None)
self.assertEqual(call.status, 'queued')
self.assertEqual(call.start_time, None)
self.assertEqual(call.end_time, None)
self.assertEqual(call.duration, None)
self.assertEqual(call.price, None)
self.assertEqual(call.direction, 'outbound-api')
self.assertEqual(call.answered_by, None)
self.assertEqual(call.api_version, 'v1')
self.assertEqual(call.forwarded_from, None)
self.assertEqual(call.caller_name, None)
self.assertEqual(call.account_sid, 'test_account')

@inlineCallbacks
def test_make_call_required_parameters_to(self):
addr = self.twiml_connection.getHost()
url = 'http://%s:%s%s%s' % (
addr.host, addr.port, '/twiml/', 'example.xml')

# Can't use the client here because it requires the required parameters
yield self.assert_parameter_missing(
'/Accounts/test-account/Calls.json', 'POST', data={
'From': '+12345', 'Url': url},
error={
'error_type': 'UsageError',
'error_message':
"Required field 'To' not supplied",
})

@inlineCallbacks
def test_make_call_required_parameters_from(self):
addr = self.twiml_connection.getHost()
url = 'http://%s:%s%s%s' % (
addr.host, addr.port, '/twiml/', 'example.xml')

# Can't use the client here because it requires the required parameters
yield self.assert_parameter_missing(
'/Accounts/test-account/Calls.json', 'POST', data={
'To': '+12345', 'Url': url},
error={
'error_type': 'UsageError',
'error_message':
"Required field 'From' not supplied",
})

@inlineCallbacks
def test_make_call_required_parameters_url(self):
addr = self.twiml_connection.getHost()
url = 'http://%s:%s%s%s' % (
addr.host, addr.port, '/twiml/', 'example.xml')

# Can't use the client here because it requires the required parameters
yield self.assert_parameter_missing(
'/Accounts/test-account/Calls.json', 'POST', data={
'To': '+12345', 'From': '+54321'},
error={
'error_type': 'UsageError',
'error_message':
"Request must have an 'Url' or an 'ApplicationSid' field",
})

response = yield self._server_request(
'/Accounts/test-account/Calls.json', method='POST',
data={'To': '+12345', 'From': '+54321', 'Url': url})
self.assertEqual(response.code, 200)
response = yield response.json()
self.assertEqual(response['to'], '+12345')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we factor out a helper method for these tests? Something like:

yield self.assert_parameter_missing(url, method, data={...}, error={})

?


response = yield self._server_request(
'/Accounts/test-account/Calls.json', method='POST',
data={
'To': '+12345',
'From': '+54321',
'ApplicationSid': 'foobar'})
self.assertEqual(response.code, 200)
response = yield response.json()
self.assertEqual(response['to'], '+12345')

@inlineCallbacks
def test_make_call_optional_parameters_senddigits(self):
call = yield self._twilio_client_create_call(
'default.xml', from_='+12345', to='+54321',
send_digits='0123456789#*w')
self.assertEqual(call.to, '+54321')

e = yield self.assertFailure(
self._twilio_client_create_call(
'default.xml', from_='+12345', to='+54321',
send_digits='0a*'),
TwilioRestException)
self.assertEqual(e.status, 400)
message = json.loads(e.msg)
self.assertEqual(message['error_type'], 'UsageError')
self.assertEqual(
message['error_message'],
"SendDigits value '0a*' is not valid. May only contain the "
"characters (0-9), '#', '*' and 'w'")

@inlineCallbacks
def test_make_call_optional_parameters_ifmachine(self):
call = yield self._twilio_client_create_call(
'default.xml', from_='+12345', to='+54321',
if_machine='Continue')
self.assertEqual(call.to, '+54321')

call = yield self._twilio_client_create_call(
'default.xml', from_='+12345', to='+54321',
if_machine='Hangup')
self.assertEqual(call.to, '+54321')

e = yield self.assertFailure(
self._twilio_client_create_call(
'default.xml', from_='+12345', to='+54321',
if_machine='foobar'),
TwilioRestException)
self.assertEqual(e.status, 400)
message = json.loads(e.msg)
self.assertEqual(message['error_type'], 'UsageError')
self.assertEqual(
message['error_message'],
"IfMachine value must be one of [None, 'Continue', 'Hangup']")


class TestServerFormatting(TestCase):

Expand Down Expand Up @@ -107,14 +383,25 @@ def test_format_xml(self):
def test_format_json(self):
format_json = TwilioAPIServer.format_json
d = {
'Root': {
'Foo': {
'Bar': {
'Baz': 'Qux',
},
'FooBar': 'BazQux',
},
'BarFoo': 'QuxBaz',
}
}
res = format_json(d)
root = json.loads(res)
expected = {
'foo': {
'bar': {
'baz': 'qux',
'baz': 'Qux',
},
'foobar': 'bazqux',
'foo_bar': 'BazQux',
},
'barfoo': 'quxbaz',
'bar_foo': 'QuxBaz',
}
res = format_json(d)
root = json.loads(res)
self.assertEqual(root, d)
self.assertEqual(root, expected)
Loading