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