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

Skip to content
< 8000 header class="HeaderMktg header-logged-out js-details-container js-header Details f4 py-3" role="banner" data-is-top="true" data-color-mode=light data-light-theme=light data-dark-theme=dark>

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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