diff options
author | Mike Crute <mike@crute.us> | 2017-10-07 22:38:43 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2017-10-07 22:38:43 +0000 |
commit | c57ed164a699c1e7f57897c0ee24f6bf88d4b236 (patch) | |
tree | dbf421f6d8689274b73b249bece0f9ecc5cf49ed | |
parent | a8932e9f004a83cae58ed0b086271e2191a04b83 (diff) | |
download | pydora-c57ed164a699c1e7f57897c0ee24f6bf88d4b236.tar.bz2 pydora-c57ed164a699c1e7f57897c0ee24f6bf88d4b236.tar.xz pydora-c57ed164a699c1e7f57897c0ee24f6bf88d4b236.zip |
Add transport tests, 100% pandora coverage!
-rwxr-xr-x | setup.py | 1 | ||||
-rw-r--r-- | tests/test_pandora/test_transport.py | 295 | ||||
-rw-r--r-- | tests/test_pandora/test_utils.py | 22 |
3 files changed, 315 insertions, 3 deletions
@@ -46,6 +46,7 @@ requires = { | |||
46 | "tests_require": [ | 46 | "tests_require": [ |
47 | "mock>=2,<3", | 47 | "mock>=2,<3", |
48 | "coverage>=4.1,<5", | 48 | "coverage>=4.1,<5", |
49 | "cryptography>=2,<3", | ||
49 | ], | 50 | ], |
50 | "install_requires": [ | 51 | "install_requires": [ |
51 | "requests>=2,<3", | 52 | "requests>=2,<3", |
diff --git a/tests/test_pandora/test_transport.py b/tests/test_pandora/test_transport.py index 5474a56..4717617 100644 --- a/tests/test_pandora/test_transport.py +++ b/tests/test_pandora/test_transport.py | |||
@@ -1,11 +1,16 @@ | |||
1 | import sys | ||
1 | import time | 2 | import time |
3 | import json | ||
4 | import random | ||
5 | import requests | ||
2 | from unittest import TestCase | 6 | from unittest import TestCase |
3 | from pandora.errors import InvalidAuthToken, PandoraException | 7 | from pandora.py2compat import Mock, call, patch |
4 | |||
5 | from pandora.py2compat import Mock, call | ||
6 | 8 | ||
9 | from pandora.errors import InvalidAuthToken, PandoraException | ||
7 | from tests.test_pandora.test_clientbuilder import TestSettingsDictBuilder | 10 | from tests.test_pandora.test_clientbuilder import TestSettingsDictBuilder |
8 | 11 | ||
12 | import pandora.transport as t | ||
13 | |||
9 | 14 | ||
10 | class SysCallError(Exception): | 15 | class SysCallError(Exception): |
11 | pass | 16 | pass |
@@ -13,6 +18,17 @@ class SysCallError(Exception): | |||
13 | 18 | ||
14 | class TestTransport(TestCase): | 19 | class TestTransport(TestCase): |
15 | 20 | ||
21 | def test_test_url_should_return_true_if_request_okay(self): | ||
22 | transport = t.APITransport(Mock()) | ||
23 | transport._http = Mock() | ||
24 | transport._http.head.return_value = Mock( | ||
25 | status_code=requests.codes.not_found) | ||
26 | |||
27 | self.assertFalse(transport.test_url("foo")) | ||
28 | |||
29 | transport._http.head.return_value = Mock(status_code=requests.codes.OK) | ||
30 | self.assertTrue(transport.test_url("foo")) | ||
31 | |||
16 | def test_call_should_retry_max_times_on_sys_call_error(self): | 32 | def test_call_should_retry_max_times_on_sys_call_error(self): |
17 | with self.assertRaises(SysCallError): | 33 | with self.assertRaises(SysCallError): |
18 | client = TestSettingsDictBuilder._build_minimal() | 34 | client = TestSettingsDictBuilder._build_minimal() |
@@ -57,3 +73,276 @@ class TestTransport(TestCase): | |||
57 | client.transport._start_request.assert_has_calls([call("method")]) | 73 | client.transport._start_request.assert_has_calls([call("method")]) |
58 | assert client.transport._start_request.call_count == 2 | 74 | assert client.transport._start_request.call_count == 2 |
59 | assert client._authenticate.call_count == 1 | 75 | assert client._authenticate.call_count == 1 |
76 | |||
77 | def test_complete_request(self): | ||
78 | transport = t.APITransport(Mock()) | ||
79 | transport._http = Mock() | ||
80 | |||
81 | http_result = Mock() | ||
82 | http_result.content = b'{"stat":"ok","result":"bar"}' | ||
83 | transport._http.post.return_value = http_result | ||
84 | |||
85 | self.assertEqual( | ||
86 | "bar", transport(t.APITransport.NO_ENCRYPT[0], foo="bar")) | ||
87 | |||
88 | |||
89 | class TestTransportSetters(TestCase): | ||
90 | |||
91 | def setUp(self): | ||
92 | self.cryptor = Mock() | ||
93 | self.transport = t.APITransport(self.cryptor) | ||
94 | |||
95 | def test_set_partner(self): | ||
96 | self.cryptor.decrypt_sync_time.return_value = 456 | ||
97 | |||
98 | self.transport.set_partner({ | ||
99 | "syncTime": "123", | ||
100 | "partnerAuthToken": "partner_auth_token", | ||
101 | "partnerId": "partner_id", | ||
102 | }) | ||
103 | |||
104 | self.cryptor.decrypt_sync_time.assert_called_with("123") | ||
105 | self.assertEqual("partner_auth_token", self.transport.auth_token) | ||
106 | self.assertEqual("partner_id", self.transport.partner_id) | ||
107 | self.assertEqual( | ||
108 | "partner_auth_token", self.transport.partner_auth_token) | ||
109 | |||
110 | self.transport.start_time = 10 | ||
111 | with patch.object(time, "time", return_value=30): | ||
112 | self.assertEqual(476, self.transport.sync_time) | ||
113 | |||
114 | def test_set_user(self): | ||
115 | self.transport.set_user({ | ||
116 | "userId": "user", | ||
117 | "userAuthToken": "auth", | ||
118 | }) | ||
119 | |||
120 | self.assertEqual("user", self.transport.user_id) | ||
121 | self.assertEqual("auth", self.transport.user_auth_token) | ||
122 | self.assertEqual("auth", self.transport.auth_token) | ||
123 | |||
124 | def test_getting_auth_token_no_login(self): | ||
125 | self.assertIsNone(self.transport.auth_token) | ||
126 | self.assertIsNone(self.transport.sync_time) | ||
127 | |||
128 | |||
129 | class TestDelayExponential(TestCase): | ||
130 | |||
131 | def test_fixed_delay(self): | ||
132 | self.assertEqual(8, t.delay_exponential(2, 2, 3)) | ||
133 | |||
134 | def test_random_delay(self): | ||
135 | with patch.object(random, "random", return_value=10): | ||
136 | self.assertEqual(20, t.delay_exponential("rand", 2, 2)) | ||
137 | |||
138 | def test_fails_with_base_zero_or_below(self): | ||
139 | with self.assertRaises(ValueError): | ||
140 | t.delay_exponential(0, 1, 1) | ||
141 | |||
142 | with self.assertRaises(ValueError): | ||
143 | t.delay_exponential(-1, 1, 1) | ||
144 | |||
145 | |||
146 | class TestRetries(TestCase): | ||
147 | |||
148 | def test_no_retries_returns_none(self): | ||
149 | @t.retries(0) | ||
150 | def foo(): | ||
151 | return True | ||
152 | |||
153 | self.assertIsNone(foo()) | ||
154 | |||
155 | |||
156 | class TestParseResponse(TestCase): | ||
157 | |||
158 | VALID_MSG_NO_BODY_JSON = b'{"stat":"ok"}' | ||
159 | VALID_MSG_JSON = b'{"stat":"ok", "result":{"foo":"bar"}}' | ||
160 | ERROR_MSG_JSON = b'{"stat":"err", "code":1001, "message":"Details"}' | ||
161 | |||
162 | def setUp(self): | ||
163 | self.transport = t.APITransport(Mock()) | ||
164 | |||
165 | def test_with_valid_response(self): | ||
166 | res = self.transport._parse_response(self.VALID_MSG_JSON) | ||
167 | self.assertEqual({ "foo": "bar" }, res) | ||
168 | |||
169 | def test_with_valid_response_no_body(self): | ||
170 | res = self.transport._parse_response(self.VALID_MSG_NO_BODY_JSON) | ||
171 | self.assertIsNone(res) | ||
172 | |||
173 | def test_with_error_response(self): | ||
174 | with self.assertRaises(InvalidAuthToken) as ex: | ||
175 | self.transport._parse_response(self.ERROR_MSG_JSON) | ||
176 | |||
177 | self.assertEqual(1001, ex.exception.code) | ||
178 | self.assertEqual("Details", ex.exception.extended_message) | ||
179 | |||
180 | |||
181 | class TestTransportRequestPrep(TestCase): | ||
182 | |||
183 | def setUp(self): | ||
184 | self.cryptor = Mock() | ||
185 | self.transport = t.APITransport(self.cryptor) | ||
186 | |||
187 | def test_start_request(self): | ||
188 | self.transport.start_time = 10 | ||
189 | self.transport._start_request("method_name") | ||
190 | self.assertEqual(10, self.transport.start_time) | ||
191 | |||
192 | def test_start_request_with_reset(self): | ||
193 | self.transport.reset = Mock() | ||
194 | self.transport._start_request(self.transport.REQUIRE_RESET[0]) | ||
195 | self.transport.reset.assert_called_with() | ||
196 | |||
197 | def test_start_request_without_time(self): | ||
198 | with patch.object(time, "time", return_value=10.0): | ||
199 | self.transport._start_request("method_name") | ||
200 | self.assertEqual(10, self.transport.start_time) | ||
201 | |||
202 | def test_make_http_request(self): | ||
203 | # url, data, params | ||
204 | http = Mock() | ||
205 | retval = Mock() | ||
206 | retval.content = "foo" | ||
207 | http.post.return_value = retval | ||
208 | |||
209 | self.transport._http = http | ||
210 | res = self.transport._make_http_request( | ||
211 | "/url", b"data", { "a":None, "b":"c" }) | ||
212 | |||
213 | http.post.assert_called_with("/url", data=b"data", params={"b":"c"}) | ||
214 | retval.raise_for_status.assert_called_with() | ||
215 | |||
216 | self.assertEqual("foo", res) | ||
217 | |||
218 | def test_build_data_not_logged_in(self): | ||
219 | self.cryptor.encrypt = lambda x: x | ||
220 | |||
221 | self.transport.partner_auth_token = "pat" | ||
222 | self.transport.server_sync_time = 123 | ||
223 | self.transport.start_time = 23 | ||
224 | |||
225 | with patch.object(time, "time", return_value=20): | ||
226 | val = self.transport._build_data("foo", {"a":"b", "c":None}) | ||
227 | |||
228 | val = json.loads(val) | ||
229 | self.assertEqual("b", val["a"]) | ||
230 | self.assertEqual("pat", val["partnerAuthToken"]) | ||
231 | self.assertEqual(120, val["syncTime"]) | ||
232 | |||
233 | def test_build_data_no_encrypt(self): | ||
234 | self.transport.user_auth_token = "uat" | ||
235 | self.transport.partner_auth_token = "pat" | ||
236 | self.transport.server_sync_time = 123 | ||
237 | self.transport.start_time = 23 | ||
238 | |||
239 | with patch.object(time, "time", return_value=20): | ||
240 | val = self.transport._build_data( | ||
241 | t.APITransport.NO_ENCRYPT[0], {"a":"b", "c":None}) | ||
242 | |||
243 | val = json.loads(val) | ||
244 | self.assertEqual("b", val["a"]) | ||
245 | self.assertEqual("uat", val["userAuthToken"]) | ||
246 | self.assertEqual(120, val["syncTime"]) | ||
247 | |||
248 | |||
249 | # All Cryptor implementations must pass these test cases unmodified | ||
250 | class CommonCryptorTestCases(object): | ||
251 | |||
252 | def test_decrypt_invalid_padding(self): | ||
253 | with self.assertRaises(ValueError): | ||
254 | data = b"12345678\x00" | ||
255 | self.assertEqual(b"12345678\x00", self.cryptor.decrypt(data)) | ||
256 | |||
257 | def test_decrypt_strip_padding(self): | ||
258 | data = b"123456\x02\x02" | ||
259 | self.assertEqual(b"123456", self.cryptor.decrypt(data)) | ||
260 | |||
261 | def test_decrypt_preserve_padding(self): | ||
262 | data = b"123456\x02\x02" | ||
263 | self.assertEqual(b"123456\x02\x02", self.cryptor.decrypt(data, False)) | ||
264 | |||
265 | def test_encrypt(self): | ||
266 | data = "123456" | ||
267 | self.assertEqual(b"123456\x02\x02", self.cryptor.encrypt(data)) | ||
268 | |||
269 | |||
270 | class TestPurePythonBlowfishCryptor(TestCase, CommonCryptorTestCases): | ||
271 | |||
272 | def setUp(self): | ||
273 | # Ugh... blowfish can't even be *imported* in python2 | ||
274 | if sys.version_info.major == 2: | ||
275 | t.blowfish = Mock() | ||
276 | |||
277 | self.cipher = Mock() | ||
278 | self.cipher.decrypt_ecb = lambda x: [x] | ||
279 | self.cipher.encrypt_ecb = lambda x: [x] | ||
280 | self.cryptor = t.PurePythonBlowfish("keys") | ||
281 | self.cryptor.cipher = self.cipher | ||
282 | |||
283 | |||
284 | class TestCryptographyBlowfish(TestCase, CommonCryptorTestCases): | ||
285 | |||
286 | class FakeCipher(object): | ||
287 | |||
288 | def update_into(self, val, buf): | ||
289 | for i, v in enumerate(val): | ||
290 | buf[i] = v | ||
291 | return len(val) | ||
292 | |||
293 | def finalize(self): | ||
294 | return b"" | ||
295 | |||
296 | def setUp(self): | ||
297 | self.cipher = Mock() | ||
298 | self.cipher.encryptor.return_value = self.FakeCipher() | ||
299 | self.cipher.decryptor.return_value = self.FakeCipher() | ||
300 | self.cryptor = t.CryptographyBlowfish("keys") | ||
301 | self.cryptor.cipher = self.cipher | ||
302 | |||
303 | |||
304 | class TestEncryptor(TestCase): | ||
305 | |||
306 | ENCODED_JSON = "7b22666f6f223a22626172227d" | ||
307 | UNENCODED_JSON = b'{"foo":"bar"}' | ||
308 | EXPECTED_TIME = 4111 | ||
309 | ENCODED_TIME = "31353037343131313539" | ||
310 | |||
311 | class NoopCrypto(object): | ||
312 | |||
313 | def __init__(self, key): | ||
314 | pass | ||
315 | |||
316 | def decrypt(self, data, strip_padding=True): | ||
317 | return data.decode("ascii") | ||
318 | |||
319 | def encrypt(self, data): | ||
320 | return data | ||
321 | |||
322 | def setUp(self): | ||
323 | self.cryptor = t.Encryptor("in", "out", self.NoopCrypto) | ||
324 | |||
325 | def test_decrypt(self): | ||
326 | self.assertEqual( | ||
327 | { "foo": "bar" }, self.cryptor.decrypt(self.ENCODED_JSON)) | ||
328 | |||
329 | def test_encrypt(self): | ||
330 | self.assertEqual( | ||
331 | self.ENCODED_JSON.encode("ascii"), | ||
332 | self.cryptor.encrypt(self.UNENCODED_JSON)) | ||
333 | |||
334 | def test_decrypt_sync_time(self): | ||
335 | self.assertEqual( | ||
336 | self.EXPECTED_TIME, | ||
337 | self.cryptor.decrypt_sync_time(self.ENCODED_TIME)) | ||
338 | |||
339 | |||
340 | class TestDefaultStrategy(TestCase): | ||
341 | |||
342 | def test_blowfish_not_available(self): | ||
343 | del sys.modules["pandora.transport"] | ||
344 | sys.modules["blowfish"] = None | ||
345 | |||
346 | import pandora.transport as t | ||
347 | self.assertIsNone(t.blowfish) | ||
348 | self.assertIs(t._default_crypto, t.CryptographyBlowfish) | ||
diff --git a/tests/test_pandora/test_utils.py b/tests/test_pandora/test_utils.py new file mode 100644 index 0000000..2b4ca83 --- /dev/null +++ b/tests/test_pandora/test_utils.py | |||
@@ -0,0 +1,22 @@ | |||
1 | import warnings | ||
2 | from unittest import TestCase | ||
3 | from pandora.py2compat import patch | ||
4 | |||
5 | from pandora import util | ||
6 | |||
7 | |||
8 | class TestDeprecatedWarning(TestCase): | ||
9 | |||
10 | def test_warning(self): | ||
11 | class Bar(object): | ||
12 | |||
13 | @util.deprecated("1.0", "2.0", "Don't use this") | ||
14 | def foo(self): | ||
15 | pass | ||
16 | |||
17 | with patch.object(warnings, "warn") as wmod: | ||
18 | Bar().foo() | ||
19 | |||
20 | wmod.assert_called_with( | ||
21 | ("foo is deprecated as of version 1.0 and will be removed in " | ||
22 | "version 2.0. Don't use this"), DeprecationWarning) | ||