1 #!/usr/bin/env python 2 # -*- coding: utf-8 -*- 3 # 4 # Copyright 2014 Google Inc. All Rights Reserved. 5 # 6 # Licensed under the Apache License, Version 2.0 (the "License"); 7 # you may not use this file except in compliance with the License. 8 # You may obtain a copy of the License at 9 # 10 # http://www.apache.org/licenses/LICENSE-2.0 11 # 12 # Unless required by applicable law or agreed to in writing, software 13 # distributed under the License is distributed on an "AS IS" BASIS, 14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 # See the License for the specific language governing permissions and 16 # limitations under the License. 17 18 19 """Discovery document tests 20 21 Unit tests for objects created from discovery documents. 22 """ 23 from __future__ import absolute_import 24 import six 25 26 __author__ = 'jcgregorio (at] google.com (Joe Gregorio)' 27 28 from six import BytesIO, StringIO 29 from six.moves.urllib.parse import urlparse, parse_qs 30 31 import copy 32 import datetime 33 import httplib2 34 import itertools 35 import json 36 import os 37 import pickle 38 import re 39 import sys 40 import unittest2 as unittest 41 42 import mock 43 44 import google.auth.credentials 45 import google_auth_httplib2 46 from googleapiclient.discovery import _fix_up_media_upload 47 from googleapiclient.discovery import _fix_up_method_description 48 from googleapiclient.discovery import _fix_up_parameters 49 from googleapiclient.discovery import _urljoin 50 from googleapiclient.discovery import build 51 from googleapiclient.discovery import build_from_document 52 from googleapiclient.discovery import DISCOVERY_URI 53 from googleapiclient.discovery import key2param 54 from googleapiclient.discovery import MEDIA_BODY_PARAMETER_DEFAULT_VALUE 55 from googleapiclient.discovery import MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE 56 from googleapiclient.discovery import ResourceMethodParameters 57 from googleapiclient.discovery import STACK_QUERY_PARAMETERS 58 from googleapiclient.discovery import STACK_QUERY_PARAMETER_DEFAULT_VALUE 59 from googleapiclient.discovery_cache import DISCOVERY_DOC_MAX_AGE 60 from googleapiclient.discovery_cache.base import Cache 61 from googleapiclient.errors import HttpError 62 from googleapiclient.errors import InvalidJsonError 63 from googleapiclient.errors import MediaUploadSizeError 64 from googleapiclient.errors import ResumableUploadError 65 from googleapiclient.errors import UnacceptableMimeTypeError 66 from googleapiclient.errors import UnknownApiNameOrVersion 67 from googleapiclient.errors import UnknownFileType 68 from googleapiclient.http import build_http 69 from googleapiclient.http import BatchHttpRequest 70 from googleapiclient.http import HttpMock 71 from googleapiclient.http import HttpMockSequence 72 from googleapiclient.http import MediaFileUpload 73 from googleapiclient.http import MediaIoBaseUpload 74 from googleapiclient.http import MediaUpload 75 from googleapiclient.http import MediaUploadProgress 76 from googleapiclient.http import tunnel_patch 77 from googleapiclient.model import JsonModel 78 from googleapiclient.schema import Schemas 79 from oauth2client import GOOGLE_TOKEN_URI 80 from oauth2client.client import OAuth2Credentials, GoogleCredentials 81 82 from googleapiclient import _helpers as util 83 84 import uritemplate 85 86 87 DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') 88 89 90 def assertUrisEqual(testcase, expected, actual): 91 """Test that URIs are the same, up to reordering of query parameters.""" 92 expected = urlparse(expected) 93 actual = urlparse(actual) 94 testcase.assertEqual(expected.scheme, actual.scheme) 95 testcase.assertEqual(expected.netloc, actual.netloc) 96 testcase.assertEqual(expected.path, actual.path) 97 testcase.assertEqual(expected.params, actual.params) 98 testcase.assertEqual(expected.fragment, actual.fragment) 99 expected_query = parse_qs(expected.query) 100 actual_query = parse_qs(actual.query) 101 for name in list(expected_query.keys()): 102 testcase.assertEqual(expected_query[name], actual_query[name]) 103 for name in list(actual_query.keys()): 104 testcase.assertEqual(expected_query[name], actual_query[name]) 105 106 107 def datafile(filename): 108 return os.path.join(DATA_DIR, filename) 109 110 111 class SetupHttplib2(unittest.TestCase): 112 113 def test_retries(self): 114 # Merely loading googleapiclient.discovery should set the RETRIES to 1. 115 self.assertEqual(1, httplib2.RETRIES) 116 117 118 class Utilities(unittest.TestCase): 119 120 def setUp(self): 121 with open(datafile('zoo.json'), 'r') as fh: 122 self.zoo_root_desc = json.loads(fh.read()) 123 self.zoo_get_method_desc = self.zoo_root_desc['methods']['query'] 124 self.zoo_animals_resource = self.zoo_root_desc['resources']['animals'] 125 self.zoo_insert_method_desc = self.zoo_animals_resource['methods']['insert'] 126 self.zoo_schema = Schemas(self.zoo_root_desc) 127 128 def test_key2param(self): 129 self.assertEqual('max_results', key2param('max-results')) 130 self.assertEqual('x007_bond', key2param('007-bond')) 131 132 def _base_fix_up_parameters_test( 133 self, method_desc, http_method, root_desc, schema): 134 self.assertEqual(method_desc['httpMethod'], http_method) 135 136 method_desc_copy = copy.deepcopy(method_desc) 137 self.assertEqual(method_desc, method_desc_copy) 138 139 parameters = _fix_up_parameters(method_desc_copy, root_desc, http_method, 140 schema) 141 142 self.assertNotEqual(method_desc, method_desc_copy) 143 144 for param_name in STACK_QUERY_PARAMETERS: 145 self.assertEqual(STACK_QUERY_PARAMETER_DEFAULT_VALUE, 146 parameters[param_name]) 147 148 for param_name, value in six.iteritems(root_desc.get('parameters', {})): 149 self.assertEqual(value, parameters[param_name]) 150 151 return parameters 152 153 def test_fix_up_parameters_get(self): 154 parameters = self._base_fix_up_parameters_test( 155 self.zoo_get_method_desc, 'GET', self.zoo_root_desc, self.zoo_schema) 156 # Since http_method is 'GET' 157 self.assertFalse('body' in parameters) 158 159 def test_fix_up_parameters_insert(self): 160 parameters = self._base_fix_up_parameters_test( 161 self.zoo_insert_method_desc, 'POST', self.zoo_root_desc, self.zoo_schema) 162 body = { 163 'description': 'The request body.', 164 'type': 'object', 165 'required': True, 166 '$ref': 'Animal', 167 } 168 self.assertEqual(parameters['body'], body) 169 170 def test_fix_up_parameters_check_body(self): 171 dummy_root_desc = {} 172 dummy_schema = { 173 'Request': { 174 'properties': { 175 "description": "Required. Dummy parameter.", 176 "type": "string" 177 } 178 } 179 } 180 no_payload_http_method = 'DELETE' 181 with_payload_http_method = 'PUT' 182 183 invalid_method_desc = {'response': 'Who cares'} 184 valid_method_desc = { 185 'request': { 186 'key1': 'value1', 187 'key2': 'value2', 188 '$ref': 'Request' 189 } 190 } 191 192 parameters = _fix_up_parameters(invalid_method_desc, dummy_root_desc, 193 no_payload_http_method, dummy_schema) 194 self.assertFalse('body' in parameters) 195 196 parameters = _fix_up_parameters(valid_method_desc, dummy_root_desc, 197 no_payload_http_method, dummy_schema) 198 self.assertFalse('body' in parameters) 199 200 parameters = _fix_up_parameters(invalid_method_desc, dummy_root_desc, 201 with_payload_http_method, dummy_schema) 202 self.assertFalse('body' in parameters) 203 204 parameters = _fix_up_parameters(valid_method_desc, dummy_root_desc, 205 with_payload_http_method, dummy_schema) 206 body = { 207 'description': 'The request body.', 208 'type': 'object', 209 'required': True, 210 '$ref': 'Request', 211 'key1': 'value1', 212 'key2': 'value2', 213 } 214 self.assertEqual(parameters['body'], body) 215 216 def test_fix_up_parameters_optional_body(self): 217 # Request with no parameters 218 dummy_schema = {'Request': {'properties': {}}} 219 method_desc = {'request': {'$ref': 'Request'}} 220 221 parameters = _fix_up_parameters(method_desc, {}, 'POST', dummy_schema) 222 self.assertFalse(parameters['body']['required']) 223 224 def _base_fix_up_method_description_test( 225 self, method_desc, initial_parameters, final_parameters, 226 final_accept, final_max_size, final_media_path_url): 227 fake_root_desc = {'rootUrl': 'http://root/', 228 'servicePath': 'fake/'} 229 fake_path_url = 'fake-path/' 230 231 accept, max_size, media_path_url = _fix_up_media_upload( 232 method_desc, fake_root_desc, fake_path_url, initial_parameters) 233 self.assertEqual(accept, final_accept) 234 self.assertEqual(max_size, final_max_size) 235 self.assertEqual(media_path_url, final_media_path_url) 236 self.assertEqual(initial_parameters, final_parameters) 237 238 def test_fix_up_media_upload_no_initial_invalid(self): 239 invalid_method_desc = {'response': 'Who cares'} 240 self._base_fix_up_method_description_test(invalid_method_desc, {}, {}, 241 [], 0, None) 242 243 def test_fix_up_media_upload_no_initial_valid_minimal(self): 244 valid_method_desc = {'mediaUpload': {'accept': []}} 245 final_parameters = {'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE, 246 'media_mime_type': MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE} 247 self._base_fix_up_method_description_test( 248 valid_method_desc, {}, final_parameters, [], 0, 249 'http://root/upload/fake/fake-path/') 250 251 def test_fix_up_media_upload_no_initial_valid_full(self): 252 valid_method_desc = {'mediaUpload': {'accept': ['*/*'], 'maxSize': '10GB'}} 253 final_parameters = {'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE, 254 'media_mime_type': MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE} 255 ten_gb = 10 * 2**30 256 self._base_fix_up_method_description_test( 257 valid_method_desc, {}, final_parameters, ['*/*'], 258 ten_gb, 'http://root/upload/fake/fake-path/') 259 260 def test_fix_up_media_upload_with_initial_invalid(self): 261 invalid_method_desc = {'response': 'Who cares'} 262 initial_parameters = {'body': {}} 263 self._base_fix_up_method_description_test( 264 invalid_method_desc, initial_parameters, 265 initial_parameters, [], 0, None) 266 267 def test_fix_up_media_upload_with_initial_valid_minimal(self): 268 valid_method_desc = {'mediaUpload': {'accept': []}} 269 initial_parameters = {'body': {}} 270 final_parameters = {'body': {'required': False}, 271 'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE, 272 'media_mime_type': MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE} 273 self._base_fix_up_method_description_test( 274 valid_method_desc, initial_parameters, final_parameters, [], 0, 275 'http://root/upload/fake/fake-path/') 276 277 def test_fix_up_media_upload_with_initial_valid_full(self): 278 valid_method_desc = {'mediaUpload': {'accept': ['*/*'], 'maxSize': '10GB'}} 279 initial_parameters = {'body': {}} 280 final_parameters = {'body': {'required': False}, 281 'media_body': MEDIA_BODY_PARAMETER_DEFAULT_VALUE, 282 'media_mime_type': MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE} 283 ten_gb = 10 * 2**30 284 self._base_fix_up_method_description_test( 285 valid_method_desc, initial_parameters, final_parameters, ['*/*'], 286 ten_gb, 'http://root/upload/fake/fake-path/') 287 288 def test_fix_up_method_description_get(self): 289 result = _fix_up_method_description(self.zoo_get_method_desc, 290 self.zoo_root_desc, self.zoo_schema) 291 path_url = 'query' 292 http_method = 'GET' 293 method_id = 'bigquery.query' 294 accept = [] 295 max_size = 0 296 media_path_url = None 297 self.assertEqual(result, (path_url, http_method, method_id, accept, 298 max_size, media_path_url)) 299 300 def test_fix_up_method_description_insert(self): 301 result = _fix_up_method_description(self.zoo_insert_method_desc, 302 self.zoo_root_desc, self.zoo_schema) 303 path_url = 'animals' 304 http_method = 'POST' 305 method_id = 'zoo.animals.insert' 306 accept = ['image/png'] 307 max_size = 1024 308 media_path_url = 'https://www.googleapis.com/upload/zoo/v1/animals' 309 self.assertEqual(result, (path_url, http_method, method_id, accept, 310 max_size, media_path_url)) 311 312 def test_urljoin(self): 313 # We want to exhaustively test various URL combinations. 314 simple_bases = ['https://www.googleapis.com', 'https://www.googleapis.com/'] 315 long_urls = ['foo/v1/bar:custom?alt=json', '/foo/v1/bar:custom?alt=json'] 316 317 long_bases = [ 318 'https://www.googleapis.com/foo/v1', 319 'https://www.googleapis.com/foo/v1/', 320 ] 321 simple_urls = ['bar:custom?alt=json', '/bar:custom?alt=json'] 322 323 final_url = 'https://www.googleapis.com/foo/v1/bar:custom?alt=json' 324 for base, url in itertools.product(simple_bases, long_urls): 325 self.assertEqual(final_url, _urljoin(base, url)) 326 for base, url in itertools.product(long_bases, simple_urls): 327 self.assertEqual(final_url, _urljoin(base, url)) 328 329 330 def test_ResourceMethodParameters_zoo_get(self): 331 parameters = ResourceMethodParameters(self.zoo_get_method_desc) 332 333 param_types = {'a': 'any', 334 'b': 'boolean', 335 'e': 'string', 336 'er': 'string', 337 'i': 'integer', 338 'n': 'number', 339 'o': 'object', 340 'q': 'string', 341 'rr': 'string'} 342 keys = list(param_types.keys()) 343 self.assertEqual(parameters.argmap, dict((key, key) for key in keys)) 344 self.assertEqual(parameters.required_params, []) 345 self.assertEqual(sorted(parameters.repeated_params), ['er', 'rr']) 346 self.assertEqual(parameters.pattern_params, {'rr': '[a-z]+'}) 347 self.assertEqual(sorted(parameters.query_params), 348 ['a', 'b', 'e', 'er', 'i', 'n', 'o', 'q', 'rr']) 349 self.assertEqual(parameters.path_params, set()) 350 self.assertEqual(parameters.param_types, param_types) 351 enum_params = {'e': ['foo', 'bar'], 352 'er': ['one', 'two', 'three']} 353 self.assertEqual(parameters.enum_params, enum_params) 354 355 def test_ResourceMethodParameters_zoo_animals_patch(self): 356 method_desc = self.zoo_animals_resource['methods']['patch'] 357 parameters = ResourceMethodParameters(method_desc) 358 359 param_types = {'name': 'string'} 360 keys = list(param_types.keys()) 361 self.assertEqual(parameters.argmap, dict((key, key) for key in keys)) 362 self.assertEqual(parameters.required_params, ['name']) 363 self.assertEqual(parameters.repeated_params, []) 364 self.assertEqual(parameters.pattern_params, {}) 365 self.assertEqual(parameters.query_params, []) 366 self.assertEqual(parameters.path_params, set(['name'])) 367 self.assertEqual(parameters.param_types, param_types) 368 self.assertEqual(parameters.enum_params, {}) 369 370 371 class DiscoveryErrors(unittest.TestCase): 372 373 def test_tests_should_be_run_with_strict_positional_enforcement(self): 374 try: 375 plus = build('plus', 'v1', None) 376 self.fail("should have raised a TypeError exception over missing http=.") 377 except TypeError: 378 pass 379 380 def test_failed_to_parse_discovery_json(self): 381 self.http = HttpMock(datafile('malformed.json'), {'status': '200'}) 382 try: 383 plus = build('plus', 'v1', http=self.http, cache_discovery=False) 384 self.fail("should have raised an exception over malformed JSON.") 385 except InvalidJsonError: 386 pass 387 388 def test_unknown_api_name_or_version(self): 389 http = HttpMockSequence([ 390 ({'status': '404'}, open(datafile('zoo.json'), 'rb').read()), 391 ({'status': '404'}, open(datafile('zoo.json'), 'rb').read()), 392 ]) 393 with self.assertRaises(UnknownApiNameOrVersion): 394 plus = build('plus', 'v1', http=http, cache_discovery=False) 395 396 def test_credentials_and_http_mutually_exclusive(self): 397 http = HttpMock(datafile('plus.json'), {'status': '200'}) 398 with self.assertRaises(ValueError): 399 build( 400 'plus', 'v1', http=http, credentials=mock.sentinel.credentials) 401 402 403 class DiscoveryFromDocument(unittest.TestCase): 404 MOCK_CREDENTIALS = mock.Mock(spec=google.auth.credentials.Credentials) 405 406 def test_can_build_from_local_document(self): 407 discovery = open(datafile('plus.json')).read() 408 plus = build_from_document( 409 discovery, base="https://www.googleapis.com/", 410 credentials=self.MOCK_CREDENTIALS) 411 self.assertTrue(plus is not None) 412 self.assertTrue(hasattr(plus, 'activities')) 413 414 def test_can_build_from_local_deserialized_document(self): 415 discovery = open(datafile('plus.json')).read() 416 discovery = json.loads(discovery) 417 plus = build_from_document( 418 discovery, base="https://www.googleapis.com/", 419 credentials=self.MOCK_CREDENTIALS) 420 self.assertTrue(plus is not None) 421 self.assertTrue(hasattr(plus, 'activities')) 422 423 def test_building_with_base_remembers_base(self): 424 discovery = open(datafile('plus.json')).read() 425 426 base = "https://www.example.com/" 427 plus = build_from_document( 428 discovery, base=base, credentials=self.MOCK_CREDENTIALS) 429 self.assertEquals("https://www.googleapis.com/plus/v1/", plus._baseUrl) 430 431 def test_building_with_optional_http_with_authorization(self): 432 discovery = open(datafile('plus.json')).read() 433 plus = build_from_document( 434 discovery, base="https://www.googleapis.com/", 435 credentials=self.MOCK_CREDENTIALS) 436 437 # plus service requires Authorization, hence we expect to see AuthorizedHttp object here 438 self.assertIsInstance(plus._http, google_auth_httplib2.AuthorizedHttp) 439 self.assertIsInstance(plus._http.http, httplib2.Http) 440 self.assertIsInstance(plus._http.http.timeout, int) 441 self.assertGreater(plus._http.http.timeout, 0) 442 443 def test_building_with_optional_http_with_no_authorization(self): 444 discovery = open(datafile('plus.json')).read() 445 # Cleanup auth field, so we would use plain http client 446 discovery = json.loads(discovery) 447 discovery['auth'] = {} 448 discovery = json.dumps(discovery) 449 450 plus = build_from_document( 451 discovery, base="https://www.googleapis.com/", 452 credentials=None) 453 # plus service requires Authorization 454 self.assertIsInstance(plus._http, httplib2.Http) 455 self.assertIsInstance(plus._http.timeout, int) 456 self.assertGreater(plus._http.timeout, 0) 457 458 def test_building_with_explicit_http(self): 459 http = HttpMock() 460 discovery = open(datafile('plus.json')).read() 461 plus = build_from_document( 462 discovery, base="https://www.googleapis.com/", http=http) 463 self.assertEquals(plus._http, http) 464 465 def test_building_with_developer_key_skips_adc(self): 466 discovery = open(datafile('plus.json')).read() 467 plus = build_from_document( 468 discovery, base="https://www.googleapis.com/", developerKey='123') 469 self.assertIsInstance(plus._http, httplib2.Http) 470 # It should not be an AuthorizedHttp, because that would indicate that 471 # application default credentials were used. 472 self.assertNotIsInstance(plus._http, google_auth_httplib2.AuthorizedHttp) 473 474 475 class DiscoveryFromHttp(unittest.TestCase): 476 def setUp(self): 477 self.old_environ = os.environ.copy() 478 479 def tearDown(self): 480 os.environ = self.old_environ 481 482 def test_userip_is_added_to_discovery_uri(self): 483 # build() will raise an HttpError on a 400, use this to pick the request uri 484 # out of the raised exception. 485 os.environ['REMOTE_ADDR'] = '10.0.0.1' 486 try: 487 http = HttpMockSequence([ 488 ({'status': '400'}, open(datafile('zoo.json'), 'rb').read()), 489 ]) 490 zoo = build('zoo', 'v1', http=http, developerKey=None, 491 discoveryServiceUrl='http://example.com') 492 self.fail('Should have raised an exception.') 493 except HttpError as e: 494 self.assertEqual(e.uri, 'http://example.com?userIp=10.0.0.1') 495 496 def test_userip_missing_is_not_added_to_discovery_uri(self): 497 # build() will raise an HttpError on a 400, use this to pick the request uri 498 # out of the raised exception. 499 try: 500 http = HttpMockSequence([ 501 ({'status': '400'}, open(datafile('zoo.json'), 'rb').read()), 502 ]) 503 zoo = build('zoo', 'v1', http=http, developerKey=None, 504 discoveryServiceUrl='http://example.com') 505 self.fail('Should have raised an exception.') 506 except HttpError as e: 507 self.assertEqual(e.uri, 'http://example.com') 508 509 def test_key_is_added_to_discovery_uri(self): 510 # build() will raise an HttpError on a 400, use this to pick the request uri 511 # out of the raised exception. 512 try: 513 http = HttpMockSequence([ 514 ({'status': '400'}, open(datafile('zoo.json'), 'rb').read()), 515 ]) 516 zoo = build('zoo', 'v1', http=http, developerKey='foo', 517 discoveryServiceUrl='http://example.com') 518 self.fail('Should have raised an exception.') 519 except HttpError as e: 520 self.assertEqual(e.uri, 'http://example.com?key=foo') 521 522 def test_discovery_loading_from_v2_discovery_uri(self): 523 http = HttpMockSequence([ 524 ({'status': '404'}, 'Not found'), 525 ({'status': '200'}, open(datafile('zoo.json'), 'rb').read()), 526 ]) 527 zoo = build('zoo', 'v1', http=http, cache_discovery=False) 528 self.assertTrue(hasattr(zoo, 'animals')) 529 530 class DiscoveryFromAppEngineCache(unittest.TestCase): 531 def test_appengine_memcache(self): 532 # Hack module import 533 self.orig_import = __import__ 534 self.mocked_api = mock.MagicMock() 535 536 def import_mock(name, *args, **kwargs): 537 if name == 'google.appengine.api': 538 return self.mocked_api 539 return self.orig_import(name, *args, **kwargs) 540 541 import_fullname = '__builtin__.__import__' 542 if sys.version_info[0] >= 3: 543 import_fullname = 'builtins.__import__' 544 545 with mock.patch(import_fullname, side_effect=import_mock): 546 namespace = 'google-api-client' 547 self.http = HttpMock(datafile('plus.json'), {'status': '200'}) 548 549 self.mocked_api.memcache.get.return_value = None 550 551 plus = build('plus', 'v1', http=self.http) 552 553 # memcache.get is called once 554 url = 'https://www.googleapis.com/discovery/v1/apis/plus/v1/rest' 555 self.mocked_api.memcache.get.assert_called_once_with(url, 556 namespace=namespace) 557 558 # memcache.set is called once 559 with open(datafile('plus.json')) as f: 560 content = f.read() 561 self.mocked_api.memcache.set.assert_called_once_with( 562 url, content, time=DISCOVERY_DOC_MAX_AGE, namespace=namespace) 563 564 # Returns the cached content this time. 565 self.mocked_api.memcache.get.return_value = content 566 567 # Make sure the contents are returned from the cache. 568 # (Otherwise it should through an error) 569 self.http = HttpMock(None, {'status': '200'}) 570 571 plus = build('plus', 'v1', http=self.http) 572 573 # memcache.get is called twice 574 self.mocked_api.memcache.get.assert_has_calls( 575 [mock.call(url, namespace=namespace), 576 mock.call(url, namespace=namespace)]) 577 578 # memcahce.set is called just once 579 self.mocked_api.memcache.set.assert_called_once_with( 580 url, content, time=DISCOVERY_DOC_MAX_AGE,namespace=namespace) 581 582 583 class DictCache(Cache): 584 def __init__(self): 585 self.d = {} 586 def get(self, url): 587 return self.d.get(url, None) 588 def set(self, url, content): 589 self.d[url] = content 590 def contains(self, url): 591 return url in self.d 592 593 594 class DiscoveryFromFileCache(unittest.TestCase): 595 def test_file_based_cache(self): 596 cache = mock.Mock(wraps=DictCache()) 597 with mock.patch('googleapiclient.discovery_cache.autodetect', 598 return_value=cache): 599 self.http = HttpMock(datafile('plus.json'), {'status': '200'}) 600 601 plus = build('plus', 'v1', http=self.http) 602 603 # cache.get is called once 604 url = 'https://www.googleapis.com/discovery/v1/apis/plus/v1/rest' 605 cache.get.assert_called_once_with(url) 606 607 # cache.set is called once 608 with open(datafile('plus.json')) as f: 609 content = f.read() 610 cache.set.assert_called_once_with(url, content) 611 612 # Make sure there is a cache entry for the plus v1 discovery doc. 613 self.assertTrue(cache.contains(url)) 614 615 # Make sure the contents are returned from the cache. 616 # (Otherwise it should through an error) 617 self.http = HttpMock(None, {'status': '200'}) 618 619 plus = build('plus', 'v1', http=self.http) 620 621 # cache.get is called twice 622 cache.get.assert_has_calls([mock.call(url), mock.call(url)]) 623 624 # cahce.set is called just once 625 cache.set.assert_called_once_with(url, content) 626 627 628 class Discovery(unittest.TestCase): 629 630 def test_method_error_checking(self): 631 self.http = HttpMock(datafile('plus.json'), {'status': '200'}) 632 plus = build('plus', 'v1', http=self.http) 633 634 # Missing required parameters 635 try: 636 plus.activities().list() 637 self.fail() 638 except TypeError as e: 639 self.assertTrue('Missing' in str(e)) 640 641 # Missing required parameters even if supplied as None. 642 try: 643 plus.activities().list(collection=None, userId=None) 644 self.fail() 645 except TypeError as e: 646 self.assertTrue('Missing' in str(e)) 647 648 # Parameter doesn't match regex 649 try: 650 plus.activities().list(collection='not_a_collection_name', userId='me') 651 self.fail() 652 except TypeError as e: 653 self.assertTrue('not an allowed value' in str(e)) 654 655 # Unexpected parameter 656 try: 657 plus.activities().list(flubber=12) 658 self.fail() 659 except TypeError as e: 660 self.assertTrue('unexpected' in str(e)) 661 662 def _check_query_types(self, request): 663 parsed = urlparse(request.uri) 664 q = parse_qs(parsed[4]) 665 self.assertEqual(q['q'], ['foo']) 666 self.assertEqual(q['i'], ['1']) 667 self.assertEqual(q['n'], ['1.0']) 668 self.assertEqual(q['b'], ['false']) 669 self.assertEqual(q['a'], ['[1, 2, 3]']) 670 self.assertEqual(q['o'], ['{\'a\': 1}']) 671 self.assertEqual(q['e'], ['bar']) 672 673 def test_type_coercion(self): 674 http = HttpMock(datafile('zoo.json'), {'status': '200'}) 675 zoo = build('zoo', 'v1', http=http) 676 677 request = zoo.query( 678 q="foo", i=1.0, n=1.0, b=0, a=[1,2,3], o={'a':1}, e='bar') 679 self._check_query_types(request) 680 request = zoo.query( 681 q="foo", i=1, n=1, b=False, a=[1,2,3], o={'a':1}, e='bar') 682 self._check_query_types(request) 683 684 request = zoo.query( 685 q="foo", i="1", n="1", b="", a=[1,2,3], o={'a':1}, e='bar', er='two') 686 687 request = zoo.query( 688 q="foo", i="1", n="1", b="", a=[1,2,3], o={'a':1}, e='bar', 689 er=['one', 'three'], rr=['foo', 'bar']) 690 self._check_query_types(request) 691 692 # Five is right out. 693 self.assertRaises(TypeError, zoo.query, er=['one', 'five']) 694 695 def test_optional_stack_query_parameters(self): 696 http = HttpMock(datafile('zoo.json'), {'status': '200'}) 697 zoo = build('zoo', 'v1', http=http) 698 request = zoo.query(trace='html', fields='description') 699 700 parsed = urlparse(request.uri) 701 q = parse_qs(parsed[4]) 702 self.assertEqual(q['trace'], ['html']) 703 self.assertEqual(q['fields'], ['description']) 704 705 def test_string_params_value_of_none_get_dropped(self): 706 http = HttpMock(datafile('zoo.json'), {'status': '200'}) 707 zoo = build('zoo', 'v1', http=http) 708 request = zoo.query(trace=None, fields='description') 709 710 parsed = urlparse(request.uri) 711 q = parse_qs(parsed[4]) 712 self.assertFalse('trace' in q) 713 714 def test_model_added_query_parameters(self): 715 http = HttpMock(datafile('zoo.json'), {'status': '200'}) 716 zoo = build('zoo', 'v1', http=http) 717 request = zoo.animals().get(name='Lion') 718 719 parsed = urlparse(request.uri) 720 q = parse_qs(parsed[4]) 721 self.assertEqual(q['alt'], ['json']) 722 self.assertEqual(request.headers['accept'], 'application/json') 723 724 def test_fallback_to_raw_model(self): 725 http = HttpMock(datafile('zoo.json'), {'status': '200'}) 726 zoo = build('zoo', 'v1', http=http) 727 request = zoo.animals().getmedia(name='Lion') 728 729 parsed = urlparse(request.uri) 730 q = parse_qs(parsed[4]) 731 self.assertTrue('alt' not in q) 732 self.assertEqual(request.headers['accept'], '*/*') 733 734 def test_patch(self): 735 http = HttpMock(datafile('zoo.json'), {'status': '200'}) 736 zoo = build('zoo', 'v1', http=http) 737 request = zoo.animals().patch(name='lion', body='{"description": "foo"}') 738 739 self.assertEqual(request.method, 'PATCH') 740 741 def test_batch_request_from_discovery(self): 742 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 743 # zoo defines a batchPath 744 zoo = build('zoo', 'v1', http=self.http) 745 batch_request = zoo.new_batch_http_request() 746 self.assertEqual(batch_request._batch_uri, 747 "https://www.googleapis.com/batchZoo") 748 749 def test_batch_request_from_default(self): 750 self.http = HttpMock(datafile('plus.json'), {'status': '200'}) 751 # plus does not define a batchPath 752 plus = build('plus', 'v1', http=self.http) 753 batch_request = plus.new_batch_http_request() 754 self.assertEqual(batch_request._batch_uri, 755 "https://www.googleapis.com/batch") 756 757 def test_tunnel_patch(self): 758 http = HttpMockSequence([ 759 ({'status': '200'}, open(datafile('zoo.json'), 'rb').read()), 760 ({'status': '200'}, 'echo_request_headers_as_json'), 761 ]) 762 http = tunnel_patch(http) 763 zoo = build('zoo', 'v1', http=http, cache_discovery=False) 764 resp = zoo.animals().patch( 765 name='lion', body='{"description": "foo"}').execute() 766 767 self.assertTrue('x-http-method-override' in resp) 768 769 def test_plus_resources(self): 770 self.http = HttpMock(datafile('plus.json'), {'status': '200'}) 771 plus = build('plus', 'v1', http=self.http) 772 self.assertTrue(getattr(plus, 'activities')) 773 self.assertTrue(getattr(plus, 'people')) 774 775 def test_oauth2client_credentials(self): 776 credentials = mock.Mock(spec=GoogleCredentials) 777 credentials.create_scoped_required.return_value = False 778 779 discovery = open(datafile('plus.json')).read() 780 service = build_from_document(discovery, credentials=credentials) 781 self.assertEqual(service._http, credentials.authorize.return_value) 782 783 def test_google_auth_credentials(self): 784 credentials = mock.Mock(spec=google.auth.credentials.Credentials) 785 discovery = open(datafile('plus.json')).read() 786 service = build_from_document(discovery, credentials=credentials) 787 788 self.assertIsInstance(service._http, google_auth_httplib2.AuthorizedHttp) 789 self.assertEqual(service._http.credentials, credentials) 790 791 def test_no_scopes_no_credentials(self): 792 # Zoo doesn't have scopes 793 discovery = open(datafile('zoo.json')).read() 794 service = build_from_document(discovery) 795 # Should be an ordinary httplib2.Http instance and not AuthorizedHttp. 796 self.assertIsInstance(service._http, httplib2.Http) 797 798 def test_full_featured(self): 799 # Zoo should exercise all discovery facets 800 # and should also have no future.json file. 801 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 802 zoo = build('zoo', 'v1', http=self.http) 803 self.assertTrue(getattr(zoo, 'animals')) 804 805 request = zoo.animals().list(name='bat', projection="full") 806 parsed = urlparse(request.uri) 807 q = parse_qs(parsed[4]) 808 self.assertEqual(q['name'], ['bat']) 809 self.assertEqual(q['projection'], ['full']) 810 811 def test_nested_resources(self): 812 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 813 zoo = build('zoo', 'v1', http=self.http) 814 self.assertTrue(getattr(zoo, 'animals')) 815 request = zoo.my().favorites().list(max_results="5") 816 parsed = urlparse(request.uri) 817 q = parse_qs(parsed[4]) 818 self.assertEqual(q['max-results'], ['5']) 819 820 @unittest.skipIf(six.PY3, 'print is not a reserved name in Python 3') 821 def test_methods_with_reserved_names(self): 822 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 823 zoo = build('zoo', 'v1', http=self.http) 824 self.assertTrue(getattr(zoo, 'animals')) 825 request = zoo.global_().print_().assert_(max_results="5") 826 parsed = urlparse(request.uri) 827 self.assertEqual(parsed[2], '/zoo/v1/global/print/assert') 828 829 def test_top_level_functions(self): 830 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 831 zoo = build('zoo', 'v1', http=self.http) 832 self.assertTrue(getattr(zoo, 'query')) 833 request = zoo.query(q="foo") 834 parsed = urlparse(request.uri) 835 q = parse_qs(parsed[4]) 836 self.assertEqual(q['q'], ['foo']) 837 838 def test_simple_media_uploads(self): 839 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 840 zoo = build('zoo', 'v1', http=self.http) 841 doc = getattr(zoo.animals().insert, '__doc__') 842 self.assertTrue('media_body' in doc) 843 844 def test_simple_media_upload_no_max_size_provided(self): 845 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 846 zoo = build('zoo', 'v1', http=self.http) 847 request = zoo.animals().crossbreed(media_body=datafile('small.png')) 848 self.assertEquals('image/png', request.headers['content-type']) 849 self.assertEquals(b'PNG', request.body[1:4]) 850 851 def test_simple_media_raise_correct_exceptions(self): 852 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 853 zoo = build('zoo', 'v1', http=self.http) 854 855 try: 856 zoo.animals().insert(media_body=datafile('smiley.png')) 857 self.fail("should throw exception if media is too large.") 858 except MediaUploadSizeError: 859 pass 860 861 try: 862 zoo.animals().insert(media_body=datafile('small.jpg')) 863 self.fail("should throw exception if mimetype is unacceptable.") 864 except UnacceptableMimeTypeError: 865 pass 866 867 def test_simple_media_good_upload(self): 868 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 869 zoo = build('zoo', 'v1', http=self.http) 870 871 request = zoo.animals().insert(media_body=datafile('small.png')) 872 self.assertEquals('image/png', request.headers['content-type']) 873 self.assertEquals(b'PNG', request.body[1:4]) 874 assertUrisEqual(self, 875 'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json', 876 request.uri) 877 878 def test_simple_media_unknown_mimetype(self): 879 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 880 zoo = build('zoo', 'v1', http=self.http) 881 882 try: 883 zoo.animals().insert(media_body=datafile('small-png')) 884 self.fail("should throw exception if mimetype is unknown.") 885 except UnknownFileType: 886 pass 887 888 request = zoo.animals().insert(media_body=datafile('small-png'), 889 media_mime_type='image/png') 890 self.assertEquals('image/png', request.headers['content-type']) 891 self.assertEquals(b'PNG', request.body[1:4]) 892 assertUrisEqual(self, 893 'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json', 894 request.uri) 895 896 def test_multipart_media_raise_correct_exceptions(self): 897 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 898 zoo = build('zoo', 'v1', http=self.http) 899 900 try: 901 zoo.animals().insert(media_body=datafile('smiley.png'), body={}) 902 self.fail("should throw exception if media is too large.") 903 except MediaUploadSizeError: 904 pass 905 906 try: 907 zoo.animals().insert(media_body=datafile('small.jpg'), body={}) 908 self.fail("should throw exception if mimetype is unacceptable.") 909 except UnacceptableMimeTypeError: 910 pass 911 912 def test_multipart_media_good_upload(self): 913 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 914 zoo = build('zoo', 'v1', http=self.http) 915 916 request = zoo.animals().insert(media_body=datafile('small.png'), body={}) 917 self.assertTrue(request.headers['content-type'].startswith( 918 'multipart/related')) 919 with open(datafile('small.png'), 'rb') as f: 920 contents = f.read() 921 boundary = re.match(b'--=+([^=]+)', request.body).group(1) 922 self.assertEqual( 923 request.body.rstrip(b"\n"), # Python 2.6 does not add a trailing \n 924 b'--===============' + boundary + b'==\n' + 925 b'Content-Type: application/json\n' + 926 b'MIME-Version: 1.0\n\n' + 927 b'{"data": {}}\n' + 928 b'--===============' + boundary + b'==\n' + 929 b'Content-Type: image/png\n' + 930 b'MIME-Version: 1.0\n' + 931 b'Content-Transfer-Encoding: binary\n\n' + 932 contents + 933 b'\n--===============' + boundary + b'==--') 934 assertUrisEqual(self, 935 'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=multipart&alt=json', 936 request.uri) 937 938 def test_media_capable_method_without_media(self): 939 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 940 zoo = build('zoo', 'v1', http=self.http) 941 942 request = zoo.animals().insert(body={}) 943 self.assertTrue(request.headers['content-type'], 'application/json') 944 945 def test_resumable_multipart_media_good_upload(self): 946 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 947 zoo = build('zoo', 'v1', http=self.http) 948 949 media_upload = MediaFileUpload(datafile('small.png'), resumable=True) 950 request = zoo.animals().insert(media_body=media_upload, body={}) 951 self.assertTrue(request.headers['content-type'].startswith( 952 'application/json')) 953 self.assertEquals('{"data": {}}', request.body) 954 self.assertEquals(media_upload, request.resumable) 955 956 self.assertEquals('image/png', request.resumable.mimetype()) 957 958 self.assertNotEquals(request.body, None) 959 self.assertEquals(request.resumable_uri, None) 960 961 http = HttpMockSequence([ 962 ({'status': '200', 963 'location': 'http://upload.example.com'}, ''), 964 ({'status': '308', 965 'location': 'http://upload.example.com/2'}, ''), 966 ({'status': '308', 967 'location': 'http://upload.example.com/3', 968 'range': '0-12'}, ''), 969 ({'status': '308', 970 'location': 'http://upload.example.com/4', 971 'range': '0-%d' % (media_upload.size() - 2)}, ''), 972 ({'status': '200'}, '{"foo": "bar"}'), 973 ]) 974 975 status, body = request.next_chunk(http=http) 976 self.assertEquals(None, body) 977 self.assertTrue(isinstance(status, MediaUploadProgress)) 978 self.assertEquals(0, status.resumable_progress) 979 980 # Two requests should have been made and the resumable_uri should have been 981 # updated for each one. 982 self.assertEquals(request.resumable_uri, 'http://upload.example.com/2') 983 self.assertEquals(media_upload, request.resumable) 984 self.assertEquals(0, request.resumable_progress) 985 986 # This next chuck call should upload the first chunk 987 status, body = request.next_chunk(http=http) 988 self.assertEquals(request.resumable_uri, 'http://upload.example.com/3') 989 self.assertEquals(media_upload, request.resumable) 990 self.assertEquals(13, request.resumable_progress) 991 992 # This call will upload the next chunk 993 status, body = request.next_chunk(http=http) 994 self.assertEquals(request.resumable_uri, 'http://upload.example.com/4') 995 self.assertEquals(media_upload.size()-1, request.resumable_progress) 996 self.assertEquals('{"data": {}}', request.body) 997 998 # Final call to next_chunk should complete the upload. 999 status, body = request.next_chunk(http=http) 1000 self.assertEquals(body, {"foo": "bar"}) 1001 self.assertEquals(status, None) 1002 1003 1004 def test_resumable_media_good_upload(self): 1005 """Not a multipart upload.""" 1006 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1007 zoo = build('zoo', 'v1', http=self.http) 1008 1009 media_upload = MediaFileUpload(datafile('small.png'), resumable=True) 1010 request = zoo.animals().insert(media_body=media_upload, body=None) 1011 self.assertEquals(media_upload, request.resumable) 1012 1013 self.assertEquals('image/png', request.resumable.mimetype()) 1014 1015 self.assertEquals(request.body, None) 1016 self.assertEquals(request.resumable_uri, None) 1017 1018 http = HttpMockSequence([ 1019 ({'status': '200', 1020 'location': 'http://upload.example.com'}, ''), 1021 ({'status': '308', 1022 'location': 'http://upload.example.com/2', 1023 'range': '0-12'}, ''), 1024 ({'status': '308', 1025 'location': 'http://upload.example.com/3', 1026 'range': '0-%d' % (media_upload.size() - 2)}, ''), 1027 ({'status': '200'}, '{"foo": "bar"}'), 1028 ]) 1029 1030 status, body = request.next_chunk(http=http) 1031 self.assertEquals(None, body) 1032 self.assertTrue(isinstance(status, MediaUploadProgress)) 1033 self.assertEquals(13, status.resumable_progress) 1034 1035 # Two requests should have been made and the resumable_uri should have been 1036 # updated for each one. 1037 self.assertEquals(request.resumable_uri, 'http://upload.example.com/2') 1038 1039 self.assertEquals(media_upload, request.resumable) 1040 self.assertEquals(13, request.resumable_progress) 1041 1042 status, body = request.next_chunk(http=http) 1043 self.assertEquals(request.resumable_uri, 'http://upload.example.com/3') 1044 self.assertEquals(media_upload.size()-1, request.resumable_progress) 1045 self.assertEquals(request.body, None) 1046 1047 # Final call to next_chunk should complete the upload. 1048 status, body = request.next_chunk(http=http) 1049 self.assertEquals(body, {"foo": "bar"}) 1050 self.assertEquals(status, None) 1051 1052 def test_resumable_media_good_upload_from_execute(self): 1053 """Not a multipart upload.""" 1054 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1055 zoo = build('zoo', 'v1', http=self.http) 1056 1057 media_upload = MediaFileUpload(datafile('small.png'), resumable=True) 1058 request = zoo.animals().insert(media_body=media_upload, body=None) 1059 assertUrisEqual(self, 1060 'https://www.googleapis.com/upload/zoo/v1/animals?uploadType=resumable&alt=json', 1061 request.uri) 1062 1063 http = HttpMockSequence([ 1064 ({'status': '200', 1065 'location': 'http://upload.example.com'}, ''), 1066 ({'status': '308', 1067 'location': 'http://upload.example.com/2', 1068 'range': '0-12'}, ''), 1069 ({'status': '308', 1070 'location': 'http://upload.example.com/3', 1071 'range': '0-%d' % media_upload.size()}, ''), 1072 ({'status': '200'}, '{"foo": "bar"}'), 1073 ]) 1074 1075 body = request.execute(http=http) 1076 self.assertEquals(body, {"foo": "bar"}) 1077 1078 def test_resumable_media_fail_unknown_response_code_first_request(self): 1079 """Not a multipart upload.""" 1080 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1081 zoo = build('zoo', 'v1', http=self.http) 1082 1083 media_upload = MediaFileUpload(datafile('small.png'), resumable=True) 1084 request = zoo.animals().insert(media_body=media_upload, body=None) 1085 1086 http = HttpMockSequence([ 1087 ({'status': '400', 1088 'location': 'http://upload.example.com'}, ''), 1089 ]) 1090 1091 try: 1092 request.execute(http=http) 1093 self.fail('Should have raised ResumableUploadError.') 1094 except ResumableUploadError as e: 1095 self.assertEqual(400, e.resp.status) 1096 1097 def test_resumable_media_fail_unknown_response_code_subsequent_request(self): 1098 """Not a multipart upload.""" 1099 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1100 zoo = build('zoo', 'v1', http=self.http) 1101 1102 media_upload = MediaFileUpload(datafile('small.png'), resumable=True) 1103 request = zoo.animals().insert(media_body=media_upload, body=None) 1104 1105 http = HttpMockSequence([ 1106 ({'status': '200', 1107 'location': 'http://upload.example.com'}, ''), 1108 ({'status': '400'}, ''), 1109 ]) 1110 1111 self.assertRaises(HttpError, request.execute, http=http) 1112 self.assertTrue(request._in_error_state) 1113 1114 http = HttpMockSequence([ 1115 ({'status': '308', 1116 'range': '0-5'}, ''), 1117 ({'status': '308', 1118 'range': '0-6'}, ''), 1119 ]) 1120 1121 status, body = request.next_chunk(http=http) 1122 self.assertEquals(status.resumable_progress, 7, 1123 'Should have first checked length and then tried to PUT more.') 1124 self.assertFalse(request._in_error_state) 1125 1126 # Put it back in an error state. 1127 http = HttpMockSequence([ 1128 ({'status': '400'}, ''), 1129 ]) 1130 self.assertRaises(HttpError, request.execute, http=http) 1131 self.assertTrue(request._in_error_state) 1132 1133 # Pretend the last request that 400'd actually succeeded. 1134 http = HttpMockSequence([ 1135 ({'status': '200'}, '{"foo": "bar"}'), 1136 ]) 1137 status, body = request.next_chunk(http=http) 1138 self.assertEqual(body, {'foo': 'bar'}) 1139 1140 def test_media_io_base_stream_unlimited_chunksize_resume(self): 1141 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1142 zoo = build('zoo', 'v1', http=self.http) 1143 1144 # Set up a seekable stream and try to upload in single chunk. 1145 fd = BytesIO(b'01234"56789"') 1146 media_upload = MediaIoBaseUpload( 1147 fd=fd, mimetype='text/plain', chunksize=-1, resumable=True) 1148 1149 request = zoo.animals().insert(media_body=media_upload, body=None) 1150 1151 # The single chunk fails, restart at the right point. 1152 http = HttpMockSequence([ 1153 ({'status': '200', 1154 'location': 'http://upload.example.com'}, ''), 1155 ({'status': '308', 1156 'location': 'http://upload.example.com/2', 1157 'range': '0-4'}, ''), 1158 ({'status': '200'}, 'echo_request_body'), 1159 ]) 1160 1161 body = request.execute(http=http) 1162 self.assertEqual('56789', body) 1163 1164 def test_media_io_base_stream_chunksize_resume(self): 1165 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1166 zoo = build('zoo', 'v1', http=self.http) 1167 1168 # Set up a seekable stream and try to upload in chunks. 1169 fd = BytesIO(b'0123456789') 1170 media_upload = MediaIoBaseUpload( 1171 fd=fd, mimetype='text/plain', chunksize=5, resumable=True) 1172 1173 request = zoo.animals().insert(media_body=media_upload, body=None) 1174 1175 # The single chunk fails, pull the content sent out of the exception. 1176 http = HttpMockSequence([ 1177 ({'status': '200', 1178 'location': 'http://upload.example.com'}, ''), 1179 ({'status': '400'}, 'echo_request_body'), 1180 ]) 1181 1182 try: 1183 body = request.execute(http=http) 1184 except HttpError as e: 1185 self.assertEqual(b'01234', e.content) 1186 1187 def test_resumable_media_handle_uploads_of_unknown_size(self): 1188 http = HttpMockSequence([ 1189 ({'status': '200', 1190 'location': 'http://upload.example.com'}, ''), 1191 ({'status': '200'}, 'echo_request_headers_as_json'), 1192 ]) 1193 1194 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1195 zoo = build('zoo', 'v1', http=self.http) 1196 1197 # Create an upload that doesn't know the full size of the media. 1198 class IoBaseUnknownLength(MediaUpload): 1199 def chunksize(self): 1200 return 10 1201 1202 def mimetype(self): 1203 return 'image/png' 1204 1205 def size(self): 1206 return None 1207 1208 def resumable(self): 1209 return True 1210 1211 def getbytes(self, begin, length): 1212 return '0123456789' 1213 1214 upload = IoBaseUnknownLength() 1215 1216 request = zoo.animals().insert(media_body=upload, body=None) 1217 status, body = request.next_chunk(http=http) 1218 self.assertEqual(body, { 1219 'Content-Range': 'bytes 0-9/*', 1220 'Content-Length': '10', 1221 }) 1222 1223 def test_resumable_media_no_streaming_on_unsupported_platforms(self): 1224 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1225 zoo = build('zoo', 'v1', http=self.http) 1226 1227 class IoBaseHasStream(MediaUpload): 1228 def chunksize(self): 1229 return 10 1230 1231 def mimetype(self): 1232 return 'image/png' 1233 1234 def size(self): 1235 return None 1236 1237 def resumable(self): 1238 return True 1239 1240 def getbytes(self, begin, length): 1241 return '0123456789' 1242 1243 def has_stream(self): 1244 return True 1245 1246 def stream(self): 1247 raise NotImplementedError() 1248 1249 upload = IoBaseHasStream() 1250 1251 orig_version = sys.version_info 1252 1253 sys.version_info = (2, 6, 5, 'final', 0) 1254 1255 request = zoo.animals().insert(media_body=upload, body=None) 1256 1257 # This should raise an exception because stream() will be called. 1258 http = HttpMockSequence([ 1259 ({'status': '200', 1260 'location': 'http://upload.example.com'}, ''), 1261 ({'status': '200'}, 'echo_request_headers_as_json'), 1262 ]) 1263 1264 self.assertRaises(NotImplementedError, request.next_chunk, http=http) 1265 1266 sys.version_info = orig_version 1267 1268 def test_resumable_media_handle_uploads_of_unknown_size_eof(self): 1269 http = HttpMockSequence([ 1270 ({'status': '200', 1271 'location': 'http://upload.example.com'}, ''), 1272 ({'status': '200'}, 'echo_request_headers_as_json'), 1273 ]) 1274 1275 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1276 zoo = build('zoo', 'v1', http=self.http) 1277 1278 fd = BytesIO(b'data goes here') 1279 1280 # Create an upload that doesn't know the full size of the media. 1281 upload = MediaIoBaseUpload( 1282 fd=fd, mimetype='image/png', chunksize=15, resumable=True) 1283 1284 request = zoo.animals().insert(media_body=upload, body=None) 1285 status, body = request.next_chunk(http=http) 1286 self.assertEqual(body, { 1287 'Content-Range': 'bytes 0-13/14', 1288 'Content-Length': '14', 1289 }) 1290 1291 def test_resumable_media_handle_resume_of_upload_of_unknown_size(self): 1292 http = HttpMockSequence([ 1293 ({'status': '200', 1294 'location': 'http://upload.example.com'}, ''), 1295 ({'status': '400'}, ''), 1296 ]) 1297 1298 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1299 zoo = build('zoo', 'v1', http=self.http) 1300 1301 # Create an upload that doesn't know the full size of the media. 1302 fd = BytesIO(b'data goes here') 1303 1304 upload = MediaIoBaseUpload( 1305 fd=fd, mimetype='image/png', chunksize=500, resumable=True) 1306 1307 request = zoo.animals().insert(media_body=upload, body=None) 1308 1309 # Put it in an error state. 1310 self.assertRaises(HttpError, request.next_chunk, http=http) 1311 1312 http = HttpMockSequence([ 1313 ({'status': '400', 1314 'range': '0-5'}, 'echo_request_headers_as_json'), 1315 ]) 1316 try: 1317 # Should resume the upload by first querying the status of the upload. 1318 request.next_chunk(http=http) 1319 except HttpError as e: 1320 expected = { 1321 'Content-Range': 'bytes */14', 1322 'content-length': '0' 1323 } 1324 self.assertEqual(expected, json.loads(e.content.decode('utf-8')), 1325 'Should send an empty body when requesting the current upload status.') 1326 1327 def test_pickle(self): 1328 sorted_resource_keys = ['_baseUrl', 1329 '_developerKey', 1330 '_dynamic_attrs', 1331 '_http', 1332 '_model', 1333 '_requestBuilder', 1334 '_resourceDesc', 1335 '_rootDesc', 1336 '_schema', 1337 'animals', 1338 'global_', 1339 'load', 1340 'loadNoTemplate', 1341 'my', 1342 'new_batch_http_request', 1343 'query', 1344 'scopedAnimals'] 1345 1346 http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1347 zoo = build('zoo', 'v1', http=http) 1348 self.assertEqual(sorted(zoo.__dict__.keys()), sorted_resource_keys) 1349 1350 pickled_zoo = pickle.dumps(zoo) 1351 new_zoo = pickle.loads(pickled_zoo) 1352 self.assertEqual(sorted(new_zoo.__dict__.keys()), sorted_resource_keys) 1353 self.assertTrue(hasattr(new_zoo, 'animals')) 1354 self.assertTrue(callable(new_zoo.animals)) 1355 self.assertTrue(hasattr(new_zoo, 'global_')) 1356 self.assertTrue(callable(new_zoo.global_)) 1357 self.assertTrue(hasattr(new_zoo, 'load')) 1358 self.assertTrue(callable(new_zoo.load)) 1359 self.assertTrue(hasattr(new_zoo, 'loadNoTemplate')) 1360 self.assertTrue(callable(new_zoo.loadNoTemplate)) 1361 self.assertTrue(hasattr(new_zoo, 'my')) 1362 self.assertTrue(callable(new_zoo.my)) 1363 self.assertTrue(hasattr(new_zoo, 'query')) 1364 self.assertTrue(callable(new_zoo.query)) 1365 self.assertTrue(hasattr(new_zoo, 'scopedAnimals')) 1366 self.assertTrue(callable(new_zoo.scopedAnimals)) 1367 1368 self.assertEqual(sorted(zoo._dynamic_attrs), sorted(new_zoo._dynamic_attrs)) 1369 self.assertEqual(zoo._baseUrl, new_zoo._baseUrl) 1370 self.assertEqual(zoo._developerKey, new_zoo._developerKey) 1371 self.assertEqual(zoo._requestBuilder, new_zoo._requestBuilder) 1372 self.assertEqual(zoo._resourceDesc, new_zoo._resourceDesc) 1373 self.assertEqual(zoo._rootDesc, new_zoo._rootDesc) 1374 # _http, _model and _schema won't be equal since we will get new 1375 # instances upon un-pickling 1376 1377 def _dummy_zoo_request(self): 1378 with open(os.path.join(DATA_DIR, 'zoo.json'), 'rU') as fh: 1379 zoo_contents = fh.read() 1380 1381 zoo_uri = uritemplate.expand(DISCOVERY_URI, 1382 {'api': 'zoo', 'apiVersion': 'v1'}) 1383 if 'REMOTE_ADDR' in os.environ: 1384 zoo_uri = util._add_query_parameter(zoo_uri, 'userIp', 1385 os.environ['REMOTE_ADDR']) 1386 1387 http = build_http() 1388 original_request = http.request 1389 def wrapped_request(uri, method='GET', *args, **kwargs): 1390 if uri == zoo_uri: 1391 return httplib2.Response({'status': '200'}), zoo_contents 1392 return original_request(uri, method=method, *args, **kwargs) 1393 http.request = wrapped_request 1394 return http 1395 1396 def _dummy_token(self): 1397 access_token = 'foo' 1398 client_id = 'some_client_id' 1399 client_secret = 'cOuDdkfjxxnv+' 1400 refresh_token = '1/0/a.df219fjls0' 1401 token_expiry = datetime.datetime.utcnow() 1402 user_agent = 'refresh_checker/1.0' 1403 return OAuth2Credentials( 1404 access_token, client_id, client_secret, 1405 refresh_token, token_expiry, GOOGLE_TOKEN_URI, 1406 user_agent) 1407 1408 def test_pickle_with_credentials(self): 1409 credentials = self._dummy_token() 1410 http = self._dummy_zoo_request() 1411 http = credentials.authorize(http) 1412 self.assertTrue(hasattr(http.request, 'credentials')) 1413 1414 zoo = build('zoo', 'v1', http=http) 1415 pickled_zoo = pickle.dumps(zoo) 1416 new_zoo = pickle.loads(pickled_zoo) 1417 self.assertEqual(sorted(zoo.__dict__.keys()), 1418 sorted(new_zoo.__dict__.keys())) 1419 new_http = new_zoo._http 1420 self.assertFalse(hasattr(new_http.request, 'credentials')) 1421 1422 def test_resumable_media_upload_no_content(self): 1423 self.http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1424 zoo = build('zoo', 'v1', http=self.http) 1425 1426 media_upload = MediaFileUpload(datafile('empty'), resumable=True) 1427 request = zoo.animals().insert(media_body=media_upload, body=None) 1428 1429 self.assertEquals(media_upload, request.resumable) 1430 self.assertEquals(request.body, None) 1431 self.assertEquals(request.resumable_uri, None) 1432 1433 http = HttpMockSequence([ 1434 ({'status': '200', 1435 'location': 'http://upload.example.com'}, ''), 1436 ({'status': '308', 1437 'location': 'http://upload.example.com/2', 1438 'range': '0-0'}, ''), 1439 ]) 1440 1441 status, body = request.next_chunk(http=http) 1442 self.assertEquals(None, body) 1443 self.assertTrue(isinstance(status, MediaUploadProgress)) 1444 self.assertEquals(0, status.progress()) 1445 1446 1447 class Next(unittest.TestCase): 1448 1449 def test_next_successful_none_on_no_next_page_token(self): 1450 self.http = HttpMock(datafile('tasks.json'), {'status': '200'}) 1451 tasks = build('tasks', 'v1', http=self.http) 1452 request = tasks.tasklists().list() 1453 self.assertEqual(None, tasks.tasklists().list_next(request, {})) 1454 1455 def test_next_successful_none_on_empty_page_token(self): 1456 self.http = HttpMock(datafile('tasks.json'), {'status': '200'}) 1457 tasks = build('tasks', 'v1', http=self.http) 1458 request = tasks.tasklists().list() 1459 next_request = tasks.tasklists().list_next( 1460 request, {'nextPageToken': ''}) 1461 self.assertEqual(None, next_request) 1462 1463 def test_next_successful_with_next_page_token(self): 1464 self.http = HttpMock(datafile('tasks.json'), {'status': '200'}) 1465 tasks = build('tasks', 'v1', http=self.http) 1466 request = tasks.tasklists().list() 1467 next_request = tasks.tasklists().list_next( 1468 request, {'nextPageToken': '123abc'}) 1469 parsed = list(urlparse(next_request.uri)) 1470 q = parse_qs(parsed[4]) 1471 self.assertEqual(q['pageToken'][0], '123abc') 1472 1473 def test_next_successful_with_next_page_token_alternate_name(self): 1474 self.http = HttpMock(datafile('bigquery.json'), {'status': '200'}) 1475 bigquery = build('bigquery', 'v2', http=self.http) 1476 request = bigquery.tabledata().list(datasetId='', projectId='', tableId='') 1477 next_request = bigquery.tabledata().list_next( 1478 request, {'pageToken': '123abc'}) 1479 parsed = list(urlparse(next_request.uri)) 1480 q = parse_qs(parsed[4]) 1481 self.assertEqual(q['pageToken'][0], '123abc') 1482 1483 def test_next_successful_with_next_page_token_in_body(self): 1484 self.http = HttpMock(datafile('logging.json'), {'status': '200'}) 1485 logging = build('logging', 'v2', http=self.http) 1486 request = logging.entries().list(body={}) 1487 next_request = logging.entries().list_next( 1488 request, {'nextPageToken': '123abc'}) 1489 body = JsonModel().deserialize(next_request.body) 1490 self.assertEqual(body['pageToken'], '123abc') 1491 1492 def test_next_with_method_with_no_properties(self): 1493 self.http = HttpMock(datafile('latitude.json'), {'status': '200'}) 1494 service = build('latitude', 'v1', http=self.http) 1495 service.currentLocation().get() 1496 1497 def test_next_nonexistent_with_no_next_page_token(self): 1498 self.http = HttpMock(datafile('drive.json'), {'status': '200'}) 1499 drive = build('drive', 'v3', http=self.http) 1500 drive.changes().watch(body={}) 1501 self.assertFalse(callable(getattr(drive.changes(), 'watch_next', None))) 1502 1503 def test_next_successful_with_next_page_token_required(self): 1504 self.http = HttpMock(datafile('drive.json'), {'status': '200'}) 1505 drive = build('drive', 'v3', http=self.http) 1506 request = drive.changes().list(pageToken='startPageToken') 1507 next_request = drive.changes().list_next( 1508 request, {'nextPageToken': '123abc'}) 1509 parsed = list(urlparse(next_request.uri)) 1510 q = parse_qs(parsed[4]) 1511 self.assertEqual(q['pageToken'][0], '123abc') 1512 1513 1514 class MediaGet(unittest.TestCase): 1515 1516 def test_get_media(self): 1517 http = HttpMock(datafile('zoo.json'), {'status': '200'}) 1518 zoo = build('zoo', 'v1', http=http) 1519 request = zoo.animals().get_media(name='Lion') 1520 1521 parsed = urlparse(request.uri) 1522 q = parse_qs(parsed[4]) 1523 self.assertEqual(q['alt'], ['media']) 1524 self.assertEqual(request.headers['accept'], '*/*') 1525 1526 http = HttpMockSequence([ 1527 ({'status': '200'}, 'standing in for media'), 1528 ]) 1529 response = request.execute(http=http) 1530 self.assertEqual(b'standing in for media', response) 1531 1532 1533 if __name__ == '__main__': 1534 unittest.main() 1535