blob: a3ff0aca5a4a32cbc12aa2b1e192419189dd62c9 [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
Joe Gregorio7cbceab2011-06-27 10:46:54 -040026# Do not remove the httplib2 import
27import httplib2
Joe Gregorio9086bd32013-06-14 16:32:05 -040028import logging
Joe Gregoriod0bd3882011-11-22 09:49:47 -050029import os
Pat Ferate497a90f2015-03-09 09:52:54 -070030import unittest2 as unittest
Joe Gregorioba5c7902012-08-03 12:48:16 -040031import urllib
Joe Gregorio9086bd32013-06-14 16:32:05 -040032import random
Joe Gregorio910b9b12012-06-12 09:36:30 -040033import StringIO
Joe Gregorio9086bd32013-06-14 16:32:05 -040034import time
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050035
John Asmuth864311d2014-04-24 15:46:08 -040036from googleapiclient.discovery import build
37from googleapiclient.errors import BatchError
38from googleapiclient.errors import HttpError
39from googleapiclient.errors import InvalidChunkSizeError
40from googleapiclient.http import BatchHttpRequest
41from googleapiclient.http import HttpMock
42from googleapiclient.http import HttpMockSequence
43from googleapiclient.http import HttpRequest
44from googleapiclient.http import MAX_URI_LENGTH
45from googleapiclient.http import MediaFileUpload
46from googleapiclient.http import MediaInMemoryUpload
47from googleapiclient.http import MediaIoBaseDownload
48from googleapiclient.http import MediaIoBaseUpload
49from googleapiclient.http import MediaUpload
50from googleapiclient.http import _StreamSlice
51from googleapiclient.http import set_user_agent
52from googleapiclient.model import JsonModel
Joe Gregorio654f4a22012-02-09 14:15:44 -050053from oauth2client.client import Credentials
54
55
56class MockCredentials(Credentials):
57 """Mock class for all Credentials objects."""
58 def __init__(self, bearer_token):
59 super(MockCredentials, self).__init__()
60 self._authorized = 0
61 self._refreshed = 0
62 self._applied = 0
63 self._bearer_token = bearer_token
64
65 def authorize(self, http):
66 self._authorized += 1
67
68 request_orig = http.request
69
70 # The closure that will replace 'httplib2.Http.request'.
71 def new_request(uri, method='GET', body=None, headers=None,
72 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
73 connection_type=None):
74 # Modify the request headers to add the appropriate
75 # Authorization header.
76 if headers is None:
77 headers = {}
78 self.apply(headers)
79
80 resp, content = request_orig(uri, method, body, headers,
81 redirections, connection_type)
82
83 return resp, content
84
85 # Replace the request method with our own closure.
86 http.request = new_request
87
88 # Set credentials as a property of the request method.
89 setattr(http.request, 'credentials', self)
90
91 return http
92
93 def refresh(self, http):
94 self._refreshed += 1
95
96 def apply(self, headers):
97 self._applied += 1
98 headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050099
100
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500101DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
102
103
104def datafile(filename):
105 return os.path.join(DATA_DIR, filename)
106
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500107class TestUserAgent(unittest.TestCase):
108
109 def test_set_user_agent(self):
110 http = HttpMockSequence([
111 ({'status': '200'}, 'echo_request_headers'),
112 ])
113
114 http = set_user_agent(http, "my_app/5.5")
115 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500116 self.assertEqual('my_app/5.5', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500117
118 def test_set_user_agent_nested(self):
119 http = HttpMockSequence([
120 ({'status': '200'}, 'echo_request_headers'),
121 ])
122
123 http = set_user_agent(http, "my_app/5.5")
124 http = set_user_agent(http, "my_library/0.1")
125 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500126 self.assertEqual('my_app/5.5 my_library/0.1', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500127
Joe Gregorio910b9b12012-06-12 09:36:30 -0400128
129class TestMediaUpload(unittest.TestCase):
130
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500131 def test_media_file_upload_to_from_json(self):
132 upload = MediaFileUpload(
133 datafile('small.png'), chunksize=500, resumable=True)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500134 self.assertEqual('image/png', upload.mimetype())
135 self.assertEqual(190, upload.size())
136 self.assertEqual(True, upload.resumable())
137 self.assertEqual(500, upload.chunksize())
138 self.assertEqual('PNG', upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500139
140 json = upload.to_json()
141 new_upload = MediaUpload.new_from_json(json)
142
Joe Gregorio654f4a22012-02-09 14:15:44 -0500143 self.assertEqual('image/png', new_upload.mimetype())
144 self.assertEqual(190, new_upload.size())
145 self.assertEqual(True, new_upload.resumable())
146 self.assertEqual(500, new_upload.chunksize())
147 self.assertEqual('PNG', new_upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500148
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400149 def test_media_file_upload_raises_on_invalid_chunksize(self):
150 self.assertRaises(InvalidChunkSizeError, MediaFileUpload,
151 datafile('small.png'), mimetype='image/png', chunksize=-2,
152 resumable=True)
153
Ali Afshar1cb6b672012-03-12 08:46:14 -0400154 def test_media_inmemory_upload(self):
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400155 media = MediaInMemoryUpload('abcdef', mimetype='text/plain', chunksize=10,
Ali Afshar1cb6b672012-03-12 08:46:14 -0400156 resumable=True)
157 self.assertEqual('text/plain', media.mimetype())
158 self.assertEqual(10, media.chunksize())
159 self.assertTrue(media.resumable())
160 self.assertEqual('bc', media.getbytes(1, 2))
161 self.assertEqual(6, media.size())
162
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500163 def test_http_request_to_from_json(self):
164
165 def _postproc(*kwargs):
166 pass
167
168 http = httplib2.Http()
169 media_upload = MediaFileUpload(
170 datafile('small.png'), chunksize=500, resumable=True)
171 req = HttpRequest(
172 http,
173 _postproc,
174 'http://example.com',
175 method='POST',
176 body='{}',
177 headers={'content-type': 'multipart/related; boundary="---flubber"'},
178 methodId='foo',
179 resumable=media_upload)
180
181 json = req.to_json()
182 new_req = HttpRequest.from_json(json, http, _postproc)
183
Joe Gregorio654f4a22012-02-09 14:15:44 -0500184 self.assertEqual({'content-type':
185 'multipart/related; boundary="---flubber"'},
186 new_req.headers)
187 self.assertEqual('http://example.com', new_req.uri)
188 self.assertEqual('{}', new_req.body)
189 self.assertEqual(http, new_req.http)
190 self.assertEqual(media_upload.to_json(), new_req.resumable.to_json())
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500191
Joe Gregorio9086bd32013-06-14 16:32:05 -0400192 self.assertEqual(random.random, new_req._rand)
193 self.assertEqual(time.sleep, new_req._sleep)
194
Joe Gregorio910b9b12012-06-12 09:36:30 -0400195
196class TestMediaIoBaseUpload(unittest.TestCase):
197
198 def test_media_io_base_upload_from_file_io(self):
199 try:
200 import io
201
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400202 fd = io.FileIO(datafile('small.png'), 'r')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400203 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400204 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400205 self.assertEqual('image/png', upload.mimetype())
206 self.assertEqual(190, upload.size())
207 self.assertEqual(True, upload.resumable())
208 self.assertEqual(500, upload.chunksize())
209 self.assertEqual('PNG', upload.getbytes(1, 3))
210 except ImportError:
211 pass
212
213 def test_media_io_base_upload_from_file_object(self):
214 f = open(datafile('small.png'), 'r')
215 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400216 fd=f, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400217 self.assertEqual('image/png', upload.mimetype())
218 self.assertEqual(190, upload.size())
219 self.assertEqual(True, upload.resumable())
220 self.assertEqual(500, upload.chunksize())
221 self.assertEqual('PNG', upload.getbytes(1, 3))
222 f.close()
223
224 def test_media_io_base_upload_serializable(self):
225 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400226 upload = MediaIoBaseUpload(fd=f, mimetype='image/png')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400227
228 try:
229 json = upload.to_json()
230 self.fail('MediaIoBaseUpload should not be serializable.')
231 except NotImplementedError:
232 pass
233
234 def test_media_io_base_upload_from_string_io(self):
235 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400236 fd = StringIO.StringIO(f.read())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400237 f.close()
238
239 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400240 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400241 self.assertEqual('image/png', upload.mimetype())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400242 self.assertEqual(190, upload.size())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400243 self.assertEqual(True, upload.resumable())
244 self.assertEqual(500, upload.chunksize())
245 self.assertEqual('PNG', upload.getbytes(1, 3))
246 f.close()
247
248 def test_media_io_base_upload_from_bytes(self):
249 try:
250 import io
251
252 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400253 fd = io.BytesIO(f.read())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400254 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400255 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400256 self.assertEqual('image/png', upload.mimetype())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400257 self.assertEqual(190, upload.size())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400258 self.assertEqual(True, upload.resumable())
259 self.assertEqual(500, upload.chunksize())
260 self.assertEqual('PNG', upload.getbytes(1, 3))
261 except ImportError:
262 pass
263
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400264 def test_media_io_base_upload_raises_on_invalid_chunksize(self):
265 try:
266 import io
267
268 f = open(datafile('small.png'), 'r')
269 fd = io.BytesIO(f.read())
270 self.assertRaises(InvalidChunkSizeError, MediaIoBaseUpload,
271 fd, 'image/png', chunksize=-2, resumable=True)
272 except ImportError:
273 pass
274
275 def test_media_io_base_upload_streamable(self):
276 try:
277 import io
278
279 fd = io.BytesIO('stuff')
280 upload = MediaIoBaseUpload(
281 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
282 self.assertEqual(True, upload.has_stream())
283 self.assertEqual(fd, upload.stream())
284 except ImportError:
285 pass
286
Joe Gregorio9086bd32013-06-14 16:32:05 -0400287 def test_media_io_base_next_chunk_retries(self):
288 try:
289 import io
290 except ImportError:
291 return
292
293 f = open(datafile('small.png'), 'r')
294 fd = io.BytesIO(f.read())
295 upload = MediaIoBaseUpload(
296 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
297
298 # Simulate 5XXs for both the request that creates the resumable upload and
299 # the upload itself.
300 http = HttpMockSequence([
301 ({'status': '500'}, ''),
302 ({'status': '500'}, ''),
303 ({'status': '503'}, ''),
304 ({'status': '200', 'location': 'location'}, ''),
305 ({'status': '500'}, ''),
306 ({'status': '500'}, ''),
307 ({'status': '503'}, ''),
308 ({'status': '200'}, '{}'),
309 ])
310
311 model = JsonModel()
312 uri = u'https://www.googleapis.com/someapi/v1/upload/?foo=bar'
313 method = u'POST'
314 request = HttpRequest(
315 http,
316 model.response,
317 uri,
318 method=method,
319 headers={},
320 resumable=upload)
321
322 sleeptimes = []
323 request._sleep = lambda x: sleeptimes.append(x)
324 request._rand = lambda: 10
325
326 request.execute(num_retries=3)
327 self.assertEqual([20, 40, 80, 20, 40, 80], sleeptimes)
328
Joe Gregorio910b9b12012-06-12 09:36:30 -0400329
Joe Gregorio708388c2012-06-15 13:43:04 -0400330class TestMediaIoBaseDownload(unittest.TestCase):
331
332 def setUp(self):
333 http = HttpMock(datafile('zoo.json'), {'status': '200'})
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400334 zoo = build('zoo', 'v1', http=http)
Joe Gregorio708388c2012-06-15 13:43:04 -0400335 self.request = zoo.animals().get_media(name='Lion')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400336 self.fd = StringIO.StringIO()
Joe Gregorio708388c2012-06-15 13:43:04 -0400337
338 def test_media_io_base_download(self):
339 self.request.http = HttpMockSequence([
340 ({'status': '200',
341 'content-range': '0-2/5'}, '123'),
342 ({'status': '200',
343 'content-range': '3-4/5'}, '45'),
344 ])
Joe Gregorio97ef1cc2013-06-13 14:47:10 -0400345 self.assertEqual(True, self.request.http.follow_redirects)
Joe Gregorio708388c2012-06-15 13:43:04 -0400346
347 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400348 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400349
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400350 self.assertEqual(self.fd, download._fd)
351 self.assertEqual(3, download._chunksize)
352 self.assertEqual(0, download._progress)
353 self.assertEqual(None, download._total_size)
354 self.assertEqual(False, download._done)
355 self.assertEqual(self.request.uri, download._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400356
357 status, done = download.next_chunk()
358
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400359 self.assertEqual(self.fd.getvalue(), '123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400360 self.assertEqual(False, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400361 self.assertEqual(3, download._progress)
362 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400363 self.assertEqual(3, status.resumable_progress)
364
365 status, done = download.next_chunk()
366
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400367 self.assertEqual(self.fd.getvalue(), '12345')
Joe Gregorio708388c2012-06-15 13:43:04 -0400368 self.assertEqual(True, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400369 self.assertEqual(5, download._progress)
370 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400371
372 def test_media_io_base_download_handle_redirects(self):
373 self.request.http = HttpMockSequence([
Joe Gregorio238feb72013-06-19 13:15:31 -0400374 ({'status': '200',
375 'content-location': 'https://secure.example.net/lion'}, ''),
Joe Gregorio708388c2012-06-15 13:43:04 -0400376 ({'status': '200',
377 'content-range': '0-2/5'}, 'abc'),
378 ])
379
380 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400381 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400382
383 status, done = download.next_chunk()
384
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400385 self.assertEqual('https://secure.example.net/lion', download._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400386
387 def test_media_io_base_download_handle_4xx(self):
388 self.request.http = HttpMockSequence([
389 ({'status': '400'}, ''),
390 ])
391
392 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400393 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400394
395 try:
396 status, done = download.next_chunk()
397 self.fail('Should raise an exception')
398 except HttpError:
399 pass
400
401 # Even after raising an exception we can pick up where we left off.
402 self.request.http = HttpMockSequence([
403 ({'status': '200',
404 'content-range': '0-2/5'}, '123'),
405 ])
406
407 status, done = download.next_chunk()
408
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400409 self.assertEqual(self.fd.getvalue(), '123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400410
Joe Gregorio9086bd32013-06-14 16:32:05 -0400411 def test_media_io_base_download_retries_5xx(self):
412 self.request.http = HttpMockSequence([
413 ({'status': '500'}, ''),
414 ({'status': '500'}, ''),
415 ({'status': '500'}, ''),
416 ({'status': '200',
417 'content-range': '0-2/5'}, '123'),
418 ({'status': '503'}, ''),
419 ({'status': '503'}, ''),
420 ({'status': '503'}, ''),
421 ({'status': '200',
422 'content-range': '3-4/5'}, '45'),
423 ])
424
425 download = MediaIoBaseDownload(
426 fd=self.fd, request=self.request, chunksize=3)
427
428 self.assertEqual(self.fd, download._fd)
429 self.assertEqual(3, download._chunksize)
430 self.assertEqual(0, download._progress)
431 self.assertEqual(None, download._total_size)
432 self.assertEqual(False, download._done)
433 self.assertEqual(self.request.uri, download._uri)
434
435 # Set time.sleep and random.random stubs.
436 sleeptimes = []
437 download._sleep = lambda x: sleeptimes.append(x)
438 download._rand = lambda: 10
439
440 status, done = download.next_chunk(num_retries=3)
441
442 # Check for exponential backoff using the rand function above.
443 self.assertEqual([20, 40, 80], sleeptimes)
444
445 self.assertEqual(self.fd.getvalue(), '123')
446 self.assertEqual(False, done)
447 self.assertEqual(3, download._progress)
448 self.assertEqual(5, download._total_size)
449 self.assertEqual(3, status.resumable_progress)
450
451 # Reset time.sleep stub.
452 del sleeptimes[0:len(sleeptimes)]
453
454 status, done = download.next_chunk(num_retries=3)
455
456 # Check for exponential backoff using the rand function above.
457 self.assertEqual([20, 40, 80], sleeptimes)
458
459 self.assertEqual(self.fd.getvalue(), '12345')
460 self.assertEqual(True, done)
461 self.assertEqual(5, download._progress)
462 self.assertEqual(5, download._total_size)
463
Joe Gregorio66f57522011-11-30 11:00:00 -0500464EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
465Content-Type: application/json
466MIME-Version: 1.0
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500467Host: www.googleapis.com
468content-length: 2\r\n\r\n{}"""
469
470
471NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
472Content-Type: application/json
473MIME-Version: 1.0
474Host: www.googleapis.com
475content-length: 0\r\n\r\n"""
Joe Gregorio66f57522011-11-30 11:00:00 -0500476
477
478RESPONSE = """HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400479Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500480Content-Length: 14
481ETag: "etag/pony"\r\n\r\n{"answer": 42}"""
482
483
484BATCH_RESPONSE = """--batch_foobarbaz
485Content-Type: application/http
486Content-Transfer-Encoding: binary
487Content-ID: <randomness+1>
488
489HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400490Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500491Content-Length: 14
492ETag: "etag/pony"\r\n\r\n{"foo": 42}
493
494--batch_foobarbaz
495Content-Type: application/http
496Content-Transfer-Encoding: binary
497Content-ID: <randomness+2>
498
499HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400500Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500501Content-Length: 14
502ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
503--batch_foobarbaz--"""
504
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500505
Joe Gregorio3fb93672012-07-25 11:31:11 -0400506BATCH_ERROR_RESPONSE = """--batch_foobarbaz
507Content-Type: application/http
508Content-Transfer-Encoding: binary
509Content-ID: <randomness+1>
510
511HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400512Content-Type: application/json
Joe Gregorio3fb93672012-07-25 11:31:11 -0400513Content-Length: 14
514ETag: "etag/pony"\r\n\r\n{"foo": 42}
515
516--batch_foobarbaz
517Content-Type: application/http
518Content-Transfer-Encoding: binary
519Content-ID: <randomness+2>
520
521HTTP/1.1 403 Access Not Configured
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400522Content-Type: application/json
523Content-Length: 245
524ETag: "etag/sheep"\r\n\r\n{
Joe Gregorio3fb93672012-07-25 11:31:11 -0400525 "error": {
526 "errors": [
527 {
528 "domain": "usageLimits",
529 "reason": "accessNotConfigured",
530 "message": "Access Not Configured",
531 "debugInfo": "QuotaState: BLOCKED"
532 }
533 ],
534 "code": 403,
535 "message": "Access Not Configured"
536 }
537}
538
539--batch_foobarbaz--"""
540
541
Joe Gregorio654f4a22012-02-09 14:15:44 -0500542BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz
543Content-Type: application/http
544Content-Transfer-Encoding: binary
545Content-ID: <randomness+1>
546
Joe Gregorioc752e332012-07-11 14:43:52 -0400547HTTP/1.1 401 Authorization Required
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{"error": {"message":
551 "Authorizaton failed."}}
552
553--batch_foobarbaz
554Content-Type: application/http
555Content-Transfer-Encoding: binary
556Content-ID: <randomness+2>
557
558HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400559Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500560Content-Length: 14
561ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
562--batch_foobarbaz--"""
563
564
565BATCH_SINGLE_RESPONSE = """--batch_foobarbaz
566Content-Type: application/http
567Content-Transfer-Encoding: binary
568Content-ID: <randomness+1>
569
570HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400571Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500572Content-Length: 14
573ETag: "etag/pony"\r\n\r\n{"foo": 42}
574--batch_foobarbaz--"""
575
576class Callbacks(object):
577 def __init__(self):
578 self.responses = {}
579 self.exceptions = {}
580
581 def f(self, request_id, response, exception):
582 self.responses[request_id] = response
583 self.exceptions[request_id] = exception
584
585
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500586class TestHttpRequest(unittest.TestCase):
587 def test_unicode(self):
588 http = HttpMock(datafile('zoo.json'), headers={'status': '200'})
589 model = JsonModel()
590 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
591 method = u'POST'
592 request = HttpRequest(
593 http,
594 model.response,
595 uri,
596 method=method,
597 body=u'{}',
598 headers={'content-type': 'application/json'})
599 request.execute()
600 self.assertEqual(uri, http.uri)
601 self.assertEqual(str, type(http.uri))
602 self.assertEqual(method, http.method)
603 self.assertEqual(str, type(http.method))
604
Joe Gregorio9086bd32013-06-14 16:32:05 -0400605 def test_retry(self):
606 num_retries = 5
607 resp_seq = [({'status': '500'}, '')] * num_retries
608 resp_seq.append(({'status': '200'}, '{}'))
609
610 http = HttpMockSequence(resp_seq)
611 model = JsonModel()
612 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
613 method = u'POST'
614 request = HttpRequest(
615 http,
616 model.response,
617 uri,
618 method=method,
619 body=u'{}',
620 headers={'content-type': 'application/json'})
621
622 sleeptimes = []
623 request._sleep = lambda x: sleeptimes.append(x)
624 request._rand = lambda: 10
625
626 request.execute(num_retries=num_retries)
627
628 self.assertEqual(num_retries, len(sleeptimes))
INADA Naokid898a372015-03-04 03:52:46 +0900629 for retry_num in range(num_retries):
Joe Gregorio9086bd32013-06-14 16:32:05 -0400630 self.assertEqual(10 * 2**(retry_num + 1), sleeptimes[retry_num])
631
632 def test_no_retry_fails_fast(self):
633 http = HttpMockSequence([
634 ({'status': '500'}, ''),
635 ({'status': '200'}, '{}')
636 ])
637 model = JsonModel()
638 uri = u'https://www.googleapis.com/someapi/v1/collection/?foo=bar'
639 method = u'POST'
640 request = HttpRequest(
641 http,
642 model.response,
643 uri,
644 method=method,
645 body=u'{}',
646 headers={'content-type': 'application/json'})
647
648 request._rand = lambda: 1.0
649 request._sleep = lambda _: self.fail('sleep should not have been called.')
650
651 try:
652 request.execute()
653 self.fail('Should have raised an exception.')
654 except HttpError:
655 pass
656
Joe Gregorio83f2ee62012-12-06 15:25:54 -0500657
Joe Gregorio66f57522011-11-30 11:00:00 -0500658class TestBatch(unittest.TestCase):
659
660 def setUp(self):
661 model = JsonModel()
662 self.request1 = HttpRequest(
663 None,
664 model.response,
665 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
666 method='POST',
667 body='{}',
668 headers={'content-type': 'application/json'})
669
670 self.request2 = HttpRequest(
671 None,
672 model.response,
673 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500674 method='GET',
675 body='',
Joe Gregorio66f57522011-11-30 11:00:00 -0500676 headers={'content-type': 'application/json'})
677
678
679 def test_id_to_from_content_id_header(self):
680 batch = BatchHttpRequest()
681 self.assertEquals('12', batch._header_to_id(batch._id_to_header('12')))
682
683 def test_invalid_content_id_header(self):
684 batch = BatchHttpRequest()
685 self.assertRaises(BatchError, batch._header_to_id, '[foo+x]')
686 self.assertRaises(BatchError, batch._header_to_id, 'foo+1')
687 self.assertRaises(BatchError, batch._header_to_id, '<foo>')
688
689 def test_serialize_request(self):
690 batch = BatchHttpRequest()
691 request = HttpRequest(
692 None,
693 None,
694 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
695 method='POST',
696 body='{}',
697 headers={'content-type': 'application/json'},
698 methodId=None,
699 resumable=None)
700 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500701 self.assertEqual(EXPECTED.splitlines(), s)
Joe Gregorio66f57522011-11-30 11:00:00 -0500702
Joe Gregoriodd813822012-01-25 10:32:47 -0500703 def test_serialize_request_media_body(self):
704 batch = BatchHttpRequest()
705 f = open(datafile('small.png'))
706 body = f.read()
707 f.close()
708
709 request = HttpRequest(
710 None,
711 None,
712 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
713 method='POST',
714 body=body,
715 headers={'content-type': 'application/json'},
716 methodId=None,
717 resumable=None)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500718 # Just testing it shouldn't raise an exception.
Joe Gregoriodd813822012-01-25 10:32:47 -0500719 s = batch._serialize_request(request).splitlines()
720
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500721 def test_serialize_request_no_body(self):
722 batch = BatchHttpRequest()
723 request = HttpRequest(
724 None,
725 None,
726 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
727 method='POST',
728 body='',
729 headers={'content-type': 'application/json'},
730 methodId=None,
731 resumable=None)
732 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500733 self.assertEqual(NO_BODY_EXPECTED.splitlines(), s)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500734
Joe Gregorio66f57522011-11-30 11:00:00 -0500735 def test_deserialize_response(self):
736 batch = BatchHttpRequest()
737 resp, content = batch._deserialize_response(RESPONSE)
738
Joe Gregorio654f4a22012-02-09 14:15:44 -0500739 self.assertEqual(200, resp.status)
740 self.assertEqual('OK', resp.reason)
741 self.assertEqual(11, resp.version)
742 self.assertEqual('{"answer": 42}', content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500743
744 def test_new_id(self):
745 batch = BatchHttpRequest()
746
747 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500748 self.assertEqual('1', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500749
750 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500751 self.assertEqual('2', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500752
753 batch.add(self.request1, request_id='3')
754
755 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500756 self.assertEqual('4', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500757
758 def test_add(self):
759 batch = BatchHttpRequest()
760 batch.add(self.request1, request_id='1')
761 self.assertRaises(KeyError, batch.add, self.request1, request_id='1')
762
763 def test_add_fail_for_resumable(self):
764 batch = BatchHttpRequest()
765
766 upload = MediaFileUpload(
767 datafile('small.png'), chunksize=500, resumable=True)
768 self.request1.resumable = upload
769 self.assertRaises(BatchError, batch.add, self.request1, request_id='1')
770
771 def test_execute(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500772 batch = BatchHttpRequest()
773 callbacks = Callbacks()
774
775 batch.add(self.request1, callback=callbacks.f)
776 batch.add(self.request2, callback=callbacks.f)
777 http = HttpMockSequence([
778 ({'status': '200',
779 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
780 BATCH_RESPONSE),
781 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400782 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500783 self.assertEqual({'foo': 42}, callbacks.responses['1'])
784 self.assertEqual(None, callbacks.exceptions['1'])
785 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
786 self.assertEqual(None, callbacks.exceptions['2'])
Joe Gregorio66f57522011-11-30 11:00:00 -0500787
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500788 def test_execute_request_body(self):
789 batch = BatchHttpRequest()
790
791 batch.add(self.request1)
792 batch.add(self.request2)
793 http = HttpMockSequence([
794 ({'status': '200',
795 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
796 'echo_request_body'),
797 ])
798 try:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400799 batch.execute(http=http)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500800 self.fail('Should raise exception')
INADA Naokic1505df2014-08-20 15:19:53 +0900801 except BatchError as e:
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500802 boundary, _ = e.content.split(None, 1)
803 self.assertEqual('--', boundary[:2])
804 parts = e.content.split(boundary)
805 self.assertEqual(4, len(parts))
806 self.assertEqual('', parts[0])
Craig Citro4282aa32014-06-21 00:33:39 -0700807 self.assertEqual('--', parts[3].rstrip())
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500808 header = parts[1].splitlines()[1]
809 self.assertEqual('Content-Type: application/http', header)
810
Joe Gregorio654f4a22012-02-09 14:15:44 -0500811 def test_execute_refresh_and_retry_on_401(self):
812 batch = BatchHttpRequest()
813 callbacks = Callbacks()
814 cred_1 = MockCredentials('Foo')
815 cred_2 = MockCredentials('Bar')
816
817 http = HttpMockSequence([
818 ({'status': '200',
819 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
820 BATCH_RESPONSE_WITH_401),
821 ({'status': '200',
822 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
823 BATCH_SINGLE_RESPONSE),
824 ])
825
826 creds_http_1 = HttpMockSequence([])
827 cred_1.authorize(creds_http_1)
828
829 creds_http_2 = HttpMockSequence([])
830 cred_2.authorize(creds_http_2)
831
832 self.request1.http = creds_http_1
833 self.request2.http = creds_http_2
834
835 batch.add(self.request1, callback=callbacks.f)
836 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400837 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500838
839 self.assertEqual({'foo': 42}, callbacks.responses['1'])
840 self.assertEqual(None, callbacks.exceptions['1'])
841 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
842 self.assertEqual(None, callbacks.exceptions['2'])
843
844 self.assertEqual(1, cred_1._refreshed)
845 self.assertEqual(0, cred_2._refreshed)
846
847 self.assertEqual(1, cred_1._authorized)
848 self.assertEqual(1, cred_2._authorized)
849
850 self.assertEqual(1, cred_2._applied)
851 self.assertEqual(2, cred_1._applied)
852
853 def test_http_errors_passed_to_callback(self):
854 batch = BatchHttpRequest()
855 callbacks = Callbacks()
856 cred_1 = MockCredentials('Foo')
857 cred_2 = MockCredentials('Bar')
858
859 http = HttpMockSequence([
860 ({'status': '200',
861 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
862 BATCH_RESPONSE_WITH_401),
863 ({'status': '200',
864 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
865 BATCH_RESPONSE_WITH_401),
866 ])
867
868 creds_http_1 = HttpMockSequence([])
869 cred_1.authorize(creds_http_1)
870
871 creds_http_2 = HttpMockSequence([])
872 cred_2.authorize(creds_http_2)
873
874 self.request1.http = creds_http_1
875 self.request2.http = creds_http_2
876
877 batch.add(self.request1, callback=callbacks.f)
878 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400879 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500880
881 self.assertEqual(None, callbacks.responses['1'])
882 self.assertEqual(401, callbacks.exceptions['1'].resp.status)
Joe Gregorioc752e332012-07-11 14:43:52 -0400883 self.assertEqual(
884 'Authorization Required', callbacks.exceptions['1'].resp.reason)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500885 self.assertEqual({u'baz': u'qux'}, callbacks.responses['2'])
886 self.assertEqual(None, callbacks.exceptions['2'])
887
Joe Gregorio66f57522011-11-30 11:00:00 -0500888 def test_execute_global_callback(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500889 callbacks = Callbacks()
890 batch = BatchHttpRequest(callback=callbacks.f)
891
892 batch.add(self.request1)
893 batch.add(self.request2)
894 http = HttpMockSequence([
895 ({'status': '200',
896 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
897 BATCH_RESPONSE),
898 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400899 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500900 self.assertEqual({'foo': 42}, callbacks.responses['1'])
901 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500902
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400903 def test_execute_batch_http_error(self):
Joe Gregorio3fb93672012-07-25 11:31:11 -0400904 callbacks = Callbacks()
905 batch = BatchHttpRequest(callback=callbacks.f)
906
907 batch.add(self.request1)
908 batch.add(self.request2)
909 http = HttpMockSequence([
910 ({'status': '200',
911 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
912 BATCH_ERROR_RESPONSE),
913 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400914 batch.execute(http=http)
Joe Gregorio3fb93672012-07-25 11:31:11 -0400915 self.assertEqual({'foo': 42}, callbacks.responses['1'])
916 expected = ('<HttpError 403 when requesting '
917 'https://www.googleapis.com/someapi/v1/collection/?foo=bar returned '
918 '"Access Not Configured">')
919 self.assertEqual(expected, str(callbacks.exceptions['2']))
Ali Afshar6f11ea12012-02-07 10:32:14 -0500920
Joe Gregorio5c120db2012-08-23 09:13:55 -0400921
Joe Gregorioba5c7902012-08-03 12:48:16 -0400922class TestRequestUriTooLong(unittest.TestCase):
923
924 def test_turn_get_into_post(self):
925
926 def _postproc(resp, content):
927 return content
928
929 http = HttpMockSequence([
930 ({'status': '200'},
931 'echo_request_body'),
932 ({'status': '200'},
933 'echo_request_headers'),
934 ])
935
936 # Send a long query parameter.
937 query = {
938 'q': 'a' * MAX_URI_LENGTH + '?&'
939 }
940 req = HttpRequest(
941 http,
942 _postproc,
943 'http://example.com?' + urllib.urlencode(query),
944 method='GET',
945 body=None,
946 headers={},
947 methodId='foo',
948 resumable=None)
949
950 # Query parameters should be sent in the body.
951 response = req.execute()
952 self.assertEqual('q=' + 'a' * MAX_URI_LENGTH + '%3F%26', response)
953
954 # Extra headers should be set.
955 response = req.execute()
956 self.assertEqual('GET', response['x-http-method-override'])
957 self.assertEqual(str(MAX_URI_LENGTH + 8), response['content-length'])
958 self.assertEqual(
959 'application/x-www-form-urlencoded', response['content-type'])
960
Joe Gregorio5c120db2012-08-23 09:13:55 -0400961
962class TestStreamSlice(unittest.TestCase):
963 """Test _StreamSlice."""
964
965 def setUp(self):
966 self.stream = StringIO.StringIO('0123456789')
967
968 def test_read(self):
969 s = _StreamSlice(self.stream, 0, 4)
970 self.assertEqual('', s.read(0))
971 self.assertEqual('0', s.read(1))
972 self.assertEqual('123', s.read())
973
974 def test_read_too_much(self):
975 s = _StreamSlice(self.stream, 1, 4)
976 self.assertEqual('1234', s.read(6))
977
978 def test_read_all(self):
979 s = _StreamSlice(self.stream, 2, 1)
980 self.assertEqual('2', s.read(-1))
981
Ali Afshar164f37e2013-01-07 14:05:45 -0800982
983class TestResponseCallback(unittest.TestCase):
984 """Test adding callbacks to responses."""
985
986 def test_ensure_response_callback(self):
987 m = JsonModel()
988 request = HttpRequest(
989 None,
990 m.response,
991 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
992 method='POST',
993 body='{}',
994 headers={'content-type': 'application/json'})
995 h = HttpMockSequence([ ({'status': 200}, '{}')])
996 responses = []
997 def _on_response(resp, responses=responses):
998 responses.append(resp)
999 request.add_response_callback(_on_response)
1000 request.execute(http=h)
1001 self.assertEqual(1, len(responses))
1002
1003
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001004if __name__ == '__main__':
Joe Gregorio9086bd32013-06-14 16:32:05 -04001005 logging.getLogger().setLevel(logging.ERROR)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001006 unittest.main()