blob: 5ae8ab6b5ad1f3ee222848e8bd6824c0592a7a5a [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
John Asmuth864311d2014-04-24 15:46:08 -040019Unit tests for the googleapiclient.http.
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050020"""
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
John Asmuth864311d2014-04-24 15:46:08 -040034from googleapiclient.discovery import build
35from googleapiclient.errors import BatchError
36from googleapiclient.errors import HttpError
37from googleapiclient.errors import InvalidChunkSizeError
38from googleapiclient.http import BatchHttpRequest
39from googleapiclient.http import HttpMock
40from googleapiclient.http import HttpMockSequence
41from googleapiclient.http import HttpRequest
42from googleapiclient.http import MAX_URI_LENGTH
43from googleapiclient.http import MediaFileUpload
44from googleapiclient.http import MediaInMemoryUpload
45from googleapiclient.http import MediaIoBaseDownload
46from googleapiclient.http import MediaIoBaseUpload
47from googleapiclient.http import MediaUpload
48from googleapiclient.http import _StreamSlice
49from googleapiclient.http import set_user_agent
50from googleapiclient.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])
Craig Citro4282aa32014-06-21 00:33:39 -0700805 self.assertEqual('--', parts[3].rstrip())
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500806 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()