blob: cb7a832d43ba6587042132d60cbd646625072340 [file] [log] [blame]
#!/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: <randomness+1>
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: <randomness+2>
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: <randomness+1>
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: <randomness+2>
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: <randomness+1>
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, '<foo>')
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()