blob: 4ef0dfb9822dbdac5e72914f62f97ad338a9df9a [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 Gregorio9086bd32013-06-14 16:32:05 -040026import logging
Joe Gregoriod0bd3882011-11-22 09:49:47 -050027import os
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050028import unittest
Joe Gregorioba5c7902012-08-03 12:48:16 -040029import urllib
Joe Gregorio9086bd32013-06-14 16:32:05 -040030import random
Joe Gregorio910b9b12012-06-12 09:36:30 -040031import StringIO
Joe Gregorio9086bd32013-06-14 16:32:05 -040032import time
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050033
Joe Gregorio708388c2012-06-15 13:43:04 -040034from apiclient.discovery import build
Joe Gregorio66f57522011-11-30 11:00:00 -050035from apiclient.errors import BatchError
Joe Gregorio708388c2012-06-15 13:43:04 -040036from apiclient.errors import HttpError
Joe Gregorioc80ac9d2012-08-21 14:09:09 -040037from apiclient.errors import InvalidChunkSizeError
Joe Gregorio66f57522011-11-30 11:00:00 -050038from apiclient.http import BatchHttpRequest
Joe Gregorio708388c2012-06-15 13:43:04 -040039from apiclient.http import HttpMock
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050040from apiclient.http import HttpMockSequence
Joe Gregoriod0bd3882011-11-22 09:49:47 -050041from apiclient.http import HttpRequest
Joe Gregorioc80ac9d2012-08-21 14:09:09 -040042from apiclient.http import MAX_URI_LENGTH
Joe Gregorio5c120db2012-08-23 09:13:55 -040043from apiclient.http import MediaFileUpload
44from apiclient.http import MediaInMemoryUpload
45from apiclient.http import MediaIoBaseDownload
46from apiclient.http import MediaIoBaseUpload
47from apiclient.http import MediaUpload
48from apiclient.http import _StreamSlice
49from apiclient.http import set_user_agent
Joe Gregorio66f57522011-11-30 11:00:00 -050050from apiclient.model import JsonModel
Joe Gregorio654f4a22012-02-09 14:15:44 -050051from oauth2client.client import Credentials
52
53
54class MockCredentials(Credentials):
55 """Mock class for all Credentials objects."""
56 def __init__(self, bearer_token):
57 super(MockCredentials, self).__init__()
58 self._authorized = 0
59 self._refreshed = 0
60 self._applied = 0
61 self._bearer_token = bearer_token
62
63 def authorize(self, http):
64 self._authorized += 1
65
66 request_orig = http.request
67
68 # The closure that will replace 'httplib2.Http.request'.
69 def new_request(uri, method='GET', body=None, headers=None,
70 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
71 connection_type=None):
72 # Modify the request headers to add the appropriate
73 # Authorization header.
74 if headers is None:
75 headers = {}
76 self.apply(headers)
77
78 resp, content = request_orig(uri, method, body, headers,
79 redirections, connection_type)
80
81 return resp, content
82
83 # Replace the request method with our own closure.
84 http.request = new_request
85
86 # Set credentials as a property of the request method.
87 setattr(http.request, 'credentials', self)
88
89 return http
90
91 def refresh(self, http):
92 self._refreshed += 1
93
94 def apply(self, headers):
95 self._applied += 1
96 headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050097
98
Joe Gregoriod0bd3882011-11-22 09:49:47 -050099DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
100
101
102def datafile(filename):
103 return os.path.join(DATA_DIR, filename)
104
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500105class TestUserAgent(unittest.TestCase):
106
107 def test_set_user_agent(self):
108 http = HttpMockSequence([
109 ({'status': '200'}, 'echo_request_headers'),
110 ])
111
112 http = set_user_agent(http, "my_app/5.5")
113 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500114 self.assertEqual('my_app/5.5', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500115
116 def test_set_user_agent_nested(self):
117 http = HttpMockSequence([
118 ({'status': '200'}, 'echo_request_headers'),
119 ])
120
121 http = set_user_agent(http, "my_app/5.5")
122 http = set_user_agent(http, "my_library/0.1")
123 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500124 self.assertEqual('my_app/5.5 my_library/0.1', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500125
Joe Gregorio910b9b12012-06-12 09:36:30 -0400126
127class TestMediaUpload(unittest.TestCase):
128
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500129 def test_media_file_upload_to_from_json(self):
130 upload = MediaFileUpload(
131 datafile('small.png'), chunksize=500, resumable=True)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500132 self.assertEqual('image/png', upload.mimetype())
133 self.assertEqual(190, upload.size())
134 self.assertEqual(True, upload.resumable())
135 self.assertEqual(500, upload.chunksize())
136 self.assertEqual('PNG', upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500137
138 json = upload.to_json()
139 new_upload = MediaUpload.new_from_json(json)
140
Joe Gregorio654f4a22012-02-09 14:15:44 -0500141 self.assertEqual('image/png', new_upload.mimetype())
142 self.assertEqual(190, new_upload.size())
143 self.assertEqual(True, new_upload.resumable())
144 self.assertEqual(500, new_upload.chunksize())
145 self.assertEqual('PNG', new_upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500146
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400147 def test_media_file_upload_raises_on_invalid_chunksize(self):
148 self.assertRaises(InvalidChunkSizeError, MediaFileUpload,
149 datafile('small.png'), mimetype='image/png', chunksize=-2,
150 resumable=True)
151
Ali Afshar1cb6b672012-03-12 08:46:14 -0400152 def test_media_inmemory_upload(self):
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400153 media = MediaInMemoryUpload('abcdef', mimetype='text/plain', chunksize=10,
Ali Afshar1cb6b672012-03-12 08:46:14 -0400154 resumable=True)
155 self.assertEqual('text/plain', media.mimetype())
156 self.assertEqual(10, media.chunksize())
157 self.assertTrue(media.resumable())
158 self.assertEqual('bc', media.getbytes(1, 2))
159 self.assertEqual(6, media.size())
160
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500161 def test_http_request_to_from_json(self):
162
163 def _postproc(*kwargs):
164 pass
165
166 http = httplib2.Http()
167 media_upload = MediaFileUpload(
168 datafile('small.png'), chunksize=500, resumable=True)
169 req = HttpRequest(
170 http,
171 _postproc,
172 'http://example.com',
173 method='POST',
174 body='{}',
175 headers={'content-type': 'multipart/related; boundary="---flubber"'},
176 methodId='foo',
177 resumable=media_upload)
178
179 json = req.to_json()
180 new_req = HttpRequest.from_json(json, http, _postproc)
181
Joe Gregorio654f4a22012-02-09 14:15:44 -0500182 self.assertEqual({'content-type':
183 'multipart/related; boundary="---flubber"'},
184 new_req.headers)
185 self.assertEqual('http://example.com', new_req.uri)
186 self.assertEqual('{}', new_req.body)
187 self.assertEqual(http, new_req.http)
188 self.assertEqual(media_upload.to_json(), new_req.resumable.to_json())
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500189
Joe Gregorio9086bd32013-06-14 16:32:05 -0400190 self.assertEqual(random.random, new_req._rand)
191 self.assertEqual(time.sleep, new_req._sleep)
192
Joe Gregorio910b9b12012-06-12 09:36:30 -0400193
194class TestMediaIoBaseUpload(unittest.TestCase):
195
196 def test_media_io_base_upload_from_file_io(self):
197 try:
198 import io
199
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400200 fd = io.FileIO(datafile('small.png'), 'r')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400201 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400202 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400203 self.assertEqual('image/png', upload.mimetype())
204 self.assertEqual(190, upload.size())
205 self.assertEqual(True, upload.resumable())
206 self.assertEqual(500, upload.chunksize())
207 self.assertEqual('PNG', upload.getbytes(1, 3))
208 except ImportError:
209 pass
210
211 def test_media_io_base_upload_from_file_object(self):
212 f = open(datafile('small.png'), 'r')
213 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400214 fd=f, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400215 self.assertEqual('image/png', upload.mimetype())
216 self.assertEqual(190, upload.size())
217 self.assertEqual(True, upload.resumable())
218 self.assertEqual(500, upload.chunksize())
219 self.assertEqual('PNG', upload.getbytes(1, 3))
220 f.close()
221
222 def test_media_io_base_upload_serializable(self):
223 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400224 upload = MediaIoBaseUpload(fd=f, mimetype='image/png')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400225
226 try:
227 json = upload.to_json()
228 self.fail('MediaIoBaseUpload should not be serializable.')
229 except NotImplementedError:
230 pass
231
232 def test_media_io_base_upload_from_string_io(self):
233 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400234 fd = StringIO.StringIO(f.read())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400235 f.close()
236
237 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400238 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400239 self.assertEqual('image/png', upload.mimetype())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400240 self.assertEqual(190, upload.size())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400241 self.assertEqual(True, upload.resumable())
242 self.assertEqual(500, upload.chunksize())
243 self.assertEqual('PNG', upload.getbytes(1, 3))
244 f.close()
245
246 def test_media_io_base_upload_from_bytes(self):
247 try:
248 import io
249
250 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400251 fd = io.BytesIO(f.read())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400252 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400253 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400254 self.assertEqual('image/png', upload.mimetype())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400255 self.assertEqual(190, upload.size())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400256 self.assertEqual(True, upload.resumable())
257 self.assertEqual(500, upload.chunksize())
258 self.assertEqual('PNG', upload.getbytes(1, 3))
259 except ImportError:
260 pass
261
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400262 def test_media_io_base_upload_raises_on_invalid_chunksize(self):
263 try:
264 import io
265
266 f = open(datafile('small.png'), 'r')
267 fd = io.BytesIO(f.read())
268 self.assertRaises(InvalidChunkSizeError, MediaIoBaseUpload,
269 fd, 'image/png', chunksize=-2, resumable=True)
270 except ImportError:
271 pass
272
273 def test_media_io_base_upload_streamable(self):
274 try:
275 import io
276
277 fd = io.BytesIO('stuff')
278 upload = MediaIoBaseUpload(
279 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
280 self.assertEqual(True, upload.has_stream())
281 self.assertEqual(fd, upload.stream())
282 except ImportError:
283 pass
284
Joe Gregorio9086bd32013-06-14 16:32:05 -0400285 def test_media_io_base_next_chunk_retries(self):
286 try:
287 import io
288 except ImportError:
289 return
290
291 f = open(datafile('small.png'), 'r')
292 fd = io.BytesIO(f.read())
293 upload = MediaIoBaseUpload(
294 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
295
296 # Simulate 5XXs for both the request that creates the resumable upload and
297 # the upload itself.
298 http = HttpMockSequence([
299 ({'status': '500'}, ''),
300 ({'status': '500'}, ''),
301 ({'status': '503'}, ''),
302 ({'status': '200', 'location': 'location'}, ''),
303 ({'status': '500'}, ''),
304 ({'status': '500'}, ''),
305 ({'status': '503'}, ''),
306 ({'status': '200'}, '{}'),
307 ])
308
309 model = JsonModel()
310 uri = u'https://www.googleapis.com/someapi/v1/upload/?foo=bar'
311 method = u'POST'
312 request = HttpRequest(
313 http,
314 model.response,
315 uri,
316 method=method,
317 headers={},
318 resumable=upload)
319
320 sleeptimes = []
321 request._sleep = lambda x: sleeptimes.append(x)
322 request._rand = lambda: 10
323
324 request.execute(num_retries=3)
325 self.assertEqual([20, 40, 80, 20, 40, 80], sleeptimes)
326
Joe Gregorio910b9b12012-06-12 09:36:30 -0400327
Joe Gregorio708388c2012-06-15 13:43:04 -0400328class TestMediaIoBaseDownload(unittest.TestCase):
329
330 def setUp(self):
331 http = HttpMock(datafile('zoo.json'), {'status': '200'})
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400332 zoo = build('zoo', 'v1', http=http)
Joe Gregorio708388c2012-06-15 13:43:04 -0400333 self.request = zoo.animals().get_media(name='Lion')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400334 self.fd = StringIO.StringIO()
Joe Gregorio708388c2012-06-15 13:43:04 -0400335
336 def test_media_io_base_download(self):
337 self.request.http = HttpMockSequence([
338 ({'status': '200',
339 'content-range': '0-2/5'}, '123'),
340 ({'status': '200',
341 'content-range': '3-4/5'}, '45'),
342 ])
Joe Gregorio97ef1cc2013-06-13 14:47:10 -0400343 self.assertEqual(True, self.request.http.follow_redirects)
Joe Gregorio708388c2012-06-15 13:43:04 -0400344
345 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400346 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400347
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400348 self.assertEqual(self.fd, download._fd)
349 self.assertEqual(3, download._chunksize)
350 self.assertEqual(0, download._progress)
351 self.assertEqual(None, download._total_size)
352 self.assertEqual(False, download._done)
353 self.assertEqual(self.request.uri, download._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400354
355 status, done = download.next_chunk()
356
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400357 self.assertEqual(self.fd.getvalue(), '123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400358 self.assertEqual(False, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400359 self.assertEqual(3, download._progress)
360 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400361 self.assertEqual(3, status.resumable_progress)
362
363 status, done = download.next_chunk()
364
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400365 self.assertEqual(self.fd.getvalue(), '12345')
Joe Gregorio708388c2012-06-15 13:43:04 -0400366 self.assertEqual(True, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400367 self.assertEqual(5, download._progress)
368 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400369
370 def test_media_io_base_download_handle_redirects(self):
371 self.request.http = HttpMockSequence([
Joe Gregorio238feb72013-06-19 13:15:31 -0400372 ({'status': '200',
373 'content-location': 'https://secure.example.net/lion'}, ''),
Joe Gregorio708388c2012-06-15 13:43:04 -0400374 ({'status': '200',
375 'content-range': '0-2/5'}, 'abc'),
376 ])
377
378 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400379 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400380
381 status, done = download.next_chunk()
382
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400383 self.assertEqual('https://secure.example.net/lion', download._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400384
385 def test_media_io_base_download_handle_4xx(self):
386 self.request.http = HttpMockSequence([
387 ({'status': '400'}, ''),
388 ])
389
390 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400391 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400392
393 try:
394 status, done = download.next_chunk()
395 self.fail('Should raise an exception')
396 except HttpError:
397 pass
398
399 # Even after raising an exception we can pick up where we left off.
400 self.request.http = HttpMockSequence([
401 ({'status': '200',
402 'content-range': '0-2/5'}, '123'),
403 ])
404
405 status, done = download.next_chunk()
406
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400407 self.assertEqual(self.fd.getvalue(), '123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400408
Joe Gregorio9086bd32013-06-14 16:32:05 -0400409 def test_media_io_base_download_retries_5xx(self):
410 self.request.http = HttpMockSequence([
411 ({'status': '500'}, ''),
412 ({'status': '500'}, ''),
413 ({'status': '500'}, ''),
414 ({'status': '200',
415 'content-range': '0-2/5'}, '123'),
416 ({'status': '503'}, ''),
417 ({'status': '503'}, ''),
418 ({'status': '503'}, ''),
419 ({'status': '200',
420 'content-range': '3-4/5'}, '45'),
421 ])
422
423 download = MediaIoBaseDownload(
424 fd=self.fd, request=self.request, chunksize=3)
425
426 self.assertEqual(self.fd, download._fd)
427 self.assertEqual(3, download._chunksize)
428 self.assertEqual(0, download._progress)
429 self.assertEqual(None, download._total_size)
430 self.assertEqual(False, download._done)
431 self.assertEqual(self.request.uri, download._uri)
432
433 # Set time.sleep and random.random stubs.
434 sleeptimes = []
435 download._sleep = lambda x: sleeptimes.append(x)
436 download._rand = lambda: 10
437
438 status, done = download.next_chunk(num_retries=3)
439
440 # Check for exponential backoff using the rand function above.
441 self.assertEqual([20, 40, 80], sleeptimes)
442
443 self.assertEqual(self.fd.getvalue(), '123')
444 self.assertEqual(False, done)
445 self.assertEqual(3, download._progress)
446 self.assertEqual(5, download._total_size)
447 self.assertEqual(3, status.resumable_progress)
448
449 # Reset time.sleep stub.
450 del sleeptimes[0:len(sleeptimes)]
451
452 status, done = download.next_chunk(num_retries=3)
453
454 # Check for exponential backoff using the rand function above.
455 self.assertEqual([20, 40, 80], sleeptimes)
456
457 self.assertEqual(self.fd.getvalue(), '12345')
458 self.assertEqual(True, done)
459 self.assertEqual(5, download._progress)
460 self.assertEqual(5, download._total_size)
461
Joe Gregorio66f57522011-11-30 11:00:00 -0500462EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
463Content-Type: application/json
464MIME-Version: 1.0
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500465Host: www.googleapis.com
466content-length: 2\r\n\r\n{}"""
467
468
469NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
470Content-Type: application/json
471MIME-Version: 1.0
472Host: www.googleapis.com
473content-length: 0\r\n\r\n"""
Joe Gregorio66f57522011-11-30 11:00:00 -0500474
475
476RESPONSE = """HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400477Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500478Content-Length: 14
479ETag: "etag/pony"\r\n\r\n{"answer": 42}"""
480
481
482BATCH_RESPONSE = """--batch_foobarbaz
483Content-Type: application/http
484Content-Transfer-Encoding: binary
485Content-ID: <randomness+1>
486
487HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400488Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500489Content-Length: 14
490ETag: "etag/pony"\r\n\r\n{"foo": 42}
491
492--batch_foobarbaz
493Content-Type: application/http
494Content-Transfer-Encoding: binary
495Content-ID: <randomness+2>
496
497HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400498Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500499Content-Length: 14
500ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
501--batch_foobarbaz--"""
502
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500503
Joe Gregorio3fb93672012-07-25 11:31:11 -0400504BATCH_ERROR_RESPONSE = """--batch_foobarbaz
505Content-Type: application/http
506Content-Transfer-Encoding: binary
507Content-ID: <randomness+1>
508
509HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400510Content-Type: application/json
Joe Gregorio3fb93672012-07-25 11:31:11 -0400511Content-Length: 14
512ETag: "etag/pony"\r\n\r\n{"foo": 42}
513
514--batch_foobarbaz
515Content-Type: application/http
516Content-Transfer-Encoding: binary
517Content-ID: <randomness+2>
518
519HTTP/1.1 403 Access Not Configured
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400520Content-Type: application/json
521Content-Length: 245
522ETag: "etag/sheep"\r\n\r\n{
Joe Gregorio3fb93672012-07-25 11:31:11 -0400523 "error": {
524 "errors": [
525 {
526 "domain": "usageLimits",
527 "reason": "accessNotConfigured",
528 "message": "Access Not Configured",
529 "debugInfo": "QuotaState: BLOCKED"
530 }
531 ],
532 "code": 403,
533 "message": "Access Not Configured"
534 }
535}
536
537--batch_foobarbaz--"""
538
539
Joe Gregorio654f4a22012-02-09 14:15:44 -0500540BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz
541Content-Type: application/http
542Content-Transfer-Encoding: binary
543Content-ID: <randomness+1>
544
Joe Gregorioc752e332012-07-11 14:43:52 -0400545HTTP/1.1 401 Authorization Required
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400546Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500547Content-Length: 14
548ETag: "etag/pony"\r\n\r\n{"error": {"message":
549 "Authorizaton failed."}}
550
551--batch_foobarbaz
552Content-Type: application/http
553Content-Transfer-Encoding: binary
554Content-ID: <randomness+2>
555
556HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400557Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500558Content-Length: 14
559ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
560--batch_foobarbaz--"""
561
562
563BATCH_SINGLE_RESPONSE = """--batch_foobarbaz
564Content-Type: application/http
565Content-Transfer-Encoding: binary
566Content-ID: <randomness+1>
567
568HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400569Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500570Content-Length: 14
571ETag: "etag/pony"\r\n\r\n{"foo": 42}
572--batch_foobarbaz--"""
573
574class Callbacks(object):
575 def __init__(self):
576 self.responses = {}
577 self.exceptions = {}
578
579 def f(self, request_id, response, exception):
580 self.responses[request_id] = response
581 self.exceptions[request_id] = exception
582
583
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500584class TestHttpRequest(unittest.TestCase):
585 def test_unicode(self):
586 http = HttpMock(datafile('zoo.json'), headers={'status': '200'})
587 model = JsonModel()
588 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
589 method = u'POST'
590 request = HttpRequest(
591 http,
592 model.response,
593 uri,
594 method=method,
595 body=u'{}',
596 headers={'content-type': 'application/json'})
597 request.execute()
598 self.assertEqual(uri, http.uri)
599 self.assertEqual(str, type(http.uri))
600 self.assertEqual(method, http.method)
601 self.assertEqual(str, type(http.method))
602
Joe Gregorio9086bd32013-06-14 16:32:05 -0400603 def test_retry(self):
604 num_retries = 5
605 resp_seq = [({'status': '500'}, '')] * num_retries
606 resp_seq.append(({'status': '200'}, '{}'))
607
608 http = HttpMockSequence(resp_seq)
609 model = JsonModel()
610 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
611 method = u'POST'
612 request = HttpRequest(
613 http,
614 model.response,
615 uri,
616 method=method,
617 body=u'{}',
618 headers={'content-type': 'application/json'})
619
620 sleeptimes = []
621 request._sleep = lambda x: sleeptimes.append(x)
622 request._rand = lambda: 10
623
624 request.execute(num_retries=num_retries)
625
626 self.assertEqual(num_retries, len(sleeptimes))
627 for retry_num in xrange(num_retries):
628 self.assertEqual(10 * 2**(retry_num + 1), sleeptimes[retry_num])
629
630 def test_no_retry_fails_fast(self):
631 http = HttpMockSequence([
632 ({'status': '500'}, ''),
633 ({'status': '200'}, '{}')
634 ])
635 model = JsonModel()
636 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
637 method = u'POST'
638 request = HttpRequest(
639 http,
640 model.response,
641 uri,
642 method=method,
643 body=u'{}',
644 headers={'content-type': 'application/json'})
645
646 request._rand = lambda: 1.0
647 request._sleep = lambda _: self.fail('sleep should not have been called.')
648
649 try:
650 request.execute()
651 self.fail('Should have raised an exception.')
652 except HttpError:
653 pass
654
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500655
Joe Gregorio66f57522011-11-30 11:00:00 -0500656class TestBatch(unittest.TestCase):
657
658 def setUp(self):
659 model = JsonModel()
660 self.request1 = HttpRequest(
661 None,
662 model.response,
663 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
664 method='POST',
665 body='{}',
666 headers={'content-type': 'application/json'})
667
668 self.request2 = HttpRequest(
669 None,
670 model.response,
671 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500672 method='GET',
673 body='',
Joe Gregorio66f57522011-11-30 11:00:00 -0500674 headers={'content-type': 'application/json'})
675
676
677 def test_id_to_from_content_id_header(self):
678 batch = BatchHttpRequest()
679 self.assertEquals('12', batch._header_to_id(batch._id_to_header('12')))
680
681 def test_invalid_content_id_header(self):
682 batch = BatchHttpRequest()
683 self.assertRaises(BatchError, batch._header_to_id, '[foo+x]')
684 self.assertRaises(BatchError, batch._header_to_id, 'foo+1')
685 self.assertRaises(BatchError, batch._header_to_id, '<foo>')
686
687 def test_serialize_request(self):
688 batch = BatchHttpRequest()
689 request = HttpRequest(
690 None,
691 None,
692 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
693 method='POST',
694 body='{}',
695 headers={'content-type': 'application/json'},
696 methodId=None,
697 resumable=None)
698 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500699 self.assertEqual(EXPECTED.splitlines(), s)
Joe Gregorio66f57522011-11-30 11:00:00 -0500700
Joe Gregoriodd813822012-01-25 10:32:47 -0500701 def test_serialize_request_media_body(self):
702 batch = BatchHttpRequest()
703 f = open(datafile('small.png'))
704 body = f.read()
705 f.close()
706
707 request = HttpRequest(
708 None,
709 None,
710 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
711 method='POST',
712 body=body,
713 headers={'content-type': 'application/json'},
714 methodId=None,
715 resumable=None)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500716 # Just testing it shouldn't raise an exception.
Joe Gregoriodd813822012-01-25 10:32:47 -0500717 s = batch._serialize_request(request).splitlines()
718
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500719 def test_serialize_request_no_body(self):
720 batch = BatchHttpRequest()
721 request = HttpRequest(
722 None,
723 None,
724 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
725 method='POST',
726 body='',
727 headers={'content-type': 'application/json'},
728 methodId=None,
729 resumable=None)
730 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500731 self.assertEqual(NO_BODY_EXPECTED.splitlines(), s)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500732
Joe Gregorio66f57522011-11-30 11:00:00 -0500733 def test_deserialize_response(self):
734 batch = BatchHttpRequest()
735 resp, content = batch._deserialize_response(RESPONSE)
736
Joe Gregorio654f4a22012-02-09 14:15:44 -0500737 self.assertEqual(200, resp.status)
738 self.assertEqual('OK', resp.reason)
739 self.assertEqual(11, resp.version)
740 self.assertEqual('{"answer": 42}', content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500741
742 def test_new_id(self):
743 batch = BatchHttpRequest()
744
745 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500746 self.assertEqual('1', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500747
748 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500749 self.assertEqual('2', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500750
751 batch.add(self.request1, request_id='3')
752
753 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500754 self.assertEqual('4', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500755
756 def test_add(self):
757 batch = BatchHttpRequest()
758 batch.add(self.request1, request_id='1')
759 self.assertRaises(KeyError, batch.add, self.request1, request_id='1')
760
761 def test_add_fail_for_resumable(self):
762 batch = BatchHttpRequest()
763
764 upload = MediaFileUpload(
765 datafile('small.png'), chunksize=500, resumable=True)
766 self.request1.resumable = upload
767 self.assertRaises(BatchError, batch.add, self.request1, request_id='1')
768
769 def test_execute(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500770 batch = BatchHttpRequest()
771 callbacks = Callbacks()
772
773 batch.add(self.request1, callback=callbacks.f)
774 batch.add(self.request2, callback=callbacks.f)
775 http = HttpMockSequence([
776 ({'status': '200',
777 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
778 BATCH_RESPONSE),
779 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400780 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500781 self.assertEqual({'foo': 42}, callbacks.responses['1'])
782 self.assertEqual(None, callbacks.exceptions['1'])
783 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
784 self.assertEqual(None, callbacks.exceptions['2'])
Joe Gregorio66f57522011-11-30 11:00:00 -0500785
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500786 def test_execute_request_body(self):
787 batch = BatchHttpRequest()
788
789 batch.add(self.request1)
790 batch.add(self.request2)
791 http = HttpMockSequence([
792 ({'status': '200',
793 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
794 'echo_request_body'),
795 ])
796 try:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400797 batch.execute(http=http)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500798 self.fail('Should raise exception')
799 except BatchError, e:
800 boundary, _ = e.content.split(None, 1)
801 self.assertEqual('--', boundary[:2])
802 parts = e.content.split(boundary)
803 self.assertEqual(4, len(parts))
804 self.assertEqual('', parts[0])
805 self.assertEqual('--', parts[3])
806 header = parts[1].splitlines()[1]
807 self.assertEqual('Content-Type: application/http', header)
808
Joe Gregorio654f4a22012-02-09 14:15:44 -0500809 def test_execute_refresh_and_retry_on_401(self):
810 batch = BatchHttpRequest()
811 callbacks = Callbacks()
812 cred_1 = MockCredentials('Foo')
813 cred_2 = MockCredentials('Bar')
814
815 http = HttpMockSequence([
816 ({'status': '200',
817 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
818 BATCH_RESPONSE_WITH_401),
819 ({'status': '200',
820 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
821 BATCH_SINGLE_RESPONSE),
822 ])
823
824 creds_http_1 = HttpMockSequence([])
825 cred_1.authorize(creds_http_1)
826
827 creds_http_2 = HttpMockSequence([])
828 cred_2.authorize(creds_http_2)
829
830 self.request1.http = creds_http_1
831 self.request2.http = creds_http_2
832
833 batch.add(self.request1, callback=callbacks.f)
834 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400835 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500836
837 self.assertEqual({'foo': 42}, callbacks.responses['1'])
838 self.assertEqual(None, callbacks.exceptions['1'])
839 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
840 self.assertEqual(None, callbacks.exceptions['2'])
841
842 self.assertEqual(1, cred_1._refreshed)
843 self.assertEqual(0, cred_2._refreshed)
844
845 self.assertEqual(1, cred_1._authorized)
846 self.assertEqual(1, cred_2._authorized)
847
848 self.assertEqual(1, cred_2._applied)
849 self.assertEqual(2, cred_1._applied)
850
851 def test_http_errors_passed_to_callback(self):
852 batch = BatchHttpRequest()
853 callbacks = Callbacks()
854 cred_1 = MockCredentials('Foo')
855 cred_2 = MockCredentials('Bar')
856
857 http = HttpMockSequence([
858 ({'status': '200',
859 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
860 BATCH_RESPONSE_WITH_401),
861 ({'status': '200',
862 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
863 BATCH_RESPONSE_WITH_401),
864 ])
865
866 creds_http_1 = HttpMockSequence([])
867 cred_1.authorize(creds_http_1)
868
869 creds_http_2 = HttpMockSequence([])
870 cred_2.authorize(creds_http_2)
871
872 self.request1.http = creds_http_1
873 self.request2.http = creds_http_2
874
875 batch.add(self.request1, callback=callbacks.f)
876 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400877 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500878
879 self.assertEqual(None, callbacks.responses['1'])
880 self.assertEqual(401, callbacks.exceptions['1'].resp.status)
Joe Gregorioc752e332012-07-11 14:43:52 -0400881 self.assertEqual(
882 'Authorization Required', callbacks.exceptions['1'].resp.reason)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500883 self.assertEqual({u'baz': u'qux'}, callbacks.responses['2'])
884 self.assertEqual(None, callbacks.exceptions['2'])
885
Joe Gregorio66f57522011-11-30 11:00:00 -0500886 def test_execute_global_callback(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500887 callbacks = Callbacks()
888 batch = BatchHttpRequest(callback=callbacks.f)
889
890 batch.add(self.request1)
891 batch.add(self.request2)
892 http = HttpMockSequence([
893 ({'status': '200',
894 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
895 BATCH_RESPONSE),
896 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400897 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500898 self.assertEqual({'foo': 42}, callbacks.responses['1'])
899 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500900
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400901 def test_execute_batch_http_error(self):
Joe Gregorio3fb93672012-07-25 11:31:11 -0400902 callbacks = Callbacks()
903 batch = BatchHttpRequest(callback=callbacks.f)
904
905 batch.add(self.request1)
906 batch.add(self.request2)
907 http = HttpMockSequence([
908 ({'status': '200',
909 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
910 BATCH_ERROR_RESPONSE),
911 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400912 batch.execute(http=http)
Joe Gregorio3fb93672012-07-25 11:31:11 -0400913 self.assertEqual({'foo': 42}, callbacks.responses['1'])
914 expected = ('<HttpError 403 when requesting '
915 'https://www.googleapis.com/someapi/v1/collection/?foo=bar returned '
916 '"Access Not Configured">')
917 self.assertEqual(expected, str(callbacks.exceptions['2']))
Ali Afshar6f11ea12012-02-07 10:32:14 -0500918
Joe Gregorio5c120db2012-08-23 09:13:55 -0400919
Joe Gregorioba5c7902012-08-03 12:48:16 -0400920class TestRequestUriTooLong(unittest.TestCase):
921
922 def test_turn_get_into_post(self):
923
924 def _postproc(resp, content):
925 return content
926
927 http = HttpMockSequence([
928 ({'status': '200'},
929 'echo_request_body'),
930 ({'status': '200'},
931 'echo_request_headers'),
932 ])
933
934 # Send a long query parameter.
935 query = {
936 'q': 'a' * MAX_URI_LENGTH + '?&'
937 }
938 req = HttpRequest(
939 http,
940 _postproc,
941 'http://example.com?' + urllib.urlencode(query),
942 method='GET',
943 body=None,
944 headers={},
945 methodId='foo',
946 resumable=None)
947
948 # Query parameters should be sent in the body.
949 response = req.execute()
950 self.assertEqual('q=' + 'a' * MAX_URI_LENGTH + '%3F%26', response)
951
952 # Extra headers should be set.
953 response = req.execute()
954 self.assertEqual('GET', response['x-http-method-override'])
955 self.assertEqual(str(MAX_URI_LENGTH + 8), response['content-length'])
956 self.assertEqual(
957 'application/x-www-form-urlencoded', response['content-type'])
958
Joe Gregorio5c120db2012-08-23 09:13:55 -0400959
960class TestStreamSlice(unittest.TestCase):
961 """Test _StreamSlice."""
962
963 def setUp(self):
964 self.stream = StringIO.StringIO('0123456789')
965
966 def test_read(self):
967 s = _StreamSlice(self.stream, 0, 4)
968 self.assertEqual('', s.read(0))
969 self.assertEqual('0', s.read(1))
970 self.assertEqual('123', s.read())
971
972 def test_read_too_much(self):
973 s = _StreamSlice(self.stream, 1, 4)
974 self.assertEqual('1234', s.read(6))
975
976 def test_read_all(self):
977 s = _StreamSlice(self.stream, 2, 1)
978 self.assertEqual('2', s.read(-1))
979
Ali Afshar164f37e2013-01-07 14:05:45 -0800980
981class TestResponseCallback(unittest.TestCase):
982 """Test adding callbacks to responses."""
983
984 def test_ensure_response_callback(self):
985 m = JsonModel()
986 request = HttpRequest(
987 None,
988 m.response,
989 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
990 method='POST',
991 body='{}',
992 headers={'content-type': 'application/json'})
993 h = HttpMockSequence([ ({'status': 200}, '{}')])
994 responses = []
995 def _on_response(resp, responses=responses):
996 responses.append(resp)
997 request.add_response_callback(_on_response)
998 request.execute(http=h)
999 self.assertEqual(1, len(responses))
1000
1001
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001002if __name__ == '__main__':
Joe Gregorio9086bd32013-06-14 16:32:05 -04001003 logging.getLogger().setLevel(logging.ERROR)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001004 unittest.main()