• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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