blob: bb21335cb0d9160afe03186b56357b86ae480c50 [file] [log] [blame]
Craig Citro15744b12015-03-02 13:34:32 -08001#!/usr/bin/env python
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05002#
Craig Citro751b7fb2014-09-23 11:20:38 -07003# Copyright 2014 Google Inc. All Rights Reserved.
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05004#
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
John Asmuth864311d2014-04-24 15:46:08 -040019Unit tests for the googleapiclient.http.
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050020"""
INADA Naokid898a372015-03-04 03:52:46 +090021from __future__ import absolute_import
22from six.moves import range
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050023
24__author__ = 'jcgregorio@google.com (Joe Gregorio)'
25
Pat Ferateed9affd2015-03-03 16:03:15 -080026from six import BytesIO, StringIO
27from io import FileIO
Pat Ferated5b61bd2015-03-03 16:04:11 -080028from six.moves.urllib.parse import urlencode
Pat Ferateed9affd2015-03-03 16:03:15 -080029
Joe Gregorio7cbceab2011-06-27 10:46:54 -040030# Do not remove the httplib2 import
31import httplib2
Joe Gregorio9086bd32013-06-14 16:32:05 -040032import logging
Joe Gregoriod0bd3882011-11-22 09:49:47 -050033import os
Pat Ferate497a90f2015-03-09 09:52:54 -070034import unittest2 as unittest
Joe Gregorio9086bd32013-06-14 16:32:05 -040035import random
Joe Gregorio9086bd32013-06-14 16:32:05 -040036import time
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050037
John Asmuth864311d2014-04-24 15:46:08 -040038from googleapiclient.discovery import build
39from googleapiclient.errors import BatchError
40from googleapiclient.errors import HttpError
41from googleapiclient.errors import InvalidChunkSizeError
42from googleapiclient.http import BatchHttpRequest
43from googleapiclient.http import HttpMock
44from googleapiclient.http import HttpMockSequence
45from googleapiclient.http import HttpRequest
46from googleapiclient.http import MAX_URI_LENGTH
47from googleapiclient.http import MediaFileUpload
48from googleapiclient.http import MediaInMemoryUpload
49from googleapiclient.http import MediaIoBaseDownload
50from googleapiclient.http import MediaIoBaseUpload
51from googleapiclient.http import MediaUpload
52from googleapiclient.http import _StreamSlice
53from googleapiclient.http import set_user_agent
54from googleapiclient.model import JsonModel
Joe Gregorio654f4a22012-02-09 14:15:44 -050055from oauth2client.client import Credentials
56
57
58class MockCredentials(Credentials):
59 """Mock class for all Credentials objects."""
60 def __init__(self, bearer_token):
61 super(MockCredentials, self).__init__()
62 self._authorized = 0
63 self._refreshed = 0
64 self._applied = 0
65 self._bearer_token = bearer_token
66
67 def authorize(self, http):
68 self._authorized += 1
69
70 request_orig = http.request
71
72 # The closure that will replace 'httplib2.Http.request'.
73 def new_request(uri, method='GET', body=None, headers=None,
74 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
75 connection_type=None):
76 # Modify the request headers to add the appropriate
77 # Authorization header.
78 if headers is None:
79 headers = {}
80 self.apply(headers)
81
82 resp, content = request_orig(uri, method, body, headers,
83 redirections, connection_type)
84
85 return resp, content
86
87 # Replace the request method with our own closure.
88 http.request = new_request
89
90 # Set credentials as a property of the request method.
91 setattr(http.request, 'credentials', self)
92
93 return http
94
95 def refresh(self, http):
96 self._refreshed += 1
97
98 def apply(self, headers):
99 self._applied += 1
100 headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500101
102
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500103DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
104
105
106def datafile(filename):
107 return os.path.join(DATA_DIR, filename)
108
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500109class TestUserAgent(unittest.TestCase):
110
111 def test_set_user_agent(self):
112 http = HttpMockSequence([
113 ({'status': '200'}, 'echo_request_headers'),
114 ])
115
116 http = set_user_agent(http, "my_app/5.5")
117 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500118 self.assertEqual('my_app/5.5', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500119
120 def test_set_user_agent_nested(self):
121 http = HttpMockSequence([
122 ({'status': '200'}, 'echo_request_headers'),
123 ])
124
125 http = set_user_agent(http, "my_app/5.5")
126 http = set_user_agent(http, "my_library/0.1")
127 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500128 self.assertEqual('my_app/5.5 my_library/0.1', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500129
Joe Gregorio910b9b12012-06-12 09:36:30 -0400130
131class TestMediaUpload(unittest.TestCase):
132
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500133 def test_media_file_upload_to_from_json(self):
134 upload = MediaFileUpload(
135 datafile('small.png'), chunksize=500, resumable=True)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500136 self.assertEqual('image/png', upload.mimetype())
137 self.assertEqual(190, upload.size())
138 self.assertEqual(True, upload.resumable())
139 self.assertEqual(500, upload.chunksize())
140 self.assertEqual('PNG', upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500141
142 json = upload.to_json()
143 new_upload = MediaUpload.new_from_json(json)
144
Joe Gregorio654f4a22012-02-09 14:15:44 -0500145 self.assertEqual('image/png', new_upload.mimetype())
146 self.assertEqual(190, new_upload.size())
147 self.assertEqual(True, new_upload.resumable())
148 self.assertEqual(500, new_upload.chunksize())
149 self.assertEqual('PNG', new_upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500150
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400151 def test_media_file_upload_raises_on_invalid_chunksize(self):
152 self.assertRaises(InvalidChunkSizeError, MediaFileUpload,
153 datafile('small.png'), mimetype='image/png', chunksize=-2,
154 resumable=True)
155
Ali Afshar1cb6b672012-03-12 08:46:14 -0400156 def test_media_inmemory_upload(self):
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400157 media = MediaInMemoryUpload('abcdef', mimetype='text/plain', chunksize=10,
Ali Afshar1cb6b672012-03-12 08:46:14 -0400158 resumable=True)
159 self.assertEqual('text/plain', media.mimetype())
160 self.assertEqual(10, media.chunksize())
161 self.assertTrue(media.resumable())
162 self.assertEqual('bc', media.getbytes(1, 2))
163 self.assertEqual(6, media.size())
164
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500165 def test_http_request_to_from_json(self):
166
167 def _postproc(*kwargs):
168 pass
169
170 http = httplib2.Http()
171 media_upload = MediaFileUpload(
172 datafile('small.png'), chunksize=500, resumable=True)
173 req = HttpRequest(
174 http,
175 _postproc,
176 'http://example.com',
177 method='POST',
178 body='{}',
179 headers={'content-type': 'multipart/related; boundary="---flubber"'},
180 methodId='foo',
181 resumable=media_upload)
182
183 json = req.to_json()
184 new_req = HttpRequest.from_json(json, http, _postproc)
185
Joe Gregorio654f4a22012-02-09 14:15:44 -0500186 self.assertEqual({'content-type':
187 'multipart/related; boundary="---flubber"'},
188 new_req.headers)
189 self.assertEqual('http://example.com', new_req.uri)
190 self.assertEqual('{}', new_req.body)
191 self.assertEqual(http, new_req.http)
192 self.assertEqual(media_upload.to_json(), new_req.resumable.to_json())
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500193
Joe Gregorio9086bd32013-06-14 16:32:05 -0400194 self.assertEqual(random.random, new_req._rand)
195 self.assertEqual(time.sleep, new_req._sleep)
196
Joe Gregorio910b9b12012-06-12 09:36:30 -0400197
198class TestMediaIoBaseUpload(unittest.TestCase):
199
200 def test_media_io_base_upload_from_file_io(self):
Pat Ferateed9affd2015-03-03 16:03:15 -0800201 fd = FileIO(datafile('small.png'), 'r')
202 upload = MediaIoBaseUpload(
203 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
204 self.assertEqual('image/png', upload.mimetype())
205 self.assertEqual(190, upload.size())
206 self.assertEqual(True, upload.resumable())
207 self.assertEqual(500, upload.chunksize())
208 self.assertEqual('PNG', upload.getbytes(1, 3))
Joe Gregorio910b9b12012-06-12 09:36:30 -0400209
210 def test_media_io_base_upload_from_file_object(self):
211 f = open(datafile('small.png'), 'r')
212 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400213 fd=f, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400214 self.assertEqual('image/png', upload.mimetype())
215 self.assertEqual(190, upload.size())
216 self.assertEqual(True, upload.resumable())
217 self.assertEqual(500, upload.chunksize())
218 self.assertEqual('PNG', upload.getbytes(1, 3))
219 f.close()
220
221 def test_media_io_base_upload_serializable(self):
222 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400223 upload = MediaIoBaseUpload(fd=f, mimetype='image/png')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400224
225 try:
226 json = upload.to_json()
227 self.fail('MediaIoBaseUpload should not be serializable.')
228 except NotImplementedError:
229 pass
230
231 def test_media_io_base_upload_from_string_io(self):
232 f = open(datafile('small.png'), 'r')
Pat Ferateed9affd2015-03-03 16:03:15 -0800233 fd = StringIO(f.read())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400234 f.close()
235
236 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400237 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400238 self.assertEqual('image/png', upload.mimetype())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400239 self.assertEqual(190, upload.size())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400240 self.assertEqual(True, upload.resumable())
241 self.assertEqual(500, upload.chunksize())
242 self.assertEqual('PNG', upload.getbytes(1, 3))
243 f.close()
244
245 def test_media_io_base_upload_from_bytes(self):
Pat Ferateed9affd2015-03-03 16:03:15 -0800246 f = open(datafile('small.png'), 'r')
247 fd = BytesIO(f.read())
248 upload = MediaIoBaseUpload(
249 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
250 self.assertEqual('image/png', upload.mimetype())
251 self.assertEqual(190, upload.size())
252 self.assertEqual(True, upload.resumable())
253 self.assertEqual(500, upload.chunksize())
254 self.assertEqual('PNG', upload.getbytes(1, 3))
Joe Gregorio910b9b12012-06-12 09:36:30 -0400255
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400256 def test_media_io_base_upload_raises_on_invalid_chunksize(self):
Pat Ferateed9affd2015-03-03 16:03:15 -0800257 f = open(datafile('small.png'), 'r')
258 fd = BytesIO(f.read())
259 self.assertRaises(InvalidChunkSizeError, MediaIoBaseUpload,
260 fd, 'image/png', chunksize=-2, resumable=True)
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400261
262 def test_media_io_base_upload_streamable(self):
Pat Ferateed9affd2015-03-03 16:03:15 -0800263 fd = BytesIO('stuff')
264 upload = MediaIoBaseUpload(
265 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
266 self.assertEqual(True, upload.has_stream())
267 self.assertEqual(fd, upload.stream())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400268
Joe Gregorio9086bd32013-06-14 16:32:05 -0400269 def test_media_io_base_next_chunk_retries(self):
Joe Gregorio9086bd32013-06-14 16:32:05 -0400270 f = open(datafile('small.png'), 'r')
Pat Ferateed9affd2015-03-03 16:03:15 -0800271 fd = BytesIO(f.read())
Joe Gregorio9086bd32013-06-14 16:32:05 -0400272 upload = MediaIoBaseUpload(
273 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
274
275 # Simulate 5XXs for both the request that creates the resumable upload and
276 # the upload itself.
277 http = HttpMockSequence([
278 ({'status': '500'}, ''),
279 ({'status': '500'}, ''),
280 ({'status': '503'}, ''),
281 ({'status': '200', 'location': 'location'}, ''),
282 ({'status': '500'}, ''),
283 ({'status': '500'}, ''),
284 ({'status': '503'}, ''),
285 ({'status': '200'}, '{}'),
286 ])
287
288 model = JsonModel()
289 uri = u'https://www.googleapis.com/someapi/v1/upload/?foo=bar'
290 method = u'POST'
291 request = HttpRequest(
292 http,
293 model.response,
294 uri,
295 method=method,
296 headers={},
297 resumable=upload)
298
299 sleeptimes = []
300 request._sleep = lambda x: sleeptimes.append(x)
301 request._rand = lambda: 10
302
303 request.execute(num_retries=3)
304 self.assertEqual([20, 40, 80, 20, 40, 80], sleeptimes)
305
Joe Gregorio910b9b12012-06-12 09:36:30 -0400306
Joe Gregorio708388c2012-06-15 13:43:04 -0400307class TestMediaIoBaseDownload(unittest.TestCase):
308
309 def setUp(self):
310 http = HttpMock(datafile('zoo.json'), {'status': '200'})
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400311 zoo = build('zoo', 'v1', http=http)
Joe Gregorio708388c2012-06-15 13:43:04 -0400312 self.request = zoo.animals().get_media(name='Lion')
Pat Ferateed9affd2015-03-03 16:03:15 -0800313 self.fd = BytesIO()
Joe Gregorio708388c2012-06-15 13:43:04 -0400314
315 def test_media_io_base_download(self):
316 self.request.http = HttpMockSequence([
317 ({'status': '200',
318 'content-range': '0-2/5'}, '123'),
319 ({'status': '200',
320 'content-range': '3-4/5'}, '45'),
321 ])
Joe Gregorio97ef1cc2013-06-13 14:47:10 -0400322 self.assertEqual(True, self.request.http.follow_redirects)
Joe Gregorio708388c2012-06-15 13:43:04 -0400323
324 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400325 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400326
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400327 self.assertEqual(self.fd, download._fd)
328 self.assertEqual(3, download._chunksize)
329 self.assertEqual(0, download._progress)
330 self.assertEqual(None, download._total_size)
331 self.assertEqual(False, download._done)
332 self.assertEqual(self.request.uri, download._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400333
334 status, done = download.next_chunk()
335
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400336 self.assertEqual(self.fd.getvalue(), '123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400337 self.assertEqual(False, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400338 self.assertEqual(3, download._progress)
339 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400340 self.assertEqual(3, status.resumable_progress)
341
342 status, done = download.next_chunk()
343
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400344 self.assertEqual(self.fd.getvalue(), '12345')
Joe Gregorio708388c2012-06-15 13:43:04 -0400345 self.assertEqual(True, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400346 self.assertEqual(5, download._progress)
347 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400348
349 def test_media_io_base_download_handle_redirects(self):
350 self.request.http = HttpMockSequence([
Joe Gregorio238feb72013-06-19 13:15:31 -0400351 ({'status': '200',
352 'content-location': 'https://secure.example.net/lion'}, ''),
Joe Gregorio708388c2012-06-15 13:43:04 -0400353 ({'status': '200',
354 'content-range': '0-2/5'}, 'abc'),
355 ])
356
357 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400358 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400359
360 status, done = download.next_chunk()
361
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400362 self.assertEqual('https://secure.example.net/lion', download._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400363
364 def test_media_io_base_download_handle_4xx(self):
365 self.request.http = HttpMockSequence([
366 ({'status': '400'}, ''),
367 ])
368
369 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400370 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400371
372 try:
373 status, done = download.next_chunk()
374 self.fail('Should raise an exception')
375 except HttpError:
376 pass
377
378 # Even after raising an exception we can pick up where we left off.
379 self.request.http = HttpMockSequence([
380 ({'status': '200',
381 'content-range': '0-2/5'}, '123'),
382 ])
383
384 status, done = download.next_chunk()
385
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400386 self.assertEqual(self.fd.getvalue(), '123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400387
Joe Gregorio9086bd32013-06-14 16:32:05 -0400388 def test_media_io_base_download_retries_5xx(self):
389 self.request.http = HttpMockSequence([
390 ({'status': '500'}, ''),
391 ({'status': '500'}, ''),
392 ({'status': '500'}, ''),
393 ({'status': '200',
394 'content-range': '0-2/5'}, '123'),
395 ({'status': '503'}, ''),
396 ({'status': '503'}, ''),
397 ({'status': '503'}, ''),
398 ({'status': '200',
399 'content-range': '3-4/5'}, '45'),
400 ])
401
402 download = MediaIoBaseDownload(
403 fd=self.fd, request=self.request, chunksize=3)
404
405 self.assertEqual(self.fd, download._fd)
406 self.assertEqual(3, download._chunksize)
407 self.assertEqual(0, download._progress)
408 self.assertEqual(None, download._total_size)
409 self.assertEqual(False, download._done)
410 self.assertEqual(self.request.uri, download._uri)
411
412 # Set time.sleep and random.random stubs.
413 sleeptimes = []
414 download._sleep = lambda x: sleeptimes.append(x)
415 download._rand = lambda: 10
416
417 status, done = download.next_chunk(num_retries=3)
418
419 # Check for exponential backoff using the rand function above.
420 self.assertEqual([20, 40, 80], sleeptimes)
421
422 self.assertEqual(self.fd.getvalue(), '123')
423 self.assertEqual(False, done)
424 self.assertEqual(3, download._progress)
425 self.assertEqual(5, download._total_size)
426 self.assertEqual(3, status.resumable_progress)
427
428 # Reset time.sleep stub.
429 del sleeptimes[0:len(sleeptimes)]
430
431 status, done = download.next_chunk(num_retries=3)
432
433 # Check for exponential backoff using the rand function above.
434 self.assertEqual([20, 40, 80], sleeptimes)
435
436 self.assertEqual(self.fd.getvalue(), '12345')
437 self.assertEqual(True, done)
438 self.assertEqual(5, download._progress)
439 self.assertEqual(5, download._total_size)
440
Joe Gregorio66f57522011-11-30 11:00:00 -0500441EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
442Content-Type: application/json
443MIME-Version: 1.0
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500444Host: www.googleapis.com
445content-length: 2\r\n\r\n{}"""
446
447
448NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
449Content-Type: application/json
450MIME-Version: 1.0
451Host: www.googleapis.com
452content-length: 0\r\n\r\n"""
Joe Gregorio66f57522011-11-30 11:00:00 -0500453
454
455RESPONSE = """HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400456Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500457Content-Length: 14
458ETag: "etag/pony"\r\n\r\n{"answer": 42}"""
459
460
461BATCH_RESPONSE = """--batch_foobarbaz
462Content-Type: application/http
463Content-Transfer-Encoding: binary
464Content-ID: <randomness+1>
465
466HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400467Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500468Content-Length: 14
469ETag: "etag/pony"\r\n\r\n{"foo": 42}
470
471--batch_foobarbaz
472Content-Type: application/http
473Content-Transfer-Encoding: binary
474Content-ID: <randomness+2>
475
476HTTP/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/sheep"\r\n\r\n{"baz": "qux"}
480--batch_foobarbaz--"""
481
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500482
Joe Gregorio3fb93672012-07-25 11:31:11 -0400483BATCH_ERROR_RESPONSE = """--batch_foobarbaz
484Content-Type: application/http
485Content-Transfer-Encoding: binary
486Content-ID: <randomness+1>
487
488HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400489Content-Type: application/json
Joe Gregorio3fb93672012-07-25 11:31:11 -0400490Content-Length: 14
491ETag: "etag/pony"\r\n\r\n{"foo": 42}
492
493--batch_foobarbaz
494Content-Type: application/http
495Content-Transfer-Encoding: binary
496Content-ID: <randomness+2>
497
498HTTP/1.1 403 Access Not Configured
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400499Content-Type: application/json
500Content-Length: 245
501ETag: "etag/sheep"\r\n\r\n{
Joe Gregorio3fb93672012-07-25 11:31:11 -0400502 "error": {
503 "errors": [
504 {
505 "domain": "usageLimits",
506 "reason": "accessNotConfigured",
507 "message": "Access Not Configured",
508 "debugInfo": "QuotaState: BLOCKED"
509 }
510 ],
511 "code": 403,
512 "message": "Access Not Configured"
513 }
514}
515
516--batch_foobarbaz--"""
517
518
Joe Gregorio654f4a22012-02-09 14:15:44 -0500519BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz
520Content-Type: application/http
521Content-Transfer-Encoding: binary
522Content-ID: <randomness+1>
523
Joe Gregorioc752e332012-07-11 14:43:52 -0400524HTTP/1.1 401 Authorization Required
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400525Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500526Content-Length: 14
527ETag: "etag/pony"\r\n\r\n{"error": {"message":
528 "Authorizaton failed."}}
529
530--batch_foobarbaz
531Content-Type: application/http
532Content-Transfer-Encoding: binary
533Content-ID: <randomness+2>
534
535HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400536Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500537Content-Length: 14
538ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
539--batch_foobarbaz--"""
540
541
542BATCH_SINGLE_RESPONSE = """--batch_foobarbaz
543Content-Type: application/http
544Content-Transfer-Encoding: binary
545Content-ID: <randomness+1>
546
547HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400548Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500549Content-Length: 14
550ETag: "etag/pony"\r\n\r\n{"foo": 42}
551--batch_foobarbaz--"""
552
553class Callbacks(object):
554 def __init__(self):
555 self.responses = {}
556 self.exceptions = {}
557
558 def f(self, request_id, response, exception):
559 self.responses[request_id] = response
560 self.exceptions[request_id] = exception
561
562
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500563class TestHttpRequest(unittest.TestCase):
564 def test_unicode(self):
565 http = HttpMock(datafile('zoo.json'), headers={'status': '200'})
566 model = JsonModel()
567 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
568 method = u'POST'
569 request = HttpRequest(
570 http,
571 model.response,
572 uri,
573 method=method,
574 body=u'{}',
575 headers={'content-type': 'application/json'})
576 request.execute()
577 self.assertEqual(uri, http.uri)
578 self.assertEqual(str, type(http.uri))
579 self.assertEqual(method, http.method)
580 self.assertEqual(str, type(http.method))
581
Joe Gregorio9086bd32013-06-14 16:32:05 -0400582 def test_retry(self):
583 num_retries = 5
584 resp_seq = [({'status': '500'}, '')] * num_retries
585 resp_seq.append(({'status': '200'}, '{}'))
586
587 http = HttpMockSequence(resp_seq)
588 model = JsonModel()
589 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
590 method = u'POST'
591 request = HttpRequest(
592 http,
593 model.response,
594 uri,
595 method=method,
596 body=u'{}',
597 headers={'content-type': 'application/json'})
598
599 sleeptimes = []
600 request._sleep = lambda x: sleeptimes.append(x)
601 request._rand = lambda: 10
602
603 request.execute(num_retries=num_retries)
604
605 self.assertEqual(num_retries, len(sleeptimes))
INADA Naokid898a372015-03-04 03:52:46 +0900606 for retry_num in range(num_retries):
Joe Gregorio9086bd32013-06-14 16:32:05 -0400607 self.assertEqual(10 * 2**(retry_num + 1), sleeptimes[retry_num])
608
609 def test_no_retry_fails_fast(self):
610 http = HttpMockSequence([
611 ({'status': '500'}, ''),
612 ({'status': '200'}, '{}')
613 ])
614 model = JsonModel()
615 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
616 method = u'POST'
617 request = HttpRequest(
618 http,
619 model.response,
620 uri,
621 method=method,
622 body=u'{}',
623 headers={'content-type': 'application/json'})
624
625 request._rand = lambda: 1.0
626 request._sleep = lambda _: self.fail('sleep should not have been called.')
627
628 try:
629 request.execute()
630 self.fail('Should have raised an exception.')
631 except HttpError:
632 pass
633
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500634
Joe Gregorio66f57522011-11-30 11:00:00 -0500635class TestBatch(unittest.TestCase):
636
637 def setUp(self):
638 model = JsonModel()
639 self.request1 = HttpRequest(
640 None,
641 model.response,
642 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
643 method='POST',
644 body='{}',
645 headers={'content-type': 'application/json'})
646
647 self.request2 = HttpRequest(
648 None,
649 model.response,
650 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500651 method='GET',
652 body='',
Joe Gregorio66f57522011-11-30 11:00:00 -0500653 headers={'content-type': 'application/json'})
654
655
656 def test_id_to_from_content_id_header(self):
657 batch = BatchHttpRequest()
658 self.assertEquals('12', batch._header_to_id(batch._id_to_header('12')))
659
660 def test_invalid_content_id_header(self):
661 batch = BatchHttpRequest()
662 self.assertRaises(BatchError, batch._header_to_id, '[foo+x]')
663 self.assertRaises(BatchError, batch._header_to_id, 'foo+1')
664 self.assertRaises(BatchError, batch._header_to_id, '<foo>')
665
666 def test_serialize_request(self):
667 batch = BatchHttpRequest()
668 request = HttpRequest(
669 None,
670 None,
671 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
672 method='POST',
673 body='{}',
674 headers={'content-type': 'application/json'},
675 methodId=None,
676 resumable=None)
677 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500678 self.assertEqual(EXPECTED.splitlines(), s)
Joe Gregorio66f57522011-11-30 11:00:00 -0500679
Joe Gregoriodd813822012-01-25 10:32:47 -0500680 def test_serialize_request_media_body(self):
681 batch = BatchHttpRequest()
682 f = open(datafile('small.png'))
683 body = f.read()
684 f.close()
685
686 request = HttpRequest(
687 None,
688 None,
689 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
690 method='POST',
691 body=body,
692 headers={'content-type': 'application/json'},
693 methodId=None,
694 resumable=None)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500695 # Just testing it shouldn't raise an exception.
Joe Gregoriodd813822012-01-25 10:32:47 -0500696 s = batch._serialize_request(request).splitlines()
697
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500698 def test_serialize_request_no_body(self):
699 batch = BatchHttpRequest()
700 request = HttpRequest(
701 None,
702 None,
703 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
704 method='POST',
705 body='',
706 headers={'content-type': 'application/json'},
707 methodId=None,
708 resumable=None)
709 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500710 self.assertEqual(NO_BODY_EXPECTED.splitlines(), s)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500711
Joe Gregorio66f57522011-11-30 11:00:00 -0500712 def test_deserialize_response(self):
713 batch = BatchHttpRequest()
714 resp, content = batch._deserialize_response(RESPONSE)
715
Joe Gregorio654f4a22012-02-09 14:15:44 -0500716 self.assertEqual(200, resp.status)
717 self.assertEqual('OK', resp.reason)
718 self.assertEqual(11, resp.version)
719 self.assertEqual('{"answer": 42}', content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500720
721 def test_new_id(self):
722 batch = BatchHttpRequest()
723
724 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500725 self.assertEqual('1', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500726
727 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500728 self.assertEqual('2', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500729
730 batch.add(self.request1, request_id='3')
731
732 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500733 self.assertEqual('4', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500734
735 def test_add(self):
736 batch = BatchHttpRequest()
737 batch.add(self.request1, request_id='1')
738 self.assertRaises(KeyError, batch.add, self.request1, request_id='1')
739
740 def test_add_fail_for_resumable(self):
741 batch = BatchHttpRequest()
742
743 upload = MediaFileUpload(
744 datafile('small.png'), chunksize=500, resumable=True)
745 self.request1.resumable = upload
746 self.assertRaises(BatchError, batch.add, self.request1, request_id='1')
747
748 def test_execute(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500749 batch = BatchHttpRequest()
750 callbacks = Callbacks()
751
752 batch.add(self.request1, callback=callbacks.f)
753 batch.add(self.request2, callback=callbacks.f)
754 http = HttpMockSequence([
755 ({'status': '200',
756 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
757 BATCH_RESPONSE),
758 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400759 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500760 self.assertEqual({'foo': 42}, callbacks.responses['1'])
761 self.assertEqual(None, callbacks.exceptions['1'])
762 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
763 self.assertEqual(None, callbacks.exceptions['2'])
Joe Gregorio66f57522011-11-30 11:00:00 -0500764
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500765 def test_execute_request_body(self):
766 batch = BatchHttpRequest()
767
768 batch.add(self.request1)
769 batch.add(self.request2)
770 http = HttpMockSequence([
771 ({'status': '200',
772 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
773 'echo_request_body'),
774 ])
775 try:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400776 batch.execute(http=http)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500777 self.fail('Should raise exception')
INADA Naokic1505df2014-08-20 15:19:53 +0900778 except BatchError as e:
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500779 boundary, _ = e.content.split(None, 1)
780 self.assertEqual('--', boundary[:2])
781 parts = e.content.split(boundary)
782 self.assertEqual(4, len(parts))
783 self.assertEqual('', parts[0])
Craig Citro4282aa32014-06-21 00:33:39 -0700784 self.assertEqual('--', parts[3].rstrip())
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500785 header = parts[1].splitlines()[1]
786 self.assertEqual('Content-Type: application/http', header)
787
Joe Gregorio654f4a22012-02-09 14:15:44 -0500788 def test_execute_refresh_and_retry_on_401(self):
789 batch = BatchHttpRequest()
790 callbacks = Callbacks()
791 cred_1 = MockCredentials('Foo')
792 cred_2 = MockCredentials('Bar')
793
794 http = HttpMockSequence([
795 ({'status': '200',
796 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
797 BATCH_RESPONSE_WITH_401),
798 ({'status': '200',
799 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
800 BATCH_SINGLE_RESPONSE),
801 ])
802
803 creds_http_1 = HttpMockSequence([])
804 cred_1.authorize(creds_http_1)
805
806 creds_http_2 = HttpMockSequence([])
807 cred_2.authorize(creds_http_2)
808
809 self.request1.http = creds_http_1
810 self.request2.http = creds_http_2
811
812 batch.add(self.request1, callback=callbacks.f)
813 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400814 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500815
816 self.assertEqual({'foo': 42}, callbacks.responses['1'])
817 self.assertEqual(None, callbacks.exceptions['1'])
818 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
819 self.assertEqual(None, callbacks.exceptions['2'])
820
821 self.assertEqual(1, cred_1._refreshed)
822 self.assertEqual(0, cred_2._refreshed)
823
824 self.assertEqual(1, cred_1._authorized)
825 self.assertEqual(1, cred_2._authorized)
826
827 self.assertEqual(1, cred_2._applied)
828 self.assertEqual(2, cred_1._applied)
829
830 def test_http_errors_passed_to_callback(self):
831 batch = BatchHttpRequest()
832 callbacks = Callbacks()
833 cred_1 = MockCredentials('Foo')
834 cred_2 = MockCredentials('Bar')
835
836 http = HttpMockSequence([
837 ({'status': '200',
838 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
839 BATCH_RESPONSE_WITH_401),
840 ({'status': '200',
841 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
842 BATCH_RESPONSE_WITH_401),
843 ])
844
845 creds_http_1 = HttpMockSequence([])
846 cred_1.authorize(creds_http_1)
847
848 creds_http_2 = HttpMockSequence([])
849 cred_2.authorize(creds_http_2)
850
851 self.request1.http = creds_http_1
852 self.request2.http = creds_http_2
853
854 batch.add(self.request1, callback=callbacks.f)
855 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400856 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500857
858 self.assertEqual(None, callbacks.responses['1'])
859 self.assertEqual(401, callbacks.exceptions['1'].resp.status)
Joe Gregorioc752e332012-07-11 14:43:52 -0400860 self.assertEqual(
861 'Authorization Required', callbacks.exceptions['1'].resp.reason)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500862 self.assertEqual({u'baz': u'qux'}, callbacks.responses['2'])
863 self.assertEqual(None, callbacks.exceptions['2'])
864
Joe Gregorio66f57522011-11-30 11:00:00 -0500865 def test_execute_global_callback(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500866 callbacks = Callbacks()
867 batch = BatchHttpRequest(callback=callbacks.f)
868
869 batch.add(self.request1)
870 batch.add(self.request2)
871 http = HttpMockSequence([
872 ({'status': '200',
873 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
874 BATCH_RESPONSE),
875 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400876 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500877 self.assertEqual({'foo': 42}, callbacks.responses['1'])
878 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500879
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400880 def test_execute_batch_http_error(self):
Joe Gregorio3fb93672012-07-25 11:31:11 -0400881 callbacks = Callbacks()
882 batch = BatchHttpRequest(callback=callbacks.f)
883
884 batch.add(self.request1)
885 batch.add(self.request2)
886 http = HttpMockSequence([
887 ({'status': '200',
888 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
889 BATCH_ERROR_RESPONSE),
890 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400891 batch.execute(http=http)
Joe Gregorio3fb93672012-07-25 11:31:11 -0400892 self.assertEqual({'foo': 42}, callbacks.responses['1'])
893 expected = ('<HttpError 403 when requesting '
894 'https://www.googleapis.com/someapi/v1/collection/?foo=bar returned '
895 '"Access Not Configured">')
896 self.assertEqual(expected, str(callbacks.exceptions['2']))
Ali Afshar6f11ea12012-02-07 10:32:14 -0500897
Joe Gregorio5c120db2012-08-23 09:13:55 -0400898
Joe Gregorioba5c7902012-08-03 12:48:16 -0400899class TestRequestUriTooLong(unittest.TestCase):
900
901 def test_turn_get_into_post(self):
902
903 def _postproc(resp, content):
904 return content
905
906 http = HttpMockSequence([
907 ({'status': '200'},
908 'echo_request_body'),
909 ({'status': '200'},
910 'echo_request_headers'),
911 ])
912
913 # Send a long query parameter.
914 query = {
915 'q': 'a' * MAX_URI_LENGTH + '?&'
916 }
917 req = HttpRequest(
918 http,
919 _postproc,
Pat Ferated5b61bd2015-03-03 16:04:11 -0800920 'http://example.com?' + urlencode(query),
Joe Gregorioba5c7902012-08-03 12:48:16 -0400921 method='GET',
922 body=None,
923 headers={},
924 methodId='foo',
925 resumable=None)
926
927 # Query parameters should be sent in the body.
928 response = req.execute()
929 self.assertEqual('q=' + 'a' * MAX_URI_LENGTH + '%3F%26', response)
930
931 # Extra headers should be set.
932 response = req.execute()
933 self.assertEqual('GET', response['x-http-method-override'])
934 self.assertEqual(str(MAX_URI_LENGTH + 8), response['content-length'])
935 self.assertEqual(
936 'application/x-www-form-urlencoded', response['content-type'])
937
Joe Gregorio5c120db2012-08-23 09:13:55 -0400938
939class TestStreamSlice(unittest.TestCase):
940 """Test _StreamSlice."""
941
942 def setUp(self):
Pat Ferateed9affd2015-03-03 16:03:15 -0800943 self.stream = BytesIO('0123456789')
Joe Gregorio5c120db2012-08-23 09:13:55 -0400944
945 def test_read(self):
946 s = _StreamSlice(self.stream, 0, 4)
947 self.assertEqual('', s.read(0))
948 self.assertEqual('0', s.read(1))
949 self.assertEqual('123', s.read())
950
951 def test_read_too_much(self):
952 s = _StreamSlice(self.stream, 1, 4)
953 self.assertEqual('1234', s.read(6))
954
955 def test_read_all(self):
956 s = _StreamSlice(self.stream, 2, 1)
957 self.assertEqual('2', s.read(-1))
958
Ali Afshar164f37e2013-01-07 14:05:45 -0800959
960class TestResponseCallback(unittest.TestCase):
961 """Test adding callbacks to responses."""
962
963 def test_ensure_response_callback(self):
964 m = JsonModel()
965 request = HttpRequest(
966 None,
967 m.response,
968 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
969 method='POST',
970 body='{}',
971 headers={'content-type': 'application/json'})
972 h = HttpMockSequence([ ({'status': 200}, '{}')])
973 responses = []
974 def _on_response(resp, responses=responses):
975 responses.append(resp)
976 request.add_response_callback(_on_response)
977 request.execute(http=h)
978 self.assertEqual(1, len(responses))
979
980
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500981if __name__ == '__main__':
Joe Gregorio9086bd32013-06-14 16:32:05 -0400982 logging.getLogger().setLevel(logging.ERROR)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500983 unittest.main()