blob: cb7a832d43ba6587042132d60cbd646625072340 [file] [log] [blame]
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001#!/usr/bin/python2.4
2#
3# Copyright 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Http tests
18
19Unit tests for the apiclient.http.
20"""
21
22__author__ = 'jcgregorio@google.com (Joe Gregorio)'
23
Joe Gregorio7cbceab2011-06-27 10:46:54 -040024# Do not remove the httplib2 import
25import httplib2
Joe Gregoriod0bd3882011-11-22 09:49:47 -050026import os
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050027import unittest
28
Joe Gregorio66f57522011-11-30 11:00:00 -050029from apiclient.errors import BatchError
30from apiclient.http import BatchHttpRequest
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050031from apiclient.http import HttpMockSequence
Joe Gregoriod0bd3882011-11-22 09:49:47 -050032from apiclient.http import HttpRequest
Joe Gregoriod0bd3882011-11-22 09:49:47 -050033from apiclient.http import MediaFileUpload
Joe Gregorio66f57522011-11-30 11:00:00 -050034from apiclient.http import MediaUpload
Ali Afshar6f11ea12012-02-07 10:32:14 -050035from apiclient.http import MediaInMemoryUpload
Joe Gregorio66f57522011-11-30 11:00:00 -050036from apiclient.http import set_user_agent
37from apiclient.model import JsonModel
Joe Gregorio654f4a22012-02-09 14:15:44 -050038from oauth2client.client import Credentials
39
40
41class MockCredentials(Credentials):
42 """Mock class for all Credentials objects."""
43 def __init__(self, bearer_token):
44 super(MockCredentials, self).__init__()
45 self._authorized = 0
46 self._refreshed = 0
47 self._applied = 0
48 self._bearer_token = bearer_token
49
50 def authorize(self, http):
51 self._authorized += 1
52
53 request_orig = http.request
54
55 # The closure that will replace 'httplib2.Http.request'.
56 def new_request(uri, method='GET', body=None, headers=None,
57 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
58 connection_type=None):
59 # Modify the request headers to add the appropriate
60 # Authorization header.
61 if headers is None:
62 headers = {}
63 self.apply(headers)
64
65 resp, content = request_orig(uri, method, body, headers,
66 redirections, connection_type)
67
68 return resp, content
69
70 # Replace the request method with our own closure.
71 http.request = new_request
72
73 # Set credentials as a property of the request method.
74 setattr(http.request, 'credentials', self)
75
76 return http
77
78 def refresh(self, http):
79 self._refreshed += 1
80
81 def apply(self, headers):
82 self._applied += 1
83 headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050084
85
Joe Gregoriod0bd3882011-11-22 09:49:47 -050086DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
87
88
89def datafile(filename):
90 return os.path.join(DATA_DIR, filename)
91
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050092class TestUserAgent(unittest.TestCase):
93
94 def test_set_user_agent(self):
95 http = HttpMockSequence([
96 ({'status': '200'}, 'echo_request_headers'),
97 ])
98
99 http = set_user_agent(http, "my_app/5.5")
100 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500101 self.assertEqual('my_app/5.5', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500102
103 def test_set_user_agent_nested(self):
104 http = HttpMockSequence([
105 ({'status': '200'}, 'echo_request_headers'),
106 ])
107
108 http = set_user_agent(http, "my_app/5.5")
109 http = set_user_agent(http, "my_library/0.1")
110 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500111 self.assertEqual('my_app/5.5 my_library/0.1', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500112
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500113 def test_media_file_upload_to_from_json(self):
114 upload = MediaFileUpload(
115 datafile('small.png'), chunksize=500, resumable=True)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500116 self.assertEqual('image/png', upload.mimetype())
117 self.assertEqual(190, upload.size())
118 self.assertEqual(True, upload.resumable())
119 self.assertEqual(500, upload.chunksize())
120 self.assertEqual('PNG', upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500121
122 json = upload.to_json()
123 new_upload = MediaUpload.new_from_json(json)
124
Joe Gregorio654f4a22012-02-09 14:15:44 -0500125 self.assertEqual('image/png', new_upload.mimetype())
126 self.assertEqual(190, new_upload.size())
127 self.assertEqual(True, new_upload.resumable())
128 self.assertEqual(500, new_upload.chunksize())
129 self.assertEqual('PNG', new_upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500130
131 def test_http_request_to_from_json(self):
132
133 def _postproc(*kwargs):
134 pass
135
136 http = httplib2.Http()
137 media_upload = MediaFileUpload(
138 datafile('small.png'), chunksize=500, resumable=True)
139 req = HttpRequest(
140 http,
141 _postproc,
142 'http://example.com',
143 method='POST',
144 body='{}',
145 headers={'content-type': 'multipart/related; boundary="---flubber"'},
146 methodId='foo',
147 resumable=media_upload)
148
149 json = req.to_json()
150 new_req = HttpRequest.from_json(json, http, _postproc)
151
Joe Gregorio654f4a22012-02-09 14:15:44 -0500152 self.assertEqual({'content-type':
153 'multipart/related; boundary="---flubber"'},
154 new_req.headers)
155 self.assertEqual('http://example.com', new_req.uri)
156 self.assertEqual('{}', new_req.body)
157 self.assertEqual(http, new_req.http)
158 self.assertEqual(media_upload.to_json(), new_req.resumable.to_json())
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500159
Joe Gregorio66f57522011-11-30 11:00:00 -0500160EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
161Content-Type: application/json
162MIME-Version: 1.0
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500163Host: www.googleapis.com
164content-length: 2\r\n\r\n{}"""
165
166
167NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
168Content-Type: application/json
169MIME-Version: 1.0
170Host: www.googleapis.com
171content-length: 0\r\n\r\n"""
Joe Gregorio66f57522011-11-30 11:00:00 -0500172
173
174RESPONSE = """HTTP/1.1 200 OK
175Content-Type application/json
176Content-Length: 14
177ETag: "etag/pony"\r\n\r\n{"answer": 42}"""
178
179
180BATCH_RESPONSE = """--batch_foobarbaz
181Content-Type: application/http
182Content-Transfer-Encoding: binary
183Content-ID: <randomness+1>
184
185HTTP/1.1 200 OK
186Content-Type application/json
187Content-Length: 14
188ETag: "etag/pony"\r\n\r\n{"foo": 42}
189
190--batch_foobarbaz
191Content-Type: application/http
192Content-Transfer-Encoding: binary
193Content-ID: <randomness+2>
194
195HTTP/1.1 200 OK
196Content-Type application/json
197Content-Length: 14
198ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
199--batch_foobarbaz--"""
200
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500201
Joe Gregorio654f4a22012-02-09 14:15:44 -0500202BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz
203Content-Type: application/http
204Content-Transfer-Encoding: binary
205Content-ID: <randomness+1>
206
207HTTP/1.1 401 Authoration Required
208Content-Type application/json
209Content-Length: 14
210ETag: "etag/pony"\r\n\r\n{"error": {"message":
211 "Authorizaton failed."}}
212
213--batch_foobarbaz
214Content-Type: application/http
215Content-Transfer-Encoding: binary
216Content-ID: <randomness+2>
217
218HTTP/1.1 200 OK
219Content-Type application/json
220Content-Length: 14
221ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
222--batch_foobarbaz--"""
223
224
225BATCH_SINGLE_RESPONSE = """--batch_foobarbaz
226Content-Type: application/http
227Content-Transfer-Encoding: binary
228Content-ID: <randomness+1>
229
230HTTP/1.1 200 OK
231Content-Type application/json
232Content-Length: 14
233ETag: "etag/pony"\r\n\r\n{"foo": 42}
234--batch_foobarbaz--"""
235
236class Callbacks(object):
237 def __init__(self):
238 self.responses = {}
239 self.exceptions = {}
240
241 def f(self, request_id, response, exception):
242 self.responses[request_id] = response
243 self.exceptions[request_id] = exception
244
245
Joe Gregorio66f57522011-11-30 11:00:00 -0500246class TestBatch(unittest.TestCase):
247
248 def setUp(self):
249 model = JsonModel()
250 self.request1 = HttpRequest(
251 None,
252 model.response,
253 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
254 method='POST',
255 body='{}',
256 headers={'content-type': 'application/json'})
257
258 self.request2 = HttpRequest(
259 None,
260 model.response,
261 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500262 method='GET',
263 body='',
Joe Gregorio66f57522011-11-30 11:00:00 -0500264 headers={'content-type': 'application/json'})
265
266
267 def test_id_to_from_content_id_header(self):
268 batch = BatchHttpRequest()
269 self.assertEquals('12', batch._header_to_id(batch._id_to_header('12')))
270
271 def test_invalid_content_id_header(self):
272 batch = BatchHttpRequest()
273 self.assertRaises(BatchError, batch._header_to_id, '[foo+x]')
274 self.assertRaises(BatchError, batch._header_to_id, 'foo+1')
275 self.assertRaises(BatchError, batch._header_to_id, '<foo>')
276
277 def test_serialize_request(self):
278 batch = BatchHttpRequest()
279 request = HttpRequest(
280 None,
281 None,
282 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
283 method='POST',
284 body='{}',
285 headers={'content-type': 'application/json'},
286 methodId=None,
287 resumable=None)
288 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500289 self.assertEqual(EXPECTED.splitlines(), s)
Joe Gregorio66f57522011-11-30 11:00:00 -0500290
Joe Gregoriodd813822012-01-25 10:32:47 -0500291 def test_serialize_request_media_body(self):
292 batch = BatchHttpRequest()
293 f = open(datafile('small.png'))
294 body = f.read()
295 f.close()
296
297 request = HttpRequest(
298 None,
299 None,
300 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
301 method='POST',
302 body=body,
303 headers={'content-type': 'application/json'},
304 methodId=None,
305 resumable=None)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500306 # Just testing it shouldn't raise an exception.
Joe Gregoriodd813822012-01-25 10:32:47 -0500307 s = batch._serialize_request(request).splitlines()
308
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500309 def test_serialize_request_no_body(self):
310 batch = BatchHttpRequest()
311 request = HttpRequest(
312 None,
313 None,
314 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
315 method='POST',
316 body='',
317 headers={'content-type': 'application/json'},
318 methodId=None,
319 resumable=None)
320 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500321 self.assertEqual(NO_BODY_EXPECTED.splitlines(), s)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500322
Joe Gregorio66f57522011-11-30 11:00:00 -0500323 def test_deserialize_response(self):
324 batch = BatchHttpRequest()
325 resp, content = batch._deserialize_response(RESPONSE)
326
Joe Gregorio654f4a22012-02-09 14:15:44 -0500327 self.assertEqual(200, resp.status)
328 self.assertEqual('OK', resp.reason)
329 self.assertEqual(11, resp.version)
330 self.assertEqual('{"answer": 42}', content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500331
332 def test_new_id(self):
333 batch = BatchHttpRequest()
334
335 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500336 self.assertEqual('1', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500337
338 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500339 self.assertEqual('2', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500340
341 batch.add(self.request1, request_id='3')
342
343 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500344 self.assertEqual('4', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500345
346 def test_add(self):
347 batch = BatchHttpRequest()
348 batch.add(self.request1, request_id='1')
349 self.assertRaises(KeyError, batch.add, self.request1, request_id='1')
350
351 def test_add_fail_for_resumable(self):
352 batch = BatchHttpRequest()
353
354 upload = MediaFileUpload(
355 datafile('small.png'), chunksize=500, resumable=True)
356 self.request1.resumable = upload
357 self.assertRaises(BatchError, batch.add, self.request1, request_id='1')
358
359 def test_execute(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500360 batch = BatchHttpRequest()
361 callbacks = Callbacks()
362
363 batch.add(self.request1, callback=callbacks.f)
364 batch.add(self.request2, callback=callbacks.f)
365 http = HttpMockSequence([
366 ({'status': '200',
367 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
368 BATCH_RESPONSE),
369 ])
370 batch.execute(http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500371 self.assertEqual({'foo': 42}, callbacks.responses['1'])
372 self.assertEqual(None, callbacks.exceptions['1'])
373 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
374 self.assertEqual(None, callbacks.exceptions['2'])
Joe Gregorio66f57522011-11-30 11:00:00 -0500375
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500376 def test_execute_request_body(self):
377 batch = BatchHttpRequest()
378
379 batch.add(self.request1)
380 batch.add(self.request2)
381 http = HttpMockSequence([
382 ({'status': '200',
383 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
384 'echo_request_body'),
385 ])
386 try:
387 batch.execute(http)
388 self.fail('Should raise exception')
389 except BatchError, e:
390 boundary, _ = e.content.split(None, 1)
391 self.assertEqual('--', boundary[:2])
392 parts = e.content.split(boundary)
393 self.assertEqual(4, len(parts))
394 self.assertEqual('', parts[0])
395 self.assertEqual('--', parts[3])
396 header = parts[1].splitlines()[1]
397 self.assertEqual('Content-Type: application/http', header)
398
Joe Gregorio654f4a22012-02-09 14:15:44 -0500399 def test_execute_refresh_and_retry_on_401(self):
400 batch = BatchHttpRequest()
401 callbacks = Callbacks()
402 cred_1 = MockCredentials('Foo')
403 cred_2 = MockCredentials('Bar')
404
405 http = HttpMockSequence([
406 ({'status': '200',
407 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
408 BATCH_RESPONSE_WITH_401),
409 ({'status': '200',
410 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
411 BATCH_SINGLE_RESPONSE),
412 ])
413
414 creds_http_1 = HttpMockSequence([])
415 cred_1.authorize(creds_http_1)
416
417 creds_http_2 = HttpMockSequence([])
418 cred_2.authorize(creds_http_2)
419
420 self.request1.http = creds_http_1
421 self.request2.http = creds_http_2
422
423 batch.add(self.request1, callback=callbacks.f)
424 batch.add(self.request2, callback=callbacks.f)
425 batch.execute(http)
426
427 self.assertEqual({'foo': 42}, callbacks.responses['1'])
428 self.assertEqual(None, callbacks.exceptions['1'])
429 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
430 self.assertEqual(None, callbacks.exceptions['2'])
431
432 self.assertEqual(1, cred_1._refreshed)
433 self.assertEqual(0, cred_2._refreshed)
434
435 self.assertEqual(1, cred_1._authorized)
436 self.assertEqual(1, cred_2._authorized)
437
438 self.assertEqual(1, cred_2._applied)
439 self.assertEqual(2, cred_1._applied)
440
441 def test_http_errors_passed_to_callback(self):
442 batch = BatchHttpRequest()
443 callbacks = Callbacks()
444 cred_1 = MockCredentials('Foo')
445 cred_2 = MockCredentials('Bar')
446
447 http = HttpMockSequence([
448 ({'status': '200',
449 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
450 BATCH_RESPONSE_WITH_401),
451 ({'status': '200',
452 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
453 BATCH_RESPONSE_WITH_401),
454 ])
455
456 creds_http_1 = HttpMockSequence([])
457 cred_1.authorize(creds_http_1)
458
459 creds_http_2 = HttpMockSequence([])
460 cred_2.authorize(creds_http_2)
461
462 self.request1.http = creds_http_1
463 self.request2.http = creds_http_2
464
465 batch.add(self.request1, callback=callbacks.f)
466 batch.add(self.request2, callback=callbacks.f)
467 batch.execute(http)
468
469 self.assertEqual(None, callbacks.responses['1'])
470 self.assertEqual(401, callbacks.exceptions['1'].resp.status)
471 self.assertEqual({u'baz': u'qux'}, callbacks.responses['2'])
472 self.assertEqual(None, callbacks.exceptions['2'])
473
Joe Gregorio66f57522011-11-30 11:00:00 -0500474 def test_execute_global_callback(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500475 callbacks = Callbacks()
476 batch = BatchHttpRequest(callback=callbacks.f)
477
478 batch.add(self.request1)
479 batch.add(self.request2)
480 http = HttpMockSequence([
481 ({'status': '200',
482 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
483 BATCH_RESPONSE),
484 ])
485 batch.execute(http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500486 self.assertEqual({'foo': 42}, callbacks.responses['1'])
487 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500488
Ali Afshar6f11ea12012-02-07 10:32:14 -0500489 def test_media_inmemory_upload(self):
490 media = MediaInMemoryUpload('abcdef', 'text/plain', chunksize=10,
491 resumable=True)
492 self.assertEqual('text/plain', media.mimetype())
493 self.assertEqual(10, media.chunksize())
494 self.assertTrue(media.resumable())
495 self.assertEqual('bc', media.getbytes(1, 2))
496
497 def test_media_inmemory_upload_json_roundtrip(self):
498 media = MediaInMemoryUpload(os.urandom(64), 'text/plain', chunksize=10,
499 resumable=True)
500 data = media.to_json()
501 newmedia = MediaInMemoryUpload.new_from_json(data)
502 self.assertEqual(media._body, newmedia._body)
503 self.assertEqual(media._chunksize, newmedia._chunksize)
504 self.assertEqual(media._resumable, newmedia._resumable)
505 self.assertEqual(media._mimetype, newmedia._mimetype)
506
507
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500508if __name__ == '__main__':
509 unittest.main()