#!/usr/bin/python2.4 # # Copyright 2010 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Http tests Unit tests for the apiclient.http. """ __author__ = 'jcgregorio@google.com (Joe Gregorio)' # Do not remove the httplib2 import import httplib2 import os import unittest from apiclient.errors import BatchError from apiclient.http import BatchHttpRequest from apiclient.http import HttpMockSequence from apiclient.http import HttpRequest from apiclient.http import MediaFileUpload from apiclient.http import MediaUpload from apiclient.http import MediaInMemoryUpload from apiclient.http import set_user_agent from apiclient.model import JsonModel from oauth2client.client import Credentials class MockCredentials(Credentials): """Mock class for all Credentials objects.""" def __init__(self, bearer_token): super(MockCredentials, self).__init__() self._authorized = 0 self._refreshed = 0 self._applied = 0 self._bearer_token = bearer_token def authorize(self, http): self._authorized += 1 request_orig = http.request # The closure that will replace 'httplib2.Http.request'. def new_request(uri, method='GET', body=None, headers=None, redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None): # Modify the request headers to add the appropriate # Authorization header. if headers is None: headers = {} self.apply(headers) resp, content = request_orig(uri, method, body, headers, redirections, connection_type) return resp, content # Replace the request method with our own closure. http.request = new_request # Set credentials as a property of the request method. setattr(http.request, 'credentials', self) return http def refresh(self, http): self._refreshed += 1 def apply(self, headers): self._applied += 1 headers['authorization'] = self._bearer_token + ' ' + str(self._refreshed) DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') def datafile(filename): return os.path.join(DATA_DIR, filename) class TestUserAgent(unittest.TestCase): def test_set_user_agent(self): http = HttpMockSequence([ ({'status': '200'}, 'echo_request_headers'), ]) http = set_user_agent(http, "my_app/5.5") resp, content = http.request("http://example.com") self.assertEqual('my_app/5.5', content['user-agent']) def test_set_user_agent_nested(self): http = HttpMockSequence([ ({'status': '200'}, 'echo_request_headers'), ]) http = set_user_agent(http, "my_app/5.5") http = set_user_agent(http, "my_library/0.1") resp, content = http.request("http://example.com") self.assertEqual('my_app/5.5 my_library/0.1', content['user-agent']) def test_media_file_upload_to_from_json(self): upload = MediaFileUpload( datafile('small.png'), chunksize=500, resumable=True) self.assertEqual('image/png', upload.mimetype()) self.assertEqual(190, upload.size()) self.assertEqual(True, upload.resumable()) self.assertEqual(500, upload.chunksize()) self.assertEqual('PNG', upload.getbytes(1, 3)) json = upload.to_json() new_upload = MediaUpload.new_from_json(json) self.assertEqual('image/png', new_upload.mimetype()) self.assertEqual(190, new_upload.size()) self.assertEqual(True, new_upload.resumable()) self.assertEqual(500, new_upload.chunksize()) self.assertEqual('PNG', new_upload.getbytes(1, 3)) def test_http_request_to_from_json(self): def _postproc(*kwargs): pass http = httplib2.Http() media_upload = MediaFileUpload( datafile('small.png'), chunksize=500, resumable=True) req = HttpRequest( http, _postproc, 'http://example.com', method='POST', body='{}', headers={'content-type': 'multipart/related; boundary="---flubber"'}, methodId='foo', resumable=media_upload) json = req.to_json() new_req = HttpRequest.from_json(json, http, _postproc) self.assertEqual({'content-type': 'multipart/related; boundary="---flubber"'}, new_req.headers) self.assertEqual('http://example.com', new_req.uri) self.assertEqual('{}', new_req.body) self.assertEqual(http, new_req.http) self.assertEqual(media_upload.to_json(), new_req.resumable.to_json()) EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1 Content-Type: application/json MIME-Version: 1.0 Host: www.googleapis.com content-length: 2\r\n\r\n{}""" NO_BODY_EXPECTED = """POST /someapi/v1/collection/?foo=bar HTTP/1.1 Content-Type: application/json MIME-Version: 1.0 Host: www.googleapis.com content-length: 0\r\n\r\n""" RESPONSE = """HTTP/1.1 200 OK Content-Type application/json Content-Length: 14 ETag: "etag/pony"\r\n\r\n{"answer": 42}""" BATCH_RESPONSE = """--batch_foobarbaz Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: HTTP/1.1 200 OK Content-Type application/json Content-Length: 14 ETag: "etag/pony"\r\n\r\n{"foo": 42} --batch_foobarbaz Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: HTTP/1.1 200 OK Content-Type application/json Content-Length: 14 ETag: "etag/sheep"\r\n\r\n{"baz": "qux"} --batch_foobarbaz--""" BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: HTTP/1.1 401 Authoration Required Content-Type application/json Content-Length: 14 ETag: "etag/pony"\r\n\r\n{"error": {"message": "Authorizaton failed."}} --batch_foobarbaz Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: HTTP/1.1 200 OK Content-Type application/json Content-Length: 14 ETag: "etag/sheep"\r\n\r\n{"baz": "qux"} --batch_foobarbaz--""" BATCH_SINGLE_RESPONSE = """--batch_foobarbaz Content-Type: application/http Content-Transfer-Encoding: binary Content-ID: HTTP/1.1 200 OK Content-Type application/json Content-Length: 14 ETag: "etag/pony"\r\n\r\n{"foo": 42} --batch_foobarbaz--""" class Callbacks(object): def __init__(self): self.responses = {} self.exceptions = {} def f(self, request_id, response, exception): self.responses[request_id] = response self.exceptions[request_id] = exception class TestBatch(unittest.TestCase): def setUp(self): model = JsonModel() self.request1 = HttpRequest( None, model.response, 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', method='POST', body='{}', headers={'content-type': 'application/json'}) self.request2 = HttpRequest( None, model.response, 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', method='GET', body='', headers={'content-type': 'application/json'}) def test_id_to_from_content_id_header(self): batch = BatchHttpRequest() self.assertEquals('12', batch._header_to_id(batch._id_to_header('12'))) def test_invalid_content_id_header(self): batch = BatchHttpRequest() self.assertRaises(BatchError, batch._header_to_id, '[foo+x]') self.assertRaises(BatchError, batch._header_to_id, 'foo+1') self.assertRaises(BatchError, batch._header_to_id, '') def test_serialize_request(self): batch = BatchHttpRequest() request = HttpRequest( None, None, 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', method='POST', body='{}', headers={'content-type': 'application/json'}, methodId=None, resumable=None) s = batch._serialize_request(request).splitlines() self.assertEqual(EXPECTED.splitlines(), s) def test_serialize_request_media_body(self): batch = BatchHttpRequest() f = open(datafile('small.png')) body = f.read() f.close() request = HttpRequest( None, None, 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', method='POST', body=body, headers={'content-type': 'application/json'}, methodId=None, resumable=None) # Just testing it shouldn't raise an exception. s = batch._serialize_request(request).splitlines() def test_serialize_request_no_body(self): batch = BatchHttpRequest() request = HttpRequest( None, None, 'https://www.googleapis.com/someapi/v1/collection/?foo=bar', method='POST', body='', headers={'content-type': 'application/json'}, methodId=None, resumable=None) s = batch._serialize_request(request).splitlines() self.assertEqual(NO_BODY_EXPECTED.splitlines(), s) def test_deserialize_response(self): batch = BatchHttpRequest() resp, content = batch._deserialize_response(RESPONSE) self.assertEqual(200, resp.status) self.assertEqual('OK', resp.reason) self.assertEqual(11, resp.version) self.assertEqual('{"answer": 42}', content) def test_new_id(self): batch = BatchHttpRequest() id_ = batch._new_id() self.assertEqual('1', id_) id_ = batch._new_id() self.assertEqual('2', id_) batch.add(self.request1, request_id='3') id_ = batch._new_id() self.assertEqual('4', id_) def test_add(self): batch = BatchHttpRequest() batch.add(self.request1, request_id='1') self.assertRaises(KeyError, batch.add, self.request1, request_id='1') def test_add_fail_for_resumable(self): batch = BatchHttpRequest() upload = MediaFileUpload( datafile('small.png'), chunksize=500, resumable=True) self.request1.resumable = upload self.assertRaises(BatchError, batch.add, self.request1, request_id='1') def test_execute(self): batch = BatchHttpRequest() callbacks = Callbacks() batch.add(self.request1, callback=callbacks.f) batch.add(self.request2, callback=callbacks.f) http = HttpMockSequence([ ({'status': '200', 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, BATCH_RESPONSE), ]) batch.execute(http) self.assertEqual({'foo': 42}, callbacks.responses['1']) self.assertEqual(None, callbacks.exceptions['1']) self.assertEqual({'baz': 'qux'}, callbacks.responses['2']) self.assertEqual(None, callbacks.exceptions['2']) def test_execute_request_body(self): batch = BatchHttpRequest() batch.add(self.request1) batch.add(self.request2) http = HttpMockSequence([ ({'status': '200', 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, 'echo_request_body'), ]) try: batch.execute(http) self.fail('Should raise exception') except BatchError, e: boundary, _ = e.content.split(None, 1) self.assertEqual('--', boundary[:2]) parts = e.content.split(boundary) self.assertEqual(4, len(parts)) self.assertEqual('', parts[0]) self.assertEqual('--', parts[3]) header = parts[1].splitlines()[1] self.assertEqual('Content-Type: application/http', header) def test_execute_refresh_and_retry_on_401(self): batch = BatchHttpRequest() callbacks = Callbacks() cred_1 = MockCredentials('Foo') cred_2 = MockCredentials('Bar') http = HttpMockSequence([ ({'status': '200', 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, BATCH_RESPONSE_WITH_401), ({'status': '200', 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, BATCH_SINGLE_RESPONSE), ]) creds_http_1 = HttpMockSequence([]) cred_1.authorize(creds_http_1) creds_http_2 = HttpMockSequence([]) cred_2.authorize(creds_http_2) self.request1.http = creds_http_1 self.request2.http = creds_http_2 batch.add(self.request1, callback=callbacks.f) batch.add(self.request2, callback=callbacks.f) batch.execute(http) self.assertEqual({'foo': 42}, callbacks.responses['1']) self.assertEqual(None, callbacks.exceptions['1']) self.assertEqual({'baz': 'qux'}, callbacks.responses['2']) self.assertEqual(None, callbacks.exceptions['2']) self.assertEqual(1, cred_1._refreshed) self.assertEqual(0, cred_2._refreshed) self.assertEqual(1, cred_1._authorized) self.assertEqual(1, cred_2._authorized) self.assertEqual(1, cred_2._applied) self.assertEqual(2, cred_1._applied) def test_http_errors_passed_to_callback(self): batch = BatchHttpRequest() callbacks = Callbacks() cred_1 = MockCredentials('Foo') cred_2 = MockCredentials('Bar') http = HttpMockSequence([ ({'status': '200', 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, BATCH_RESPONSE_WITH_401), ({'status': '200', 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, BATCH_RESPONSE_WITH_401), ]) creds_http_1 = HttpMockSequence([]) cred_1.authorize(creds_http_1) creds_http_2 = HttpMockSequence([]) cred_2.authorize(creds_http_2) self.request1.http = creds_http_1 self.request2.http = creds_http_2 batch.add(self.request1, callback=callbacks.f) batch.add(self.request2, callback=callbacks.f) batch.execute(http) self.assertEqual(None, callbacks.responses['1']) self.assertEqual(401, callbacks.exceptions['1'].resp.status) self.assertEqual({u'baz': u'qux'}, callbacks.responses['2']) self.assertEqual(None, callbacks.exceptions['2']) def test_execute_global_callback(self): callbacks = Callbacks() batch = BatchHttpRequest(callback=callbacks.f) batch.add(self.request1) batch.add(self.request2) http = HttpMockSequence([ ({'status': '200', 'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'}, BATCH_RESPONSE), ]) batch.execute(http) self.assertEqual({'foo': 42}, callbacks.responses['1']) self.assertEqual({'baz': 'qux'}, callbacks.responses['2']) def test_media_inmemory_upload(self): media = MediaInMemoryUpload('abcdef', 'text/plain', chunksize=10, resumable=True) self.assertEqual('text/plain', media.mimetype()) self.assertEqual(10, media.chunksize()) self.assertTrue(media.resumable()) self.assertEqual('bc', media.getbytes(1, 2)) def test_media_inmemory_upload_json_roundtrip(self): media = MediaInMemoryUpload(os.urandom(64), 'text/plain', chunksize=10, resumable=True) data = media.to_json() newmedia = MediaInMemoryUpload.new_from_json(data) self.assertEqual(media._body, newmedia._body) self.assertEqual(media._chunksize, newmedia._chunksize) self.assertEqual(media._resumable, newmedia._resumable) self.assertEqual(media._mimetype, newmedia._mimetype) if __name__ == '__main__': unittest.main()