blob: 3609f40e50e3316668e816db1bf9d1ad8889cee7 [file] [log] [blame]
Joe Gregorio6bcbcea2011-03-10 15:26:05 -05001#!/usr/bin/python2.4
2#
3# Copyright 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Http tests
18
19Unit tests for the apiclient.http.
20"""
21
22__author__ = 'jcgregorio@google.com (Joe Gregorio)'
23
Joe Gregorio7cbceab2011-06-27 10:46:54 -040024# Do not remove the httplib2 import
25import httplib2
Joe Gregoriod0bd3882011-11-22 09:49:47 -050026import os
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050027import unittest
Joe Gregorioba5c7902012-08-03 12:48:16 -040028import urllib
Joe Gregorio910b9b12012-06-12 09:36:30 -040029import StringIO
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050030
Joe Gregorio708388c2012-06-15 13:43:04 -040031from apiclient.discovery import build
Joe Gregorio66f57522011-11-30 11:00:00 -050032from apiclient.errors import BatchError
Joe Gregorio708388c2012-06-15 13:43:04 -040033from apiclient.errors import HttpError
Joe Gregorioc80ac9d2012-08-21 14:09:09 -040034from apiclient.errors import InvalidChunkSizeError
Joe Gregorio66f57522011-11-30 11:00:00 -050035from apiclient.http import BatchHttpRequest
Joe Gregorio708388c2012-06-15 13:43:04 -040036from apiclient.http import HttpMock
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050037from apiclient.http import HttpMockSequence
Joe Gregoriod0bd3882011-11-22 09:49:47 -050038from apiclient.http import HttpRequest
Joe Gregorioc80ac9d2012-08-21 14:09:09 -040039from apiclient.http import MAX_URI_LENGTH
Joe Gregorio5c120db2012-08-23 09:13:55 -040040from apiclient.http import MediaFileUpload
41from apiclient.http import MediaInMemoryUpload
42from apiclient.http import MediaIoBaseDownload
43from apiclient.http import MediaIoBaseUpload
44from apiclient.http import MediaUpload
45from apiclient.http import _StreamSlice
46from apiclient.http import set_user_agent
Joe Gregorio66f57522011-11-30 11:00:00 -050047from apiclient.model import JsonModel
Joe Gregorio654f4a22012-02-09 14:15:44 -050048from oauth2client.client import Credentials
49
50
51class MockCredentials(Credentials):
52 """Mock class for all Credentials objects."""
53 def __init__(self, bearer_token):
54 super(MockCredentials, self).__init__()
55 self._authorized = 0
56 self._refreshed = 0
57 self._applied = 0
58 self._bearer_token = bearer_token
59
60 def authorize(self, http):
61 self._authorized += 1
62
63 request_orig = http.request
64
65 # The closure that will replace 'httplib2.Http.request'.
66 def new_request(uri, method='GET', body=None, headers=None,
67 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
68 connection_type=None):
69 # Modify the request headers to add the appropriate
70 # Authorization header.
71 if headers is None:
72 headers = {}
73 self.apply(headers)
74
75 resp, content = request_orig(uri, method, body, headers,
76 redirections, connection_type)
77
78 return resp, content
79
80 # Replace the request method with our own closure.
81 http.request = new_request
82
83 # Set credentials as a property of the request method.
84 setattr(http.request, 'credentials', self)
85
86 return http
87
88 def refresh(self, http):
89 self._refreshed += 1
90
91 def apply(self, headers):
92 self._applied += 1
93 headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed)
Joe Gregorio6bcbcea2011-03-10 15:26:05 -050094
95
Joe Gregoriod0bd3882011-11-22 09:49:47 -050096DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
97
98
99def datafile(filename):
100 return os.path.join(DATA_DIR, filename)
101
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500102class TestUserAgent(unittest.TestCase):
103
104 def test_set_user_agent(self):
105 http = HttpMockSequence([
106 ({'status': '200'}, 'echo_request_headers'),
107 ])
108
109 http = set_user_agent(http, "my_app/5.5")
110 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500111 self.assertEqual('my_app/5.5', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500112
113 def test_set_user_agent_nested(self):
114 http = HttpMockSequence([
115 ({'status': '200'}, 'echo_request_headers'),
116 ])
117
118 http = set_user_agent(http, "my_app/5.5")
119 http = set_user_agent(http, "my_library/0.1")
120 resp, content = http.request("http://example.com")
Joe Gregorio654f4a22012-02-09 14:15:44 -0500121 self.assertEqual('my_app/5.5 my_library/0.1', content['user-agent'])
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500122
Joe Gregorio910b9b12012-06-12 09:36:30 -0400123
124class TestMediaUpload(unittest.TestCase):
125
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500126 def test_media_file_upload_to_from_json(self):
127 upload = MediaFileUpload(
128 datafile('small.png'), chunksize=500, resumable=True)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500129 self.assertEqual('image/png', upload.mimetype())
130 self.assertEqual(190, upload.size())
131 self.assertEqual(True, upload.resumable())
132 self.assertEqual(500, upload.chunksize())
133 self.assertEqual('PNG', upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500134
135 json = upload.to_json()
136 new_upload = MediaUpload.new_from_json(json)
137
Joe Gregorio654f4a22012-02-09 14:15:44 -0500138 self.assertEqual('image/png', new_upload.mimetype())
139 self.assertEqual(190, new_upload.size())
140 self.assertEqual(True, new_upload.resumable())
141 self.assertEqual(500, new_upload.chunksize())
142 self.assertEqual('PNG', new_upload.getbytes(1, 3))
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500143
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400144 def test_media_file_upload_raises_on_invalid_chunksize(self):
145 self.assertRaises(InvalidChunkSizeError, MediaFileUpload,
146 datafile('small.png'), mimetype='image/png', chunksize=-2,
147 resumable=True)
148
Ali Afshar1cb6b672012-03-12 08:46:14 -0400149 def test_media_inmemory_upload(self):
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400150 media = MediaInMemoryUpload('abcdef', mimetype='text/plain', chunksize=10,
Ali Afshar1cb6b672012-03-12 08:46:14 -0400151 resumable=True)
152 self.assertEqual('text/plain', media.mimetype())
153 self.assertEqual(10, media.chunksize())
154 self.assertTrue(media.resumable())
155 self.assertEqual('bc', media.getbytes(1, 2))
156 self.assertEqual(6, media.size())
157
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500158 def test_http_request_to_from_json(self):
159
160 def _postproc(*kwargs):
161 pass
162
163 http = httplib2.Http()
164 media_upload = MediaFileUpload(
165 datafile('small.png'), chunksize=500, resumable=True)
166 req = HttpRequest(
167 http,
168 _postproc,
169 'http://example.com',
170 method='POST',
171 body='{}',
172 headers={'content-type': 'multipart/related; boundary="---flubber"'},
173 methodId='foo',
174 resumable=media_upload)
175
176 json = req.to_json()
177 new_req = HttpRequest.from_json(json, http, _postproc)
178
Joe Gregorio654f4a22012-02-09 14:15:44 -0500179 self.assertEqual({'content-type':
180 'multipart/related; boundary="---flubber"'},
181 new_req.headers)
182 self.assertEqual('http://example.com', new_req.uri)
183 self.assertEqual('{}', new_req.body)
184 self.assertEqual(http, new_req.http)
185 self.assertEqual(media_upload.to_json(), new_req.resumable.to_json())
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500186
Joe Gregorio910b9b12012-06-12 09:36:30 -0400187
188class TestMediaIoBaseUpload(unittest.TestCase):
189
190 def test_media_io_base_upload_from_file_io(self):
191 try:
192 import io
193
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400194 fd = io.FileIO(datafile('small.png'), 'r')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400195 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400196 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400197 self.assertEqual('image/png', upload.mimetype())
198 self.assertEqual(190, upload.size())
199 self.assertEqual(True, upload.resumable())
200 self.assertEqual(500, upload.chunksize())
201 self.assertEqual('PNG', upload.getbytes(1, 3))
202 except ImportError:
203 pass
204
205 def test_media_io_base_upload_from_file_object(self):
206 f = open(datafile('small.png'), 'r')
207 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400208 fd=f, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400209 self.assertEqual('image/png', upload.mimetype())
210 self.assertEqual(190, upload.size())
211 self.assertEqual(True, upload.resumable())
212 self.assertEqual(500, upload.chunksize())
213 self.assertEqual('PNG', upload.getbytes(1, 3))
214 f.close()
215
216 def test_media_io_base_upload_serializable(self):
217 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400218 upload = MediaIoBaseUpload(fd=f, mimetype='image/png')
Joe Gregorio910b9b12012-06-12 09:36:30 -0400219
220 try:
221 json = upload.to_json()
222 self.fail('MediaIoBaseUpload should not be serializable.')
223 except NotImplementedError:
224 pass
225
226 def test_media_io_base_upload_from_string_io(self):
227 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400228 fd = StringIO.StringIO(f.read())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400229 f.close()
230
231 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400232 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400233 self.assertEqual('image/png', upload.mimetype())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400234 self.assertEqual(190, upload.size())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400235 self.assertEqual(True, upload.resumable())
236 self.assertEqual(500, upload.chunksize())
237 self.assertEqual('PNG', upload.getbytes(1, 3))
238 f.close()
239
240 def test_media_io_base_upload_from_bytes(self):
241 try:
242 import io
243
244 f = open(datafile('small.png'), 'r')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400245 fd = io.BytesIO(f.read())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400246 upload = MediaIoBaseUpload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400247 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
Joe Gregorio910b9b12012-06-12 09:36:30 -0400248 self.assertEqual('image/png', upload.mimetype())
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400249 self.assertEqual(190, upload.size())
Joe Gregorio910b9b12012-06-12 09:36:30 -0400250 self.assertEqual(True, upload.resumable())
251 self.assertEqual(500, upload.chunksize())
252 self.assertEqual('PNG', upload.getbytes(1, 3))
253 except ImportError:
254 pass
255
Joe Gregorioc80ac9d2012-08-21 14:09:09 -0400256 def test_media_io_base_upload_raises_on_invalid_chunksize(self):
257 try:
258 import io
259
260 f = open(datafile('small.png'), 'r')
261 fd = io.BytesIO(f.read())
262 self.assertRaises(InvalidChunkSizeError, MediaIoBaseUpload,
263 fd, 'image/png', chunksize=-2, resumable=True)
264 except ImportError:
265 pass
266
267 def test_media_io_base_upload_streamable(self):
268 try:
269 import io
270
271 fd = io.BytesIO('stuff')
272 upload = MediaIoBaseUpload(
273 fd=fd, mimetype='image/png', chunksize=500, resumable=True)
274 self.assertEqual(True, upload.has_stream())
275 self.assertEqual(fd, upload.stream())
276 except ImportError:
277 pass
278
Joe Gregorio910b9b12012-06-12 09:36:30 -0400279
Joe Gregorio708388c2012-06-15 13:43:04 -0400280class TestMediaIoBaseDownload(unittest.TestCase):
281
282 def setUp(self):
283 http = HttpMock(datafile('zoo.json'), {'status': '200'})
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400284 zoo = build('zoo', 'v1', http=http)
Joe Gregorio708388c2012-06-15 13:43:04 -0400285 self.request = zoo.animals().get_media(name='Lion')
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400286 self.fd = StringIO.StringIO()
Joe Gregorio708388c2012-06-15 13:43:04 -0400287
288 def test_media_io_base_download(self):
289 self.request.http = HttpMockSequence([
290 ({'status': '200',
291 'content-range': '0-2/5'}, '123'),
292 ({'status': '200',
293 'content-range': '3-4/5'}, '45'),
294 ])
295
296 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400297 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400298
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400299 self.assertEqual(self.fd, download._fd)
300 self.assertEqual(3, download._chunksize)
301 self.assertEqual(0, download._progress)
302 self.assertEqual(None, download._total_size)
303 self.assertEqual(False, download._done)
304 self.assertEqual(self.request.uri, download._uri)
Joe Gregorio708388c2012-06-15 13:43:04 -0400305
306 status, done = download.next_chunk()
307
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400308 self.assertEqual(self.fd.getvalue(), '123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400309 self.assertEqual(False, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400310 self.assertEqual(3, download._progress)
311 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400312 self.assertEqual(3, status.resumable_progress)
313
314 status, done = download.next_chunk()
315
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400316 self.assertEqual(self.fd.getvalue(), '12345')
Joe Gregorio708388c2012-06-15 13:43:04 -0400317 self.assertEqual(True, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400318 self.assertEqual(5, download._progress)
319 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400320
321 def test_media_io_base_download_handle_redirects(self):
322 self.request.http = HttpMockSequence([
323 ({'status': '307',
324 'location': 'https://secure.example.net/lion'}, ''),
325 ({'status': '200',
326 'content-range': '0-2/5'}, 'abc'),
327 ])
328
329 download = MediaIoBaseDownload(
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400330 fd=self.fd, request=self.request, chunksize=3)
Joe Gregorio708388c2012-06-15 13:43:04 -0400331
332 status, done = download.next_chunk()
333
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400334 self.assertEqual('https://secure.example.net/lion', download._uri)
335 self.assertEqual(self.fd.getvalue(), 'abc')
Joe Gregorio708388c2012-06-15 13:43:04 -0400336 self.assertEqual(False, done)
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400337 self.assertEqual(3, download._progress)
338 self.assertEqual(5, download._total_size)
Joe Gregorio708388c2012-06-15 13:43:04 -0400339
340 def test_media_io_base_download_handle_4xx(self):
341 self.request.http = HttpMockSequence([
342 ({'status': '400'}, ''),
343 ])
344
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
348 try:
349 status, done = download.next_chunk()
350 self.fail('Should raise an exception')
351 except HttpError:
352 pass
353
354 # Even after raising an exception we can pick up where we left off.
355 self.request.http = HttpMockSequence([
356 ({'status': '200',
357 'content-range': '0-2/5'}, '123'),
358 ])
359
360 status, done = download.next_chunk()
361
Joe Gregorio4a2c29f2012-07-12 12:52:47 -0400362 self.assertEqual(self.fd.getvalue(), '123')
Joe Gregorio708388c2012-06-15 13:43:04 -0400363
Joe Gregorio66f57522011-11-30 11:00:00 -0500364EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
365Content-Type: application/json
366MIME-Version: 1.0
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500367Host: www.googleapis.com
368content-length: 2\r\n\r\n{}"""
369
370
371NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1
372Content-Type: application/json
373MIME-Version: 1.0
374Host: www.googleapis.com
375content-length: 0\r\n\r\n"""
Joe Gregorio66f57522011-11-30 11:00:00 -0500376
377
378RESPONSE = """HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400379Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500380Content-Length: 14
381ETag: "etag/pony"\r\n\r\n{"answer": 42}"""
382
383
384BATCH_RESPONSE = """--batch_foobarbaz
385Content-Type: application/http
386Content-Transfer-Encoding: binary
387Content-ID: <randomness+1>
388
389HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400390Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500391Content-Length: 14
392ETag: "etag/pony"\r\n\r\n{"foo": 42}
393
394--batch_foobarbaz
395Content-Type: application/http
396Content-Transfer-Encoding: binary
397Content-ID: <randomness+2>
398
399HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400400Content-Type: application/json
Joe Gregorio66f57522011-11-30 11:00:00 -0500401Content-Length: 14
402ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
403--batch_foobarbaz--"""
404
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500405
Joe Gregorio3fb93672012-07-25 11:31:11 -0400406BATCH_ERROR_RESPONSE = """--batch_foobarbaz
407Content-Type: application/http
408Content-Transfer-Encoding: binary
409Content-ID: <randomness+1>
410
411HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400412Content-Type: application/json
Joe Gregorio3fb93672012-07-25 11:31:11 -0400413Content-Length: 14
414ETag: "etag/pony"\r\n\r\n{"foo": 42}
415
416--batch_foobarbaz
417Content-Type: application/http
418Content-Transfer-Encoding: binary
419Content-ID: <randomness+2>
420
421HTTP/1.1 403 Access Not Configured
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400422Content-Type: application/json
423Content-Length: 245
424ETag: "etag/sheep"\r\n\r\n{
Joe Gregorio3fb93672012-07-25 11:31:11 -0400425 "error": {
426 "errors": [
427 {
428 "domain": "usageLimits",
429 "reason": "accessNotConfigured",
430 "message": "Access Not Configured",
431 "debugInfo": "QuotaState: BLOCKED"
432 }
433 ],
434 "code": 403,
435 "message": "Access Not Configured"
436 }
437}
438
439--batch_foobarbaz--"""
440
441
Joe Gregorio654f4a22012-02-09 14:15:44 -0500442BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz
443Content-Type: application/http
444Content-Transfer-Encoding: binary
445Content-ID: <randomness+1>
446
Joe Gregorioc752e332012-07-11 14:43:52 -0400447HTTP/1.1 401 Authorization Required
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400448Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500449Content-Length: 14
450ETag: "etag/pony"\r\n\r\n{"error": {"message":
451 "Authorizaton failed."}}
452
453--batch_foobarbaz
454Content-Type: application/http
455Content-Transfer-Encoding: binary
456Content-ID: <randomness+2>
457
458HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400459Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500460Content-Length: 14
461ETag: "etag/sheep"\r\n\r\n{"baz": "qux"}
462--batch_foobarbaz--"""
463
464
465BATCH_SINGLE_RESPONSE = """--batch_foobarbaz
466Content-Type: application/http
467Content-Transfer-Encoding: binary
468Content-ID: <randomness+1>
469
470HTTP/1.1 200 OK
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400471Content-Type: application/json
Joe Gregorio654f4a22012-02-09 14:15:44 -0500472Content-Length: 14
473ETag: "etag/pony"\r\n\r\n{"foo": 42}
474--batch_foobarbaz--"""
475
476class Callbacks(object):
477 def __init__(self):
478 self.responses = {}
479 self.exceptions = {}
480
481 def f(self, request_id, response, exception):
482 self.responses[request_id] = response
483 self.exceptions[request_id] = exception
484
485
Joe Gregorio66f57522011-11-30 11:00:00 -0500486class TestBatch(unittest.TestCase):
487
488 def setUp(self):
489 model = JsonModel()
490 self.request1 = HttpRequest(
491 None,
492 model.response,
493 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
494 method='POST',
495 body='{}',
496 headers={'content-type': 'application/json'})
497
498 self.request2 = HttpRequest(
499 None,
500 model.response,
501 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500502 method='GET',
503 body='',
Joe Gregorio66f57522011-11-30 11:00:00 -0500504 headers={'content-type': 'application/json'})
505
506
507 def test_id_to_from_content_id_header(self):
508 batch = BatchHttpRequest()
509 self.assertEquals('12', batch._header_to_id(batch._id_to_header('12')))
510
511 def test_invalid_content_id_header(self):
512 batch = BatchHttpRequest()
513 self.assertRaises(BatchError, batch._header_to_id, '[foo+x]')
514 self.assertRaises(BatchError, batch._header_to_id, 'foo+1')
515 self.assertRaises(BatchError, batch._header_to_id, '<foo>')
516
517 def test_serialize_request(self):
518 batch = BatchHttpRequest()
519 request = HttpRequest(
520 None,
521 None,
522 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
523 method='POST',
524 body='{}',
525 headers={'content-type': 'application/json'},
526 methodId=None,
527 resumable=None)
528 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500529 self.assertEqual(EXPECTED.splitlines(), s)
Joe Gregorio66f57522011-11-30 11:00:00 -0500530
Joe Gregoriodd813822012-01-25 10:32:47 -0500531 def test_serialize_request_media_body(self):
532 batch = BatchHttpRequest()
533 f = open(datafile('small.png'))
534 body = f.read()
535 f.close()
536
537 request = HttpRequest(
538 None,
539 None,
540 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
541 method='POST',
542 body=body,
543 headers={'content-type': 'application/json'},
544 methodId=None,
545 resumable=None)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500546 # Just testing it shouldn't raise an exception.
Joe Gregoriodd813822012-01-25 10:32:47 -0500547 s = batch._serialize_request(request).splitlines()
548
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500549 def test_serialize_request_no_body(self):
550 batch = BatchHttpRequest()
551 request = HttpRequest(
552 None,
553 None,
554 'https://www.googleapis.com/someapi/v1/collection/?foo=bar',
555 method='POST',
556 body='',
557 headers={'content-type': 'application/json'},
558 methodId=None,
559 resumable=None)
560 s = batch._serialize_request(request).splitlines()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500561 self.assertEqual(NO_BODY_EXPECTED.splitlines(), s)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500562
Joe Gregorio66f57522011-11-30 11:00:00 -0500563 def test_deserialize_response(self):
564 batch = BatchHttpRequest()
565 resp, content = batch._deserialize_response(RESPONSE)
566
Joe Gregorio654f4a22012-02-09 14:15:44 -0500567 self.assertEqual(200, resp.status)
568 self.assertEqual('OK', resp.reason)
569 self.assertEqual(11, resp.version)
570 self.assertEqual('{"answer": 42}', content)
Joe Gregorio66f57522011-11-30 11:00:00 -0500571
572 def test_new_id(self):
573 batch = BatchHttpRequest()
574
575 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500576 self.assertEqual('1', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500577
578 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500579 self.assertEqual('2', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500580
581 batch.add(self.request1, request_id='3')
582
583 id_ = batch._new_id()
Joe Gregorio654f4a22012-02-09 14:15:44 -0500584 self.assertEqual('4', id_)
Joe Gregorio66f57522011-11-30 11:00:00 -0500585
586 def test_add(self):
587 batch = BatchHttpRequest()
588 batch.add(self.request1, request_id='1')
589 self.assertRaises(KeyError, batch.add, self.request1, request_id='1')
590
591 def test_add_fail_for_resumable(self):
592 batch = BatchHttpRequest()
593
594 upload = MediaFileUpload(
595 datafile('small.png'), chunksize=500, resumable=True)
596 self.request1.resumable = upload
597 self.assertRaises(BatchError, batch.add, self.request1, request_id='1')
598
599 def test_execute(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500600 batch = BatchHttpRequest()
601 callbacks = Callbacks()
602
603 batch.add(self.request1, callback=callbacks.f)
604 batch.add(self.request2, callback=callbacks.f)
605 http = HttpMockSequence([
606 ({'status': '200',
607 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
608 BATCH_RESPONSE),
609 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400610 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500611 self.assertEqual({'foo': 42}, callbacks.responses['1'])
612 self.assertEqual(None, callbacks.exceptions['1'])
613 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
614 self.assertEqual(None, callbacks.exceptions['2'])
Joe Gregorio66f57522011-11-30 11:00:00 -0500615
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500616 def test_execute_request_body(self):
617 batch = BatchHttpRequest()
618
619 batch.add(self.request1)
620 batch.add(self.request2)
621 http = HttpMockSequence([
622 ({'status': '200',
623 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
624 'echo_request_body'),
625 ])
626 try:
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400627 batch.execute(http=http)
Joe Gregorio5d1171b2012-01-05 10:48:24 -0500628 self.fail('Should raise exception')
629 except BatchError, e:
630 boundary, _ = e.content.split(None, 1)
631 self.assertEqual('--', boundary[:2])
632 parts = e.content.split(boundary)
633 self.assertEqual(4, len(parts))
634 self.assertEqual('', parts[0])
635 self.assertEqual('--', parts[3])
636 header = parts[1].splitlines()[1]
637 self.assertEqual('Content-Type: application/http', header)
638
Joe Gregorio654f4a22012-02-09 14:15:44 -0500639 def test_execute_refresh_and_retry_on_401(self):
640 batch = BatchHttpRequest()
641 callbacks = Callbacks()
642 cred_1 = MockCredentials('Foo')
643 cred_2 = MockCredentials('Bar')
644
645 http = HttpMockSequence([
646 ({'status': '200',
647 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
648 BATCH_RESPONSE_WITH_401),
649 ({'status': '200',
650 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
651 BATCH_SINGLE_RESPONSE),
652 ])
653
654 creds_http_1 = HttpMockSequence([])
655 cred_1.authorize(creds_http_1)
656
657 creds_http_2 = HttpMockSequence([])
658 cred_2.authorize(creds_http_2)
659
660 self.request1.http = creds_http_1
661 self.request2.http = creds_http_2
662
663 batch.add(self.request1, callback=callbacks.f)
664 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400665 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500666
667 self.assertEqual({'foo': 42}, callbacks.responses['1'])
668 self.assertEqual(None, callbacks.exceptions['1'])
669 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
670 self.assertEqual(None, callbacks.exceptions['2'])
671
672 self.assertEqual(1, cred_1._refreshed)
673 self.assertEqual(0, cred_2._refreshed)
674
675 self.assertEqual(1, cred_1._authorized)
676 self.assertEqual(1, cred_2._authorized)
677
678 self.assertEqual(1, cred_2._applied)
679 self.assertEqual(2, cred_1._applied)
680
681 def test_http_errors_passed_to_callback(self):
682 batch = BatchHttpRequest()
683 callbacks = Callbacks()
684 cred_1 = MockCredentials('Foo')
685 cred_2 = MockCredentials('Bar')
686
687 http = HttpMockSequence([
688 ({'status': '200',
689 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
690 BATCH_RESPONSE_WITH_401),
691 ({'status': '200',
692 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
693 BATCH_RESPONSE_WITH_401),
694 ])
695
696 creds_http_1 = HttpMockSequence([])
697 cred_1.authorize(creds_http_1)
698
699 creds_http_2 = HttpMockSequence([])
700 cred_2.authorize(creds_http_2)
701
702 self.request1.http = creds_http_1
703 self.request2.http = creds_http_2
704
705 batch.add(self.request1, callback=callbacks.f)
706 batch.add(self.request2, callback=callbacks.f)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400707 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500708
709 self.assertEqual(None, callbacks.responses['1'])
710 self.assertEqual(401, callbacks.exceptions['1'].resp.status)
Joe Gregorioc752e332012-07-11 14:43:52 -0400711 self.assertEqual(
712 'Authorization Required', callbacks.exceptions['1'].resp.reason)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500713 self.assertEqual({u'baz': u'qux'}, callbacks.responses['2'])
714 self.assertEqual(None, callbacks.exceptions['2'])
715
Joe Gregorio66f57522011-11-30 11:00:00 -0500716 def test_execute_global_callback(self):
Joe Gregorio66f57522011-11-30 11:00:00 -0500717 callbacks = Callbacks()
718 batch = BatchHttpRequest(callback=callbacks.f)
719
720 batch.add(self.request1)
721 batch.add(self.request2)
722 http = HttpMockSequence([
723 ({'status': '200',
724 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
725 BATCH_RESPONSE),
726 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400727 batch.execute(http=http)
Joe Gregorio654f4a22012-02-09 14:15:44 -0500728 self.assertEqual({'foo': 42}, callbacks.responses['1'])
729 self.assertEqual({'baz': 'qux'}, callbacks.responses['2'])
Joe Gregoriod0bd3882011-11-22 09:49:47 -0500730
Joe Gregorio20b54fb2012-07-26 09:59:35 -0400731 def test_execute_batch_http_error(self):
Joe Gregorio3fb93672012-07-25 11:31:11 -0400732 callbacks = Callbacks()
733 batch = BatchHttpRequest(callback=callbacks.f)
734
735 batch.add(self.request1)
736 batch.add(self.request2)
737 http = HttpMockSequence([
738 ({'status': '200',
739 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
740 BATCH_ERROR_RESPONSE),
741 ])
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400742 batch.execute(http=http)
Joe Gregorio3fb93672012-07-25 11:31:11 -0400743 self.assertEqual({'foo': 42}, callbacks.responses['1'])
744 expected = ('<HttpError 403 when requesting '
745 'https://www.googleapis.com/someapi/v1/collection/?foo=bar returned '
746 '"Access Not Configured">')
747 self.assertEqual(expected, str(callbacks.exceptions['2']))
Ali Afshar6f11ea12012-02-07 10:32:14 -0500748
Joe Gregorio5c120db2012-08-23 09:13:55 -0400749
Joe Gregorioba5c7902012-08-03 12:48:16 -0400750class TestRequestUriTooLong(unittest.TestCase):
751
752 def test_turn_get_into_post(self):
753
754 def _postproc(resp, content):
755 return content
756
757 http = HttpMockSequence([
758 ({'status': '200'},
759 'echo_request_body'),
760 ({'status': '200'},
761 'echo_request_headers'),
762 ])
763
764 # Send a long query parameter.
765 query = {
766 'q': 'a' * MAX_URI_LENGTH + '?&'
767 }
768 req = HttpRequest(
769 http,
770 _postproc,
771 'http://example.com?' + urllib.urlencode(query),
772 method='GET',
773 body=None,
774 headers={},
775 methodId='foo',
776 resumable=None)
777
778 # Query parameters should be sent in the body.
779 response = req.execute()
780 self.assertEqual('q=' + 'a' * MAX_URI_LENGTH + '%3F%26', response)
781
782 # Extra headers should be set.
783 response = req.execute()
784 self.assertEqual('GET', response['x-http-method-override'])
785 self.assertEqual(str(MAX_URI_LENGTH + 8), response['content-length'])
786 self.assertEqual(
787 'application/x-www-form-urlencoded', response['content-type'])
788
Joe Gregorio5c120db2012-08-23 09:13:55 -0400789
790class TestStreamSlice(unittest.TestCase):
791 """Test _StreamSlice."""
792
793 def setUp(self):
794 self.stream = StringIO.StringIO('0123456789')
795
796 def test_read(self):
797 s = _StreamSlice(self.stream, 0, 4)
798 self.assertEqual('', s.read(0))
799 self.assertEqual('0', s.read(1))
800 self.assertEqual('123', s.read())
801
802 def test_read_too_much(self):
803 s = _StreamSlice(self.stream, 1, 4)
804 self.assertEqual('1234', s.read(6))
805
806 def test_read_all(self):
807 s = _StreamSlice(self.stream, 2, 1)
808 self.assertEqual('2', s.read(-1))
809
Joe Gregorio6bcbcea2011-03-10 15:26:05 -0500810if __name__ == '__main__':
811 unittest.main()