8000 Merge remote-tracking branch 'origin/signal-beta' into edge · brianduan/twilio-python@6bf0e94 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6bf0e94

Browse files
committed
Merge remote-tracking branch 'origin/signal-beta' into edge
2 parents 07bf639 + a20892d commit 6bf0e94

File tree

11 files changed

+365
-97
lines changed

11 files changed

+365
-97
lines changed

tests/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
sphinx
2+
httplib2==0.8
23
mock==0.8.0
34
nose
45
coverage

tests/test_access_token.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import unittest
2+
3+
from nose.tools import assert_equal, assert_is_not_none
4+
from twilio.jwt import decode
5+
from twilio.access_token import AccessToken
6+
7+
ACCOUNT_SID = 'AC123'
8+
SIGNING_KEY_SID = 'SK123'
9+
10+
11+
class AccessTokenTest(unittest.TestCase):
12+
def _validate_claims(self, payload):
13+
assert_equal(SIGNING_KEY_SID, payload['iss'])
14+
assert_equal(ACCOUNT_SID, payload['sub'])
15+
assert_is_not_none(payload['nbf'])
16+
assert_is_not_none(payload['exp'])
17+
assert_equal(payload['nbf'] + 3600, payload['exp'])
18+
assert_is_not_none(payload['jti'])
19+
assert_equal('{}-{}'.format(payload['iss'], payload['nbf']),
20+
payload['jti'])
21+
assert_is_not_none(payload['grants'])
22+
23+
def test_empty_grants(self):
24+
scat = AccessToken(SIGNING_KEY_SID, ACCOUNT_SID, 'secret')
25+
token = str(scat)
26+
assert_is_not_none(token)
27+
payload = decode(token, 'secret')
28+
self._validate_claims(payload)
29+
assert_equal([], payload['grants'])
30+
31+
def test_single_grant(self):
32+
scat = AccessToken(SIGNING_KEY_SID, ACCOUNT_SID, 'secret')
33+
scat.add_grant('https://api.twilio.com/**')
34+
token = str(scat)
35+
assert_is_not_none(token)
36+
payload = decode(token, 'secret')
37+
self._validate_claims(payload)
38+
assert_equal(1, len(payload['grants']))
39+
assert_equal('https://api.twilio.com/**', payload['grants'][0]['res'])
40+
assert_equal(['*'], payload['grants'][0]['act'])
41+
42+
def test_endpoint_grant(self):
43+
scat = AccessToken(SIGNING_KEY_SID, ACCOUNT_SID, 'secret')
44+
scat.add_endpoint_grant('bob')
45+
token = str(scat)
46+
assert_is_not_none(token)
47+
payload = decode(token, 'secret')
48+
self._validate_claims(payload)
49+
assert_equal(1, len(payload['grants']))
50+
assert_equal('sip:bob@AC123.endpoint.twilio.com',
51+
payload['grants'][0]['res'])
52+
assert_equal(['listen', 'invite'], payload['grants'][0]['act'])
53+
54+
def test_rest_grant(self):
55+
scat = AccessToken(SIGNING_KEY_SID, ACCOUNT_SID, 'secret')
56+
scat.add_rest_grant('/Apps')
57+
token = str(scat)
58+
assert_is_not_none(token)
59+
payload = decode(token, 'secret')
60+
self._validate_claims(payload)
61+
assert_equal(1, len(payload['grants']))
62+
assert_equal('https://api.twilio.com/2010-04-01/Accounts/AC123/Apps',
63+
payload['grants'][0]['res'])
64+
assert_equal(['*'], payload['grants'][0]['act'])
65+
66+
def test_enable_nts(self):
67+
scat = AccessToken(SIGNING_KEY_SID, ACCOUNT_SID, 'secret')
68+
scat.enable_nts()
69+
token = str(scat)
70+
assert_is_not_none(token)
71+
payload = decode(token, 'secret')
72+
self._validate_claims(payload)
73+
assert_equal(1, len(payload['grants']))
74+
assert_equal('https://api.twilio.com/2010-04-01/Accounts/AC123/Tokens.json',
75+
payload['grants'][0]['res'])
76+
assert_equal(['POST'], payload['grants'][0]['act'])

tests/test_make_request.py

Lines changed: 118 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@
33
44
Uses the awesome httpbin.org to validate responses
55
"""
6+
import base64
67
import platform
8+
import unittest
79

8-
import twilio
10+
from httplib2 import Response
911
from nose.tools import assert_equal, raises
1012
from mock import patch, Mock, ANY
13+
14+
import twilio
1115
from twilio.rest.exceptions import TwilioRestException
1216
from twilio.rest.resources.base import make_request, make_twilio_request
1317
from twilio.rest.resources.connection import Connection
1418
from twilio.rest.resources.connection import PROXY_TYPE_SOCKS5
1519

20+
1621
get_headers = {
1722
"User-Agent": "twilio-python/{version} (Python {python_version})".format(
1823
version=twilio.__version__,
@@ -26,96 +31,115 @@
2631
post_headers["Content-Type"] = "application/x-www-form-urlencoded"
2732

2833

29-
@patch('twilio.rest.resources.base.Response')
30-
@patch('httplib2.Http')
31-
def test_get_params(http_mock, response_mock):
32-
http = Mock()
33-
http.request.return_value = (Mock(), Mock())
34-
http_mock.return_value = http
35-
make_request("GET", "http://httpbin.org/get", params={"hey": "you"})
36-
http.request.assert_called_with("http://httpbin.org/get?hey=you", "GET",
37-
body=None, headers=None)
38-
39-
40-
@patch('twilio.rest.resources.base.Response')
41-
@patch('httplib2.Http')
42-
def test_get_extra_params(http_mock, response_mock):
43-
http = Mock()
44-
http.request.return_value = (Mock(), Mock())
45-
http_mock.return_value = http
46-
make_request("GET", "http://httpbin.org/get?foo=bar", params={"hey": "you"})
47-
http.request.assert_called_with("http://httpbin.org/get?foo=bar&hey=you", "GET",
48-
body=None, headers=None)
49-
50-
51-
@patch('twilio.rest.resources.base.Response')
52-
@patch('httplib2.Http')
53-
def test_resp_uri(http_mock, response_mock):
54-
http = Mock()
55-
http.request.return_value = (Mock(), Mock())
56-
http_mock.return_value = http
57-
make_request("GET", "http://httpbin.org/get")
58-
http.request.assert_called_with("http://httpbin.org/get", "GET",
59-
body=None, headers=None)
60-
61-
62-
@patch('twilio.rest.resources.base.Response')
63-
@patch('httplib2.Http')
64-
def test_sequence_data(http_mock, response_mock):
65-
http = Mock()
66-
http.request.return_value = (Mock(), Mock())
67-
http_mock.return_value = http
68-
make_request(
69-
"POST",
70-
"http://httpbin.org/post",
71-
data={"a_list": ["here", "is", "some", "stuff"]},
72-
)
73-
http.request.assert_called_with(
74-
"http://httpbin.org/post",
75-
"POST",
76-
body="a_list=here&a_list=is&a_list=some&a_list=stuff",
77-
headers=None,
78-
)
79-
80-
81-
@patch('twilio.rest.resources.base.make_request')
82-
def test_make_twilio_request_headers(mock):
83-
url = "http://random/url"
84-
make_twilio_request("POST", url, use_json_extension=True)
85-
mock.assert_called_with("POST", "http://random/url.json",
86-
headers=post_headers)
87-
88-
89-
@raises(TwilioRestException)
90-
@patch('twilio.rest.resources.base.make_request')
91-
def test_make_twilio_request_bad_data(mock):
92-
resp = Mock()
93-
resp.ok = False
94-
resp.return_value = "error"
95-
mock.return_value = resp
96-
97-
url = "http://random/url"
98-
make_twilio_request("POST", url)
99-
mock.assert_called_with("POST", "http://random/url.json",
100-
headers=post_headers)
101-
102-
103-
@patch('twilio.rest.resources.base.Response')
104-
@patch('httplib2.Http')
105-
def test_proxy_info(http_mock, resp_mock):
106-
http = Mock()
107-
http.request.return_value = (Mock(), Mock())
108-
http_mock.return_value = http
109-
Connection.set_proxy_info(
110-
'example.com',
111-
8080,
112-
proxy_type=PROXY_TYPE_SOCKS5,
113-
)
114-
make_request("GET", "http://httpbin.org/get")
115-
http_mock.assert_called_with(timeout=None, ca_certs=ANY, proxy_info=ANY)
116-
http.request.assert_called_with("http://httpbin.org/get", "GET",
117-
body=None, headers=None)
118-
proxy_info = http_mock.call_args[1]['proxy_info']
119-
assert_equal(proxy_info.proxy_host, 'example.com')
120-
assert_equal(proxy_info.proxy_port, 8080)
121-
assert_equal(proxy_info.proxy_type, PROXY_TYPE_SOCKS5)
34+
class MakeRequestTest(unittest.TestCase):
35+
36+
@patch('twilio.rest.resources.base.Response')
37+
@patch('httplib2.Http')
38+
def test_get_params(self, http_mock, response_mock):
39+
http = Mock()
40+
http.request.return_value = (Mock(), Mock())
41+
http_mock.return_value = http
42+
make_request("GET", "http://httpbin.org/get", params={"hey": "you"})
43+
http.request.assert_called_with("http://httpbin.org/get?hey=you", "GET",
44+
body=None, headers=None)
45+
46+
@patch('twilio.rest.resources.base.Response')
47+
@patch('httplib2.Http')
48+
def test_get_extra_params(self, http_mock, response_mock):
49+
http = Mock()
50+
http.request.return_value = (Mock(), Mock())
51+
http_mock.return_value = http
52+
make_request("GET", "http://httpbin.org/get?foo=bar", params={"hey": "you"})
53+
http.request.assert_called_with("http://httpbin.org/get?foo=bar&hey=you", "GET",
54+
body=None, headers=None)
55+
56+
@patch('twilio.rest.resources.base.Response')
57+
@patch('httplib2.Http')
58+
def test_resp_uri(self, http_mock, response_mock):
59+
http = Mock()
60+
http.request.return_value = (Mock(), Mock())
61+
http_mock.return_value = http
62+
make_request("GET", "http://httpbin.org/get")
63+
http.request.assert_called_with("http://httpbin.org/get", "GET",
64+
body=None, headers=None)
65+
66+
@patch('twilio.rest.resources.base.Response')
67+
@patch('httplib2.Http')
68+
def test_sequence_data(self, http_mock, response_mock):
69+
http = Mock()
70+
http.request.return_value = (Mock(), Mock())
71+
http_mock.return_value = http
72+
make_request(
73+
"POST",
74+
"http://httpbin.org/post",
75+
data={"a_list": ["here", "is", "some", "stuff"]},
76+
)
77+
http.request.assert_called_with(
78+
"http://httpbin.org/post",
79+
"POST",
80+
body="a_list=here&a_list=is&a_list=some&a_list=stuff",
81+
headers=None,
82+
)
83+
84+
@patch('twilio.rest.resources.base.Response')
85+
@patch('httplib2.Http._conn_request')
86+
def test_make_request_basic_auth(self, mock_request, mock_response):
87+
response = Response({
88+
'status': '401',
89+
'WWW-Authenticate': 'Basic realm="Twilio API"'
90+
})
91+
mock_request.side_effect = [(response, Mock()), (Mock(), Mock())]
92+
make_request('GET', 'http://httpbin.org/get', auth=('AC123', 'AuthToken'))
93+
mock_request.assert_called_with(
94+
ANY,
95+
'/get',
96+
'GET',
97+
None,
98+
{
99+
'accept-encoding': 'gzip, deflate',
100+
'authorization': 'Basic {}'.format(
101+
base64.b64encode("{}:{}".format('AC123', 'AuthToken'))
102+
),
103+
'user-agent': ANY,
104+
}
105+
)
106+
107+
@patch('twilio.rest.resources.base.make_request')
108+
def test_make_twilio_request_headers(self, mock):
109+
url = "http://random/url"
110+
make_twilio_request("POST", url, use_json_extension=True)
111+
mock.assert_called_with("POST", "http://random/url.json",
112+
headers=post_headers)
113+
114+
@raises(TwilioRestException)
115+
@patch('twilio.rest.resources.base.make_request')
116+
def test_make_twilio_request_bad_data(self, mock):
117+
resp = Mock()
118+
resp.ok = False
119+
resp.return_value = "error"
120+
mock.return_value = resp
121+
122+
url = "http://random/url"
123+
make_twilio_request("POST", url)
124+
mock.assert_called_with("POST", "http://random/url.json",
125+
headers=post_headers)
126+
127+
@patch('twilio.rest.resources.base.Response')
128+
@patch('httplib2.Http')
129+
def test_proxy_info(self, http_mock, resp_mock):
130+
http = Mock()
131+
http.request.return_value = (Mock(), Mock())
132+
http_mock.return_value = http
133+
Connection.set_proxy_info(
134+
'example.com',
135+
8080,
136+
proxy_type=PROXY_TYPE_SOCKS5,
137+
)
138+
make_request("GET", "http://httpbin.org/get")
139+
http_mock.assert_called_with(timeout=None, ca_certs=ANY, proxy_info=ANY)
140+
http.request.assert_called_with("http://httpbin.org/get", "GET",
141+
body=None, headers=None)
142+
proxy_info = http_mock.call_args[1]['proxy_info']
143+
assert_equal(proxy_info.proxy_host, 'example.com')
144+
assert_equal(proxy_info.proxy_port, 8080)
145+
assert_equal(proxy_info.proxy_type, PROXY_TYPE_SOCKS5)

tests/test_signing_keys.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import unittest
2+
from nose.tools import assert_raises
3+
from twilio.rest.resources.signing_keys import SigningKeys
4+
5+
6+
class SigningKeysTest(unittest.TestCase):
7+
8+
def test_list(self):
9+
"""
10+
Tests the Error part
11+
"""
12+
keys = SigningKeys(self, [], {})
13+
assert_raises(AttributeError, keys.list)

twilio/access_token.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import time
2+
3+
import jwt
4+
5+
ALL = '*'
6+
7+
# HTTP Actions
8+
HTTP_DELETE = 'DELETE'
9+
HTTP_GET = 'GET'
10+
HTTP_POST = 'POST'
11+
HTTP_PUT = 'PUT'
12+
13+
# Client Actions
14+
CLIENT_LISTEN = 'listen'
15+
CLIENT_INVITE = 'invite'
16+
17+
18+
class AccessToken(object):
19+
def __init__(self, signing_key_sid, account_sid, secret, ttl=3600):
20+
self.signing_key_sid = signing_key_sid
21+
self.account_sid = account_sid
22+
self.secret = secret
23+
self.ttl = ttl
24+
self.grants = []
25+
26+
def add_grant(self, resource, actions=ALL):
27+
if not isinstance(actions, list):
28+
actions = [actions]
29+
30+
self.grants.append({
31+
'res': resource,
32+
'act': actions,
33+
})
34+
return self
35+
36+
def add_rest_grant(self, uri, actions=ALL):
37+
resource = 'https://api.twilio.com/2010-04-01/Accounts/{}/{}'.format(
38+
self.account_sid,
39+
uri.lstrip('/'),
40+
)
41+
return self.add_grant(resource, actions)
42+
43+
def add_endpoint_grant(self, endpoint, actions=None):
44+
actions = actions or [CLIENT_LISTEN, CLIENT_INVITE]
45+
resource = 'sip:{}@{}.endpoint.twilio.com'.format(endpoint,
46+
self.account_sid)
47+
return self.add_grant(resource, actions)
48+
49+
def enable_nts(self):
50+
return self.add_rest_grant('/Tokens.json', HTTP_POST)
51+
52+
def to_jwt(self):
53+
now = int(time.time())
54+
headers = {
55+
"cty": "twilio-sat;v=1"
56+
}
57+
payload = {
58+
"jti": '{}-{}'.format(self.signing_key_sid, now),
59+
"iss": self.signing_key_sid,
60+
"sub": self.account_sid,
61+
"nbf": now,
62+
"exp": now + self.ttl,
63+
"grants": self.grants
64+
}
65+
66+
return jwt.encode(payload, self.secret, headers=headers)
67+
68+
def __str__(self):
69+
return self.to_jwt()

0 commit comments

Comments
 (0)
0