blob: 499366ae238bc97cca4c40b91f2bb7ee29e57bf3 [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
Ali Afshar1cb6b672012-03-12 08:46:14 -0400131 def test_media_inmemory_upload(self):
132 media = MediaInMemoryUpload('abcdef', 'text/plain', chunksize=10,
133 resumable=True)
134 self.assertEqual('text/plain', media.mimetype())
135 self.assertEqual(10, media.chunksize())
136 self.assertTrue(media.resumable())
137 self.assertEqual('bc', media.getbytes(1, 2))
138 self.assertEqual(6, media.size())
139
140 def test_media_inmemory_upload_json_roundtrip(self):
141 media = MediaInMemoryUpload(os.urandom(64), 'text/plain', chunksize=10,
142 resumable=True)
143 data = media.to_json()
144 newmedia = MediaInMemoryUpload.new_from_json(data)
145 self.assertEqual(media._body, newmedia._body)
146 self.assertEqual(media._chunksize, newmedia._chunksize)
147 self.assertEqual(media._resumable, newmedia._resumable)
148 self.assertEqual(media._mimetype, newmedia._mimetype)
149
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500150 def test_http_request_to_from_json(self):
151
152 def _postproc(*kwargs):
153 pass
154
155 http = httplib2.Http()
156 media_upload = MediaFileUpload(
157 datafile('small.png'), chunksize=500, resumable=True)
158 req = HttpRequest(
159 http,
160 _postproc,
161 'http://example.com',
162 method='POST',
163 body='{}',
164 headers={'content-type': 'multipart/related; boundary="---flubber"'},
165 methodId='foo',
166 resumable=media_upload)
167
168 json = req.to_json()
169 new_req = HttpRequest.from_json(json, http, _postproc)
170
Joe Gregorio654f4a22012-02-09 14:15:44 -0500171 self.assertEqual({'content-type':
172 'multipart/related; boundary="---flubber"'},
173 new_req.headers)
174 self.assertEqual('http://example.com', new_req.uri)
175 self.assertEqual('{}', new_req.body)
176 self.assertEqual(http, new_req.http)
177 self.assertEqual(media_upload.to_json(), new_req.resumable.to_json())
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500178
Joe Gregorio66f57522011-11-30 11:00:00 -0500179EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
180Content-Type: application/json
181MIME-Version: 1.0
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500182Host: www.googleapis.com
183content-length: 2\r\n\r\n{}"""
184
185
186NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
187Content-Type: application/json
188MIME-Version: 1.0
189Host: www.googleapis.com
190content-length: 0\r\n\r\n"""
Joe Gregorio66f57522011-11-30 11:00:00 -0500191
192
193RESPONSE = """HTTP/1.1 200 OK
194Content-Type application/json
195Content-Length: 14
196ETag: "etag/pony"\r\n\r\n{"answer": 42}"""
197
198
199BATCH_RESPONSE = """--batch_foobarbaz
200Content-Type: application/http
201Content-Transfer-Encoding: binary
202Content-ID: <randomness+1>
203
204HTTP/1.1 200 OK
205Content-Type application/json
206Content-Length: 14
207ETag: "etag/pony"\r\n\r\n{"foo": 42}
208
209--batch_foobarbaz
210Content-Type: application/http
211Content-Transfer-Encoding: binary
212Content-ID: <randomness+2>
213
214HTTP/1.1 200 OK
215Content-Type application/json
216Content-Length: 14
217ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
218--batch_foobarbaz--"""
219
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500220
Joe Gregorio654f4a22012-02-09 14:15:44 -0500221BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz
222Content-Type: application/http
223Content-Transfer-Encoding: binary
224Content-ID: <randomness+1>
225
226HTTP/1.1 401 Authoration Required
227Content-Type application/json
228Content-Length: 14
229ETag: "etag/pony"\r\n\r\n{"error": {"message":
230 "Authorizaton failed."}}
231
232--batch_foobarbaz
233Content-Type: application/http
234Content-Transfer-Encoding: binary
235Content-ID: <randomness+2>
236
237HTTP/1.1 200 OK
238Content-Type application/json
239Content-Length: 14
240ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
241--batch_foobarbaz--"""
242
243
244BATCH_SINGLE_RESPONSE = """--batch_foobarbaz
245Content-Type: application/http
246Content-Transfer-Encoding: binary
247Content-ID: <randomness+1>
248
249HTTP/1.1 200 OK
250Content-Type application/json
251Content-Length: 14
252ETag: "etag/pony"\r\n\r\n{"foo": 42}
253--batch_foobarbaz--"""
254
255class Callbacks(object):
256 def __init__(self):
257 self.responses = {}
258 self.exceptions = {}
259
260 def f(self, request_id, response, exception):
261 self.responses[request_id] = response
262 self.exceptions[request_id] = exception
263
264
Joe Gregorio66f57522011-11-30 11:00:00 -0500265class TestBatch(unittest.TestCase):
266
267 def setUp(self):
268 model = JsonModel()
269 self.request1 = HttpRequest(
270 None,
271 model.response,
272 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
273 method='POST',
274 body='{}',
275 headers={'content-type': 'application/json'})
276
277 self.request2 = HttpRequest(
278 None,
279 model.response,
280 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500281 method='GET',
282 body='',
Joe Gregorio66f57522011-11-30 11:00:00 -0500283 headers={'content-type': 'application/json'})
284
285
286 def test_id_to_from_content_id_header(self):
287 batch = BatchHttpRequest()
288 self.assertEquals('12', batch._header_to_id(batch._id_to_header('12')))
289
290 def test_invalid_content_id_header(self):
291 batch = BatchHttpRequest()
292 self.assertRaises(BatchError, batch._header_to_id, '[foo+x]')
293 self.assertRaises(BatchError, batch._header_to_id, 'foo+1')
294 self.assertRaises(BatchError, batch._header_to_id, '<foo>')
295
296 def test_serialize_request(self):
297 batch = BatchHttpRequest()
298 request = HttpRequest(
299 None,
300 None,
301 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
302 method='POST',
303 body='{}',
304 headers={'content-type': 'application/json'},
305 methodId=None,
306 resumable=None)
307 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500308 self.assertEqual(EXPECTED.splitlines(), s)
Joe Gregorio66f57522011-11-30 11:00:00 -0500309
Joe Gregoriodd813822012-01-25 10:32:47 -0500310 def test_serialize_request_media_body(self):
311 batch = BatchHttpRequest()
312 f = open(datafile('small.png'))
313 body = f.read()
314 f.close()
315
316 request = HttpRequest(
317 None,
318 None,
319 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
320 method='POST',
321 body=body,
322 headers={'content-type': 'application/json'},
323 methodId=None,
324 resumable=None)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500325 # Just testing it shouldn't raise an exception.
Joe Gregoriodd813822012-01-25 10:32:47 -0500326 s = batch._serialize_request(request).splitlines()
327
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500328 def test_serialize_request_no_body(self):
329 batch = BatchHttpRequest()
330 request = HttpRequest(
331 None,
332 None,
333 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
334 method='POST',
335 body='',
336 headers={'content-type': 'application/json'},
337 methodId=None,
338 resumable=None)
339 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500340 self.assertEqual(NO_BODY_EXPECTED.splitlines(), s)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500341
Joe Gregorio66f57522011-11-30 11:00:00 -0500342 def test_deserialize_response(self):
343 batch = BatchHttpRequest()
344 resp, content = batch._deserialize_response(RESPONSE)
345
Joe Gregorio654f4a22012-02-09 14:15:44 -0500346 self.assertEqual(200, resp.status)
347 self.assertEqual('OK', resp.reason)
348 self.assertEqual(11, resp.version)
349 self.assertEqual('{"answer": 42}', content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500350
351 def test_new_id(self):
352 batch = BatchHttpRequest()
353
354 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500355 self.assertEqual('1', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500356
357 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500358 self.assertEqual('2', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500359
360 batch.add(self.request1, request_id='3')
361
362 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500363 self.assertEqual('4', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500364
365 def test_add(self):
366 batch = BatchHttpRequest()
367 batch.add(self.request1, request_id='1')
368 self.assertRaises(KeyError, batch.add, self.request1, request_id='1')
369
370 def test_add_fail_for_resumable(self):
371 batch = BatchHttpRequest()
372
373 upload = MediaFileUpload(
374 datafile('small.png'), chunksize=500, resumable=True)
375 self.request1.resumable = upload
376 self.assertRaises(BatchError, batch.add, self.request1, request_id='1')
377
378 def test_execute(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500379 batch = BatchHttpRequest()
380 callbacks = Callbacks()
381
382 batch.add(self.request1, callback=callbacks.f)
383 batch.add(self.request2, callback=callbacks.f)
384 http = HttpMockSequence([
385 ({'status': '200',
386 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
387 BATCH_RESPONSE),
388 ])
389 batch.execute(http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500390 self.assertEqual({'foo': 42}, callbacks.responses['1'])
391 self.assertEqual(None, callbacks.exceptions['1'])
392 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
393 self.assertEqual(None, callbacks.exceptions['2'])
Joe Gregorio66f57522011-11-30 11:00:00 -0500394
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500395 def test_execute_request_body(self):
396 batch = BatchHttpRequest()
397
398 batch.add(self.request1)
399 batch.add(self.request2)
400 http = HttpMockSequence([
401 ({'status': '200',
402 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
403 'echo_request_body'),
404 ])
405 try:
406 batch.execute(http)
407 self.fail('Should raise exception')
408 except BatchError, e:
409 boundary, _ = e.content.split(None, 1)
410 self.assertEqual('--', boundary[:2])
411 parts = e.content.split(boundary)
412 self.assertEqual(4, len(parts))
413 self.assertEqual('', parts[0])
414 self.assertEqual('--', parts[3])
415 header = parts[1].splitlines()[1]
416 self.assertEqual('Content-Type: application/http', header)
417
Joe Gregorio654f4a22012-02-09 14:15:44 -0500418 def test_execute_refresh_and_retry_on_401(self):
419 batch = BatchHttpRequest()
420 callbacks = Callbacks()
421 cred_1 = MockCredentials('Foo')
422 cred_2 = MockCredentials('Bar')
423
424 http = HttpMockSequence([
425 ({'status': '200',
426 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
427 BATCH_RESPONSE_WITH_401),
428 ({'status': '200',
429 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
430 BATCH_SINGLE_RESPONSE),
431 ])
432
433 creds_http_1 = HttpMockSequence([])
434 cred_1.authorize(creds_http_1)
435
436 creds_http_2 = HttpMockSequence([])
437 cred_2.authorize(creds_http_2)
438
439 self.request1.http = creds_http_1
440 self.request2.http = creds_http_2
441
442 batch.add(self.request1, callback=callbacks.f)
443 batch.add(self.request2, callback=callbacks.f)
444 batch.execute(http)
445
446 self.assertEqual({'foo': 42}, callbacks.responses['1'])
447 self.assertEqual(None, callbacks.exceptions['1'])
448 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
449 self.assertEqual(None, callbacks.exceptions['2'])
450
451 self.assertEqual(1, cred_1._refreshed)
452 self.assertEqual(0, cred_2._refreshed)
453
454 self.assertEqual(1, cred_1._authorized)
455 self.assertEqual(1, cred_2._authorized)
456
457 self.assertEqual(1, cred_2._applied)
458 self.assertEqual(2, cred_1._applied)
459
460 def test_http_errors_passed_to_callback(self):
461 batch = BatchHttpRequest()
462 callbacks = Callbacks()
463 cred_1 = MockCredentials('Foo')
464 cred_2 = MockCredentials('Bar')
465
466 http = HttpMockSequence([
467 ({'status': '200',
468 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
469 BATCH_RESPONSE_WITH_401),
470 ({'status': '200',
471 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
472 BATCH_RESPONSE_WITH_401),
473 ])
474
475 creds_http_1 = HttpMockSequence([])
476 cred_1.authorize(creds_http_1)
477
478 creds_http_2 = HttpMockSequence([])
479 cred_2.authorize(creds_http_2)
480
481 self.request1.http = creds_http_1
482 self.request2.http = creds_http_2
483
484 batch.add(self.request1, callback=callbacks.f)
485 batch.add(self.request2, callback=callbacks.f)
486 batch.execute(http)
487
488 self.assertEqual(None, callbacks.responses['1'])
489 self.assertEqual(401, callbacks.exceptions['1'].resp.status)
490 self.assertEqual({u'baz': u'qux'}, callbacks.responses['2'])
491 self.assertEqual(None, callbacks.exceptions['2'])
492
Joe Gregorio66f57522011-11-30 11:00:00 -0500493 def test_execute_global_callback(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500494 callbacks = Callbacks()
495 batch = BatchHttpRequest(callback=callbacks.f)
496
497 batch.add(self.request1)
498 batch.add(self.request2)
499 http = HttpMockSequence([
500 ({'status': '200',
501 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
502 BATCH_RESPONSE),
503 ])
504 batch.execute(http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500505 self.assertEqual({'foo': 42}, callbacks.responses['1'])
506 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500507
Ali Afshar6f11ea12012-02-07 10:32:14 -0500508
509
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500510if __name__ == '__main__':
511 unittest.main()