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
24
25__author__ = "[email protected] (Joe Gregorio)"
26
27from collections import defaultdict
28import copy
29import datetime
30import httplib2
31import io
32import itertools
33import json
34import os
35import pickle
36import re
37import sys
38import unittest
39import urllib
40
41from parameterized import parameterized
42import mock
43
44import google.auth.credentials
45from google.auth.transport import mtls
46from google.auth.exceptions import MutualTLSChannelError
47import google_auth_httplib2
48import google.api_core.exceptions
49
50from googleapiclient.discovery import _fix_up_media_upload
51from googleapiclient.discovery import _fix_up_method_description
52from googleapiclient.discovery import _fix_up_parameters
53from googleapiclient.discovery import _urljoin
54from googleapiclient.discovery import build
55from googleapiclient.discovery import build_from_document
56from googleapiclient.discovery import DISCOVERY_URI
57from googleapiclient.discovery import key2param
58from googleapiclient.discovery import MEDIA_BODY_PARAMETER_DEFAULT_VALUE
59from googleapiclient.discovery import MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE
60from googleapiclient.discovery import ResourceMethodParameters
61from googleapiclient.discovery import STACK_QUERY_PARAMETERS
62from googleapiclient.discovery import STACK_QUERY_PARAMETER_DEFAULT_VALUE
63from googleapiclient.discovery import V1_DISCOVERY_URI
64from googleapiclient.discovery import V2_DISCOVERY_URI
65from googleapiclient.discovery_cache import DISCOVERY_DOC_MAX_AGE
66from googleapiclient.discovery_cache.base import Cache
67from googleapiclient.errors import HttpError
68from googleapiclient.errors import InvalidJsonError
69from googleapiclient.errors import MediaUploadSizeError
70from googleapiclient.errors import ResumableUploadError
71from googleapiclient.errors import UnacceptableMimeTypeError
72from googleapiclient.errors import UnknownApiNameOrVersion
73from googleapiclient.errors import UnknownFileType
74from googleapiclient.http import build_http
75from googleapiclient.http import BatchHttpRequest
76from googleapiclient.http import HttpMock
77from googleapiclient.http import HttpMockSequence
78from googleapiclient.http import MediaFileUpload
79from googleapiclient.http import MediaIoBaseUpload
80from googleapiclient.http import MediaUpload
81from googleapiclient.http import MediaUploadProgress
82from googleapiclient.http import tunnel_patch
83from googleapiclient.model import JsonModel
84from googleapiclient.schema import Schemas
85from oauth2client import GOOGLE_TOKEN_URI
86from oauth2client.client import OAuth2Credentials, GoogleCredentials
87
88
89from googleapiclient import _helpers as util
90
91import uritemplate
92
93
94DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
95
96
97def assertUrisEqual(testcase, expected, actual):
98    """Test that URIs are the same, up to reordering of query parameters."""
99    expected = urllib.parse.urlparse(expected)
100    actual = urllib.parse.urlparse(actual)
101    testcase.assertEqual(expected.scheme, actual.scheme)
102    testcase.assertEqual(expected.netloc, actual.netloc)
103    testcase.assertEqual(expected.path, actual.path)
104    testcase.assertEqual(expected.params, actual.params)
105    testcase.assertEqual(expected.fragment, actual.fragment)
106    expected_query = urllib.parse.parse_qs(expected.query)
107    actual_query = urllib.parse.parse_qs(actual.query)
108    for name in list(expected_query.keys()):
109        testcase.assertEqual(expected_query[name], actual_query[name])
110    for name in list(actual_query.keys()):
111        testcase.assertEqual(expected_query[name], actual_query[name])
112
113
114def assert_discovery_uri(testcase, actual, service_name, version, discovery):
115    """Assert that discovery URI used was the one that was expected
116    for a given service and version."""
117    params = {"api": service_name, "apiVersion": version}
118    expanded_requested_uri = uritemplate.expand(discovery, params)
119    assertUrisEqual(testcase, expanded_requested_uri, actual)
120
121
122def validate_discovery_requests(testcase, http_mock, service_name, version, discovery):
123    """Validates that there have > 0 calls to Http Discovery
124     and that LAST discovery URI used was the one that was expected
125    for a given service and version."""
126    testcase.assertTrue(len(http_mock.request_sequence) > 0)
127    if len(http_mock.request_sequence) > 0:
128        actual_uri = http_mock.request_sequence[-1][0]
129        assert_discovery_uri(testcase, actual_uri, service_name, version, discovery)
130
131
132def datafile(filename):
133    return os.path.join(DATA_DIR, filename)
134
135
136def read_datafile(filename, mode="r"):
137    with open(datafile(filename), mode=mode) as f:
138        return f.read()
139
140
141class SetupHttplib2(unittest.TestCase):
142    def test_retries(self):
143        # Merely loading googleapiclient.discovery should set the RETRIES to 1.
144        self.assertEqual(1, httplib2.RETRIES)
145
146
147class Utilities(unittest.TestCase):
148    def setUp(self):
149        self.zoo_root_desc = json.loads(read_datafile("zoo.json", "r"))
150        self.zoo_get_method_desc = self.zoo_root_desc["methods"]["query"]
151        self.zoo_animals_resource = self.zoo_root_desc["resources"]["animals"]
152        self.zoo_insert_method_desc = self.zoo_animals_resource["methods"]["insert"]
153        self.zoo_schema = Schemas(self.zoo_root_desc)
154
155    def test_key2param(self):
156        self.assertEqual("max_results", key2param("max-results"))
157        self.assertEqual("x007_bond", key2param("007-bond"))
158
159    def _base_fix_up_parameters_test(self, method_desc, http_method, root_desc, schema):
160        self.assertEqual(method_desc["httpMethod"], http_method)
161
162        method_desc_copy = copy.deepcopy(method_desc)
163        self.assertEqual(method_desc, method_desc_copy)
164
165        parameters = _fix_up_parameters(
166            method_desc_copy, root_desc, http_method, schema
167        )
168
169        self.assertNotEqual(method_desc, method_desc_copy)
170
171        for param_name in STACK_QUERY_PARAMETERS:
172            self.assertEqual(
173                STACK_QUERY_PARAMETER_DEFAULT_VALUE, parameters[param_name]
174            )
175
176        for param_name, value in root_desc.get("parameters", {}).items():
177            self.assertEqual(value, parameters[param_name])
178
179        return parameters
180
181    def test_fix_up_parameters_get(self):
182        parameters = self._base_fix_up_parameters_test(
183            self.zoo_get_method_desc, "GET", self.zoo_root_desc, self.zoo_schema
184        )
185        # Since http_method is 'GET'
186        self.assertFalse("body" in parameters)
187
188    def test_fix_up_parameters_insert(self):
189        parameters = self._base_fix_up_parameters_test(
190            self.zoo_insert_method_desc, "POST", self.zoo_root_desc, self.zoo_schema
191        )
192        body = {"description": "The request body.", "type": "object", "$ref": "Animal"}
193        self.assertEqual(parameters["body"], body)
194
195    def test_fix_up_parameters_check_body(self):
196        dummy_root_desc = {}
197        dummy_schema = {
198            "Request": {
199                "properties": {
200                    "description": "Required. Dummy parameter.",
201                    "type": "string",
202                }
203            }
204        }
205        no_payload_http_method = "DELETE"
206        with_payload_http_method = "PUT"
207
208        invalid_method_desc = {"response": "Who cares"}
209        valid_method_desc = {
210            "request": {"key1": "value1", "key2": "value2", "$ref": "Request"}
211        }
212
213        parameters = _fix_up_parameters(
214            invalid_method_desc, dummy_root_desc, no_payload_http_method, dummy_schema
215        )
216        self.assertFalse("body" in parameters)
217
218        parameters = _fix_up_parameters(
219            valid_method_desc, dummy_root_desc, no_payload_http_method, dummy_schema
220        )
221        self.assertFalse("body" in parameters)
222
223        parameters = _fix_up_parameters(
224            invalid_method_desc, dummy_root_desc, with_payload_http_method, dummy_schema
225        )
226        self.assertFalse("body" in parameters)
227
228        parameters = _fix_up_parameters(
229            valid_method_desc, dummy_root_desc, with_payload_http_method, dummy_schema
230        )
231        body = {
232            "description": "The request body.",
233            "type": "object",
234            "$ref": "Request",
235            "key1": "value1",
236            "key2": "value2",
237        }
238        self.assertEqual(parameters["body"], body)
239
240    def test_fix_up_parameters_optional_body(self):
241        # Request with no parameters
242        dummy_schema = {"Request": {"properties": {}}}
243        method_desc = {"request": {"$ref": "Request"}}
244
245        parameters = _fix_up_parameters(method_desc, {}, "POST", dummy_schema)
246
247    def _base_fix_up_method_description_test(
248        self,
249        method_desc,
250        initial_parameters,
251        final_parameters,
252        final_accept,
253        final_max_size,
254        final_media_path_url,
255    ):
256        fake_root_desc = {
257            "rootUrl": "http://root/",
258            "servicePath": "fake/",
259            "mtlsRootUrl": "http://root/",
260        }
261        fake_path_url = "fake-path/"
262
263        accept, max_size, media_path_url = _fix_up_media_upload(
264            method_desc, fake_root_desc, fake_path_url, initial_parameters
265        )
266        self.assertEqual(accept, final_accept)
267        self.assertEqual(max_size, final_max_size)
268        self.assertEqual(media_path_url, final_media_path_url)
269        self.assertEqual(initial_parameters, final_parameters)
270
271    def test_fix_up_media_upload_no_initial_invalid(self):
272        invalid_method_desc = {"response": "Who cares"}
273        self._base_fix_up_method_description_test(
274            invalid_method_desc, {}, {}, [], 0, None
275        )
276
277    def test_fix_up_media_upload_no_initial_valid_minimal(self):
278        valid_method_desc = {"mediaUpload": {"accept": []}}
279        final_parameters = {
280            "media_body": MEDIA_BODY_PARAMETER_DEFAULT_VALUE,
281            "media_mime_type": MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE,
282        }
283        self._base_fix_up_method_description_test(
284            valid_method_desc,
285            {},
286            final_parameters,
287            [],
288            0,
289            "http://root/upload/fake/fake-path/",
290        )
291
292    def test_fix_up_media_upload_no_initial_valid_full(self):
293        valid_method_desc = {"mediaUpload": {"accept": ["*/*"], "maxSize": "10GB"}}
294        final_parameters = {
295            "media_body": MEDIA_BODY_PARAMETER_DEFAULT_VALUE,
296            "media_mime_type": MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE,
297        }
298        ten_gb = 10 * 2 ** 30
299        self._base_fix_up_method_description_test(
300            valid_method_desc,
301            {},
302            final_parameters,
303            ["*/*"],
304            ten_gb,
305            "http://root/upload/fake/fake-path/",
306        )
307
308    def test_fix_up_media_upload_with_initial_invalid(self):
309        invalid_method_desc = {"response": "Who cares"}
310        initial_parameters = {"body": {}}
311        self._base_fix_up_method_description_test(
312            invalid_method_desc, initial_parameters, initial_parameters, [], 0, None
313        )
314
315    def test_fix_up_media_upload_with_initial_valid_minimal(self):
316        valid_method_desc = {"mediaUpload": {"accept": []}}
317        initial_parameters = {"body": {}}
318        final_parameters = {
319            "body": {},
320            "media_body": MEDIA_BODY_PARAMETER_DEFAULT_VALUE,
321            "media_mime_type": MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE,
322        }
323        self._base_fix_up_method_description_test(
324            valid_method_desc,
325            initial_parameters,
326            final_parameters,
327            [],
328            0,
329            "http://root/upload/fake/fake-path/",
330        )
331
332    def test_fix_up_media_upload_with_initial_valid_full(self):
333        valid_method_desc = {"mediaUpload": {"accept": ["*/*"], "maxSize": "10GB"}}
334        initial_parameters = {"body": {}}
335        final_parameters = {
336            "body": {},
337            "media_body": MEDIA_BODY_PARAMETER_DEFAULT_VALUE,
338            "media_mime_type": MEDIA_MIME_TYPE_PARAMETER_DEFAULT_VALUE,
339        }
340        ten_gb = 10 * 2 ** 30
341        self._base_fix_up_method_description_test(
342            valid_method_desc,
343            initial_parameters,
344            final_parameters,
345            ["*/*"],
346            ten_gb,
347            "http://root/upload/fake/fake-path/",
348        )
349
350    def test_fix_up_method_description_get(self):
351        result = _fix_up_method_description(
352            self.zoo_get_method_desc, self.zoo_root_desc, self.zoo_schema
353        )
354        path_url = "query"
355        http_method = "GET"
356        method_id = "bigquery.query"
357        accept = []
358        max_size = 0
359        media_path_url = None
360        self.assertEqual(
361            result, (path_url, http_method, method_id, accept, max_size, media_path_url)
362        )
363
364    def test_fix_up_method_description_insert(self):
365        result = _fix_up_method_description(
366            self.zoo_insert_method_desc, self.zoo_root_desc, self.zoo_schema
367        )
368        path_url = "animals"
369        http_method = "POST"
370        method_id = "zoo.animals.insert"
371        accept = ["image/png"]
372        max_size = 1024
373        media_path_url = "https://www.googleapis.com/upload/zoo/v1/animals"
374        self.assertEqual(
375            result, (path_url, http_method, method_id, accept, max_size, media_path_url)
376        )
377
378    def test_urljoin(self):
379        # We want to exhaustively test various URL combinations.
380        simple_bases = ["https://www.googleapis.com", "https://www.googleapis.com/"]
381        long_urls = ["foo/v1/bar:custom?alt=json", "/foo/v1/bar:custom?alt=json"]
382
383        long_bases = [
384            "https://www.googleapis.com/foo/v1",
385            "https://www.googleapis.com/foo/v1/",
386        ]
387        simple_urls = ["bar:custom?alt=json", "/bar:custom?alt=json"]
388
389        final_url = "https://www.googleapis.com/foo/v1/bar:custom?alt=json"
390        for base, url in itertools.product(simple_bases, long_urls):
391            self.assertEqual(final_url, _urljoin(base, url))
392        for base, url in itertools.product(long_bases, simple_urls):
393            self.assertEqual(final_url, _urljoin(base, url))
394
395    def test_ResourceMethodParameters_zoo_get(self):
396        parameters = ResourceMethodParameters(self.zoo_get_method_desc)
397
398        param_types = {
399            "a": "any",
400            "b": "boolean",
401            "e": "string",
402            "er": "string",
403            "i": "integer",
404            "n": "number",
405            "o": "object",
406            "q": "string",
407            "rr": "string",
408        }
409        keys = list(param_types.keys())
410        self.assertEqual(parameters.argmap, dict((key, key) for key in keys))
411        self.assertEqual(parameters.required_params, [])
412        self.assertEqual(sorted(parameters.repeated_params), ["er", "rr"])
413        self.assertEqual(parameters.pattern_params, {"rr": "[a-z]+"})
414        self.assertEqual(
415            sorted(parameters.query_params),
416            ["a", "b", "e", "er", "i", "n", "o", "q", "rr"],
417        )
418        self.assertEqual(parameters.path_params, set())
419        self.assertEqual(parameters.param_types, param_types)
420        enum_params = {"e": ["foo", "bar"], "er": ["one", "two", "three"]}
421        self.assertEqual(parameters.enum_params, enum_params)
422
423    def test_ResourceMethodParameters_zoo_animals_patch(self):
424        method_desc = self.zoo_animals_resource["methods"]["patch"]
425        parameters = ResourceMethodParameters(method_desc)
426
427        param_types = {"name": "string"}
428        keys = list(param_types.keys())
429        self.assertEqual(parameters.argmap, dict((key, key) for key in keys))
430        self.assertEqual(parameters.required_params, ["name"])
431        self.assertEqual(parameters.repeated_params, [])
432        self.assertEqual(parameters.pattern_params, {})
433        self.assertEqual(parameters.query_params, [])
434        self.assertEqual(parameters.path_params, set(["name"]))
435        self.assertEqual(parameters.param_types, param_types)
436        self.assertEqual(parameters.enum_params, {})
437
438
439class Discovery(unittest.TestCase):
440    def test_discovery_http_is_closed(self):
441        http = HttpMock(datafile("malformed.json"), {"status": "200"})
442        service = build("plus", "v1", credentials=mock.sentinel.credentials)
443        http.close.assert_called_once()
444
445
446class DiscoveryErrors(unittest.TestCase):
447    def test_tests_should_be_run_with_strict_positional_enforcement(self):
448        try:
449            plus = build("plus", "v1", None, static_discovery=False)
450            self.fail("should have raised a TypeError exception over missing http=.")
451        except TypeError:
452            pass
453
454    def test_failed_to_parse_discovery_json(self):
455        self.http = HttpMock(datafile("malformed.json"), {"status": "200"})
456        try:
457            plus = build("plus", "v1", http=self.http, cache_discovery=False, static_discovery=False)
458            self.fail("should have raised an exception over malformed JSON.")
459        except InvalidJsonError:
460            pass
461
462    def test_unknown_api_name_or_version(self):
463        http = HttpMockSequence(
464            [
465                ({"status": "404"}, read_datafile("zoo.json", "rb")),
466                ({"status": "404"}, read_datafile("zoo.json", "rb")),
467            ]
468        )
469        with self.assertRaises(UnknownApiNameOrVersion):
470            plus = build("plus", "v1", http=http, cache_discovery=False)
471
472    def test_credentials_and_http_mutually_exclusive(self):
473        http = HttpMock(datafile("plus.json"), {"status": "200"})
474        with self.assertRaises(ValueError):
475            build("plus", "v1", http=http, credentials=mock.sentinel.credentials, static_discovery=False)
476
477    def test_credentials_file_and_http_mutually_exclusive(self):
478        http = HttpMock(datafile("plus.json"), {"status": "200"})
479        with self.assertRaises(ValueError):
480            build(
481                "plus",
482                "v1",
483                http=http,
484                client_options=google.api_core.client_options.ClientOptions(
485                    credentials_file="credentials.json"
486                ),
487                static_discovery=False,
488            )
489
490    def test_credentials_and_credentials_file_mutually_exclusive(self):
491        with self.assertRaises(google.api_core.exceptions.DuplicateCredentialArgs):
492            build(
493                "plus",
494                "v1",
495                credentials=mock.sentinel.credentials,
496                client_options=google.api_core.client_options.ClientOptions(
497                    credentials_file="credentials.json"
498                ),
499                static_discovery=False,
500            )
501
502
503class DiscoveryFromDocument(unittest.TestCase):
504    MOCK_CREDENTIALS = mock.Mock(spec=google.auth.credentials.Credentials)
505
506    def test_can_build_from_local_document(self):
507        discovery = read_datafile("plus.json")
508        plus = build_from_document(
509            discovery,
510            base="https://www.googleapis.com/",
511            credentials=self.MOCK_CREDENTIALS,
512        )
513        self.assertIsNotNone(plus)
514        self.assertTrue(hasattr(plus, "activities"))
515
516    def test_can_build_from_local_deserialized_document(self):
517        discovery = read_datafile("plus.json")
518        discovery = json.loads(discovery)
519        plus = build_from_document(
520            discovery,
521            base="https://www.googleapis.com/",
522            credentials=self.MOCK_CREDENTIALS,
523        )
524        self.assertIsNotNone(plus)
525        self.assertTrue(hasattr(plus, "activities"))
526
527    def test_building_with_base_remembers_base(self):
528        discovery = read_datafile("plus.json")
529
530        base = "https://www.example.com/"
531        plus = build_from_document(
532            discovery, base=base, credentials=self.MOCK_CREDENTIALS
533        )
534        self.assertEqual("https://www.googleapis.com/plus/v1/", plus._baseUrl)
535
536    def test_building_with_optional_http_with_authorization(self):
537        discovery = read_datafile("plus.json")
538        plus = build_from_document(
539            discovery,
540            base="https://www.googleapis.com/",
541            credentials=self.MOCK_CREDENTIALS,
542        )
543
544        # plus service requires Authorization, hence we expect to see AuthorizedHttp object here
545        self.assertIsInstance(plus._http, google_auth_httplib2.AuthorizedHttp)
546        self.assertIsInstance(plus._http.http, httplib2.Http)
547        self.assertIsInstance(plus._http.http.timeout, int)
548        self.assertGreater(plus._http.http.timeout, 0)
549
550    def test_building_with_optional_http_with_no_authorization(self):
551        discovery = read_datafile("plus.json")
552        # Cleanup auth field, so we would use plain http client
553        discovery = json.loads(discovery)
554        discovery["auth"] = {}
555        discovery = json.dumps(discovery)
556
557        plus = build_from_document(
558            discovery, base="https://www.googleapis.com/", credentials=None
559        )
560        # plus service requires Authorization
561        self.assertIsInstance(plus._http, httplib2.Http)
562        self.assertIsInstance(plus._http.timeout, int)
563        self.assertGreater(plus._http.timeout, 0)
564
565    def test_building_with_explicit_http(self):
566        http = HttpMock()
567        discovery = read_datafile("plus.json")
568        plus = build_from_document(
569            discovery, base="https://www.googleapis.com/", http=http
570        )
571        self.assertEqual(plus._http, http)
572
573    def test_building_with_developer_key_skips_adc(self):
574        discovery = read_datafile("plus.json")
575        plus = build_from_document(
576            discovery, base="https://www.googleapis.com/", developerKey="123"
577        )
578        self.assertIsInstance(plus._http, httplib2.Http)
579        # It should not be an AuthorizedHttp, because that would indicate that
580        # application default credentials were used.
581        self.assertNotIsInstance(plus._http, google_auth_httplib2.AuthorizedHttp)
582
583    def test_building_with_context_manager(self):
584        discovery = read_datafile("plus.json")
585        with mock.patch("httplib2.Http") as http:
586            with build_from_document(discovery, base="https://www.googleapis.com/", credentials=self.MOCK_CREDENTIALS) as plus:
587                self.assertIsNotNone(plus)
588                self.assertTrue(hasattr(plus, "activities"))
589            plus._http.http.close.assert_called_once()
590
591    def test_resource_close(self):
592        discovery = read_datafile("plus.json")
593
594        with mock.patch("httplib2.Http", autospec=True) as httplib2_http:
595            http = httplib2_http()
596            plus = build_from_document(
597                discovery,
598                base="https://www.googleapis.com/",
599                http=http,
600            )
601            plus.close()
602            http.close.assert_called_once()
603
604    def test_resource_close_authorized_http(self):
605        discovery = read_datafile("plus.json")
606        with mock.patch("google_auth_httplib2.AuthorizedHttp", autospec=True):
607            plus = build_from_document(
608                discovery,
609                base="https://www.googleapis.com/",
610                credentials=self.MOCK_CREDENTIALS,
611            )
612            plus.close()
613            plus._http.close.assert_called_once()
614
615    def test_api_endpoint_override_from_client_options(self):
616        discovery = read_datafile("plus.json")
617        api_endpoint = "https://foo.googleapis.com/"
618        options = google.api_core.client_options.ClientOptions(
619            api_endpoint=api_endpoint
620        )
621        plus = build_from_document(
622            discovery, client_options=options, credentials=self.MOCK_CREDENTIALS
623        )
624
625        self.assertEqual(plus._baseUrl, api_endpoint)
626
627    def test_api_endpoint_override_from_client_options_mapping_object(self):
628
629        discovery = read_datafile("plus.json")
630        api_endpoint = "https://foo.googleapis.com/"
631        mapping_object = defaultdict(str)
632        mapping_object["api_endpoint"] = api_endpoint
633        plus = build_from_document(
634            discovery, client_options=mapping_object, credentials=self.MOCK_CREDENTIALS
635        )
636
637        self.assertEqual(plus._baseUrl, api_endpoint)
638
639    def test_api_endpoint_override_from_client_options_dict(self):
640        discovery = read_datafile("plus.json")
641        api_endpoint = "https://foo.googleapis.com/"
642        plus = build_from_document(
643            discovery,
644            client_options={"api_endpoint": api_endpoint},
645            credentials=self.MOCK_CREDENTIALS,
646        )
647
648        self.assertEqual(plus._baseUrl, api_endpoint)
649
650    def test_scopes_from_client_options(self):
651        discovery = read_datafile("plus.json")
652
653        with mock.patch("googleapiclient._auth.default_credentials") as default:
654            plus = build_from_document(
655                discovery, client_options={"scopes": ["1", "2"]},
656            )
657
658        default.assert_called_once_with(scopes=["1", "2"], quota_project_id=None)
659
660    def test_quota_project_from_client_options(self):
661        discovery = read_datafile("plus.json")
662
663        with mock.patch("googleapiclient._auth.default_credentials") as default:
664            plus = build_from_document(
665                discovery,
666                client_options=google.api_core.client_options.ClientOptions(
667                    quota_project_id="my-project"
668                ),
669            )
670
671        default.assert_called_once_with(scopes=None, quota_project_id="my-project")
672
673    def test_credentials_file_from_client_options(self):
674        discovery = read_datafile("plus.json")
675
676        with mock.patch("googleapiclient._auth.credentials_from_file") as default:
677            plus = build_from_document(
678                discovery,
679                client_options=google.api_core.client_options.ClientOptions(
680                    credentials_file="credentials.json"
681                ),
682            )
683
684        default.assert_called_once_with(
685            "credentials.json", scopes=None, quota_project_id=None
686        )
687
688    def test_self_signed_jwt_enabled(self):
689        service_account_file_path = os.path.join(DATA_DIR, "service_account.json")
690        creds = google.oauth2.service_account.Credentials.from_service_account_file(service_account_file_path)
691
692        discovery = read_datafile("logging.json")
693
694        with mock.patch("google.oauth2.service_account.Credentials._create_self_signed_jwt") as _create_self_signed_jwt:
695            build_from_document(
696                discovery,
697                credentials=creds,
698                always_use_jwt_access=True,
699            )
700            _create_self_signed_jwt.assert_called_with("https://logging.googleapis.com/")
701
702    def test_self_signed_jwt_disabled(self):
703        service_account_file_path = os.path.join(DATA_DIR, "service_account.json")
704        creds = google.oauth2.service_account.Credentials.from_service_account_file(service_account_file_path)
705
706        discovery = read_datafile("logging.json")
707
708        with mock.patch("google.oauth2.service_account.Credentials._create_self_signed_jwt") as _create_self_signed_jwt:
709            build_from_document(
710                discovery,
711                credentials=creds,
712            )
713            _create_self_signed_jwt.assert_not_called()
714
715
716REGULAR_ENDPOINT = "https://www.googleapis.com/plus/v1/"
717MTLS_ENDPOINT = "https://www.mtls.googleapis.com/plus/v1/"
718
719
720class DiscoveryFromDocumentMutualTLS(unittest.TestCase):
721    MOCK_CREDENTIALS = mock.Mock(spec=google.auth.credentials.Credentials)
722    ADC_CERT_PATH = "adc_cert_path"
723    ADC_KEY_PATH = "adc_key_path"
724    ADC_PASSPHRASE = "adc_passphrase"
725
726    def check_http_client_cert(self, resource, has_client_cert="false"):
727        if isinstance(resource._http, google_auth_httplib2.AuthorizedHttp):
728            certs = list(resource._http.http.certificates.iter(""))
729        else:
730            certs = list(resource._http.certificates.iter(""))
731        if has_client_cert == "true":
732            self.assertEqual(len(certs), 1)
733            self.assertEqual(
734                certs[0], (self.ADC_KEY_PATH, self.ADC_CERT_PATH, self.ADC_PASSPHRASE)
735            )
736        else:
737            self.assertEqual(len(certs), 0)
738
739    def client_encrypted_cert_source(self):
740        return self.ADC_CERT_PATH, self.ADC_KEY_PATH, self.ADC_PASSPHRASE
741
742    @parameterized.expand(
743        [
744            ("never", "true"),
745            ("auto", "true"),
746            ("always", "true"),
747            ("never", "false"),
748            ("auto", "false"),
749            ("always", "false"),
750        ]
751    )
752    def test_mtls_not_trigger_if_http_provided(self, use_mtls_env, use_client_cert):
753        discovery = read_datafile("plus.json")
754
755        with mock.patch.dict(
756            "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
757        ):
758            with mock.patch.dict(
759                "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
760            ):
761                plus = build_from_document(discovery, http=httplib2.Http())
762                self.assertIsNotNone(plus)
763                self.assertEqual(plus._baseUrl, REGULAR_ENDPOINT)
764                self.check_http_client_cert(plus, has_client_cert="false")
765
766    @parameterized.expand(
767        [
768            ("never", "true"),
769            ("auto", "true"),
770            ("always", "true"),
771            ("never", "false"),
772            ("auto", "false"),
773            ("always", "false"),
774        ]
775    )
776    def test_exception_with_client_cert_source(self, use_mtls_env, use_client_cert):
777        discovery = read_datafile("plus.json")
778        with mock.patch.dict(
779            "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
780        ):
781            with mock.patch.dict(
782                "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
783            ):
784                with self.assertRaises(MutualTLSChannelError):
785                    build_from_document(
786                        discovery,
787                        credentials=self.MOCK_CREDENTIALS,
788                        client_options={"client_cert_source": mock.Mock()},
789                    )
790
791    @parameterized.expand(
792        [
793            ("never", "true", REGULAR_ENDPOINT),
794            ("auto", "true", MTLS_ENDPOINT),
795            ("always", "true", MTLS_ENDPOINT),
796            ("never", "false", REGULAR_ENDPOINT),
797            ("auto", "false", REGULAR_ENDPOINT),
798            ("always", "false", MTLS_ENDPOINT),
799        ]
800    )
801    def test_mtls_with_provided_client_cert(
802        self, use_mtls_env, use_client_cert, base_url
803    ):
804        discovery = read_datafile("plus.json")
805
806        with mock.patch.dict(
807            "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
808        ):
809            with mock.patch.dict(
810                "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
811            ):
812                plus = build_from_document(
813                    discovery,
814                    credentials=self.MOCK_CREDENTIALS,
815                    client_options={
816                        "client_encrypted_cert_source": self.client_encrypted_cert_source
817                    },
818                )
819                self.assertIsNotNone(plus)
820                self.check_http_client_cert(plus, has_client_cert=use_client_cert)
821                self.assertEqual(plus._baseUrl, base_url)
822
823    @parameterized.expand(
824        [
825            ("never", "true"),
826            ("auto", "true"),
827            ("always", "true"),
828            ("never", "false"),
829            ("auto", "false"),
830            ("always", "false"),
831        ]
832    )
833    def test_endpoint_not_switch(self, use_mtls_env, use_client_cert):
834        # Test endpoint is not switched if user provided api endpoint
835        discovery = read_datafile("plus.json")
836
837        with mock.patch.dict(
838            "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
839        ):
840            with mock.patch.dict(
841                "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
842            ):
843                plus = build_from_document(
844                    discovery,
845                    credentials=self.MOCK_CREDENTIALS,
846                    client_options={
847                        "api_endpoint": "https://foo.googleapis.com",
848                        "client_encrypted_cert_source": self.client_encrypted_cert_source,
849                    },
850                )
851                self.assertIsNotNone(plus)
852                self.check_http_client_cert(plus, has_client_cert=use_client_cert)
853                self.assertEqual(plus._baseUrl, "https://foo.googleapis.com")
854
855    @parameterized.expand(
856        [
857            ("never", "true", REGULAR_ENDPOINT),
858            ("auto", "true", MTLS_ENDPOINT),
859            ("always", "true", MTLS_ENDPOINT),
860            ("never", "false", REGULAR_ENDPOINT),
861            ("auto", "false", REGULAR_ENDPOINT),
862            ("always", "false", MTLS_ENDPOINT),
863        ]
864    )
865    @mock.patch(
866        "google.auth.transport.mtls.has_default_client_cert_source", autospec=True
867    )
868    @mock.patch(
869        "google.auth.transport.mtls.default_client_encrypted_cert_source", autospec=True
870    )
871    def test_mtls_with_default_client_cert(
872        self,
873        use_mtls_env,
874        use_client_cert,
875        base_url,
876        default_client_encrypted_cert_source,
877        has_default_client_cert_source,
878    ):
879        has_default_client_cert_source.return_value = True
880        default_client_encrypted_cert_source.return_value = (
881            self.client_encrypted_cert_source
882        )
883        discovery = read_datafile("plus.json")
884
885        with mock.patch.dict(
886            "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
887        ):
888            with mock.patch.dict(
889                "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
890            ):
891                plus = build_from_document(
892                    discovery,
893                    credentials=self.MOCK_CREDENTIALS,
894                    adc_cert_path=self.ADC_CERT_PATH,
895                    adc_key_path=self.ADC_KEY_PATH,
896                )
897                self.assertIsNotNone(plus)
898                self.check_http_client_cert(plus, has_client_cert=use_client_cert)
899                self.assertEqual(plus._baseUrl, base_url)
900
901    @parameterized.expand(
902        [
903            ("never", "true", REGULAR_ENDPOINT),
904            ("auto", "true", REGULAR_ENDPOINT),
905            ("always", "true", MTLS_ENDPOINT),
906            ("never", "false", REGULAR_ENDPOINT),
907            ("auto", "false", REGULAR_ENDPOINT),
908            ("always", "false", MTLS_ENDPOINT),
909        ]
910    )
911    @mock.patch(
912        "google.auth.transport.mtls.has_default_client_cert_source", autospec=True
913    )
914    def test_mtls_with_no_client_cert(
915        self, use_mtls_env, use_client_cert, base_url, has_default_client_cert_source
916    ):
917        has_default_client_cert_source.return_value = False
918        discovery = read_datafile("plus.json")
919
920        with mock.patch.dict(
921            "os.environ", {"GOOGLE_API_USE_MTLS_ENDPOINT": use_mtls_env}
922        ):
923            with mock.patch.dict(
924                "os.environ", {"GOOGLE_API_USE_CLIENT_CERTIFICATE": use_client_cert}
925            ):
926                plus = build_from_document(
927                    discovery,
928                    credentials=self.MOCK_CREDENTIALS,
929                    adc_cert_path=self.ADC_CERT_PATH,
930                    adc_key_path=self.ADC_KEY_PATH,
931                )
932                self.assertIsNotNone(plus)
933                self.check_http_client_cert(plus, has_client_cert="false")
934                self.assertEqual(plus._baseUrl, base_url)
935
936
937class DiscoveryFromHttp(unittest.TestCase):
938    def setUp(self):
939        self.old_environ = os.environ.copy()
940
941    def tearDown(self):
942        os.environ = self.old_environ
943
944    def test_userip_is_added_to_discovery_uri(self):
945        # build() will raise an HttpError on a 400, use this to pick the request uri
946        # out of the raised exception.
947        os.environ["REMOTE_ADDR"] = "10.0.0.1"
948        try:
949            http = HttpMockSequence(
950                [({"status": "400"}, read_datafile("zoo.json", "rb"))]
951            )
952            zoo = build(
953                "zoo",
954                "v1",
955                http=http,
956                developerKey=None,
957                discoveryServiceUrl="http://example.com"
958            )
959            self.fail("Should have raised an exception.")
960        except HttpError as e:
961            self.assertEqual(e.uri, "http://example.com?userIp=10.0.0.1")
962
963    def test_userip_missing_is_not_added_to_discovery_uri(self):
964        # build() will raise an HttpError on a 400, use this to pick the request uri
965        # out of the raised exception.
966        try:
967            http = HttpMockSequence(
968                [({"status": "400"}, read_datafile("zoo.json", "rb"))]
969            )
970            zoo = build(
971                "zoo",
972                "v1",
973                http=http,
974                developerKey=None,
975                discoveryServiceUrl="http://example.com"
976            )
977            self.fail("Should have raised an exception.")
978        except HttpError as e:
979            self.assertEqual(e.uri, "http://example.com")
980
981    def test_key_is_added_to_discovery_uri(self):
982        # build() will raise an HttpError on a 400, use this to pick the request uri
983        # out of the raised exception.
984        try:
985            http = HttpMockSequence(
986                [({"status": "400"}, read_datafile("zoo.json", "rb"))]
987            )
988            zoo = build(
989                "zoo",
990                "v1",
991                http=http,
992                developerKey="foo",
993                discoveryServiceUrl="http://example.com",
994                static_discovery=False,
995            )
996            self.fail("Should have raised an exception.")
997        except HttpError as e:
998            self.assertEqual(e.uri, "http://example.com?key=foo")
999
1000    def test_discovery_loading_from_v2_discovery_uri(self):
1001        http = HttpMockSequence(
1002            [
1003                ({"status": "404"}, "Not found"),
1004                ({"status": "200"}, read_datafile("zoo.json", "rb")),
1005            ]
1006        )
1007        zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
1008        self.assertTrue(hasattr(zoo, "animals"))
1009
1010    def test_api_endpoint_override_from_client_options(self):
1011        http = HttpMockSequence(
1012            [
1013                ({"status": "404"}, "Not found"),
1014                ({"status": "200"}, read_datafile("zoo.json", "rb")),
1015            ]
1016        )
1017        api_endpoint = "https://foo.googleapis.com/"
1018        options = google.api_core.client_options.ClientOptions(
1019            api_endpoint=api_endpoint
1020        )
1021        zoo = build(
1022            "zoo", "v1", http=http, cache_discovery=False, client_options=options, static_discovery=False
1023        )
1024        self.assertEqual(zoo._baseUrl, api_endpoint)
1025
1026    def test_api_endpoint_override_from_client_options_dict(self):
1027        http = HttpMockSequence(
1028            [
1029                ({"status": "404"}, "Not found"),
1030                ({"status": "200"}, read_datafile("zoo.json", "rb")),
1031            ]
1032        )
1033        api_endpoint = "https://foo.googleapis.com/"
1034        zoo = build(
1035            "zoo",
1036            "v1",
1037            http=http,
1038            cache_discovery=False,
1039            client_options={"api_endpoint": api_endpoint},
1040            static_discovery=False,
1041        )
1042        self.assertEqual(zoo._baseUrl, api_endpoint)
1043
1044    def test_discovery_with_empty_version_uses_v2(self):
1045        http = HttpMockSequence([({"status": "200"}, read_datafile("zoo.json", "rb")),])
1046        build("zoo", version=None, http=http, cache_discovery=False, static_discovery=False)
1047        validate_discovery_requests(self, http, "zoo", None, V2_DISCOVERY_URI)
1048
1049    def test_discovery_with_empty_version_preserves_custom_uri(self):
1050        http = HttpMockSequence([({"status": "200"}, read_datafile("zoo.json", "rb")),])
1051        custom_discovery_uri = "https://foo.bar/$discovery"
1052        build(
1053            "zoo",
1054            version=None,
1055            http=http,
1056            cache_discovery=False,
1057            discoveryServiceUrl=custom_discovery_uri,
1058            static_discovery=False,
1059        )
1060        validate_discovery_requests(self, http, "zoo", None, custom_discovery_uri)
1061
1062    def test_discovery_with_valid_version_uses_v1(self):
1063        http = HttpMockSequence([({"status": "200"}, read_datafile("zoo.json", "rb")),])
1064        build("zoo", version="v123", http=http, cache_discovery=False, static_discovery=False)
1065        validate_discovery_requests(self, http, "zoo", "v123", V1_DISCOVERY_URI)
1066
1067
1068class DiscoveryRetryFromHttp(unittest.TestCase):
1069    def test_repeated_500_retries_and_fails(self):
1070        http = HttpMockSequence(
1071            [
1072                ({"status": "500"}, read_datafile("500.json", "rb")),
1073                ({"status": "503"}, read_datafile("503.json", "rb")),
1074            ]
1075        )
1076        with self.assertRaises(HttpError):
1077            with mock.patch("time.sleep") as mocked_sleep:
1078                build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
1079
1080        mocked_sleep.assert_called_once()
1081        # We also want to verify that we stayed with v1 discovery
1082        validate_discovery_requests(self, http, "zoo", "v1", V1_DISCOVERY_URI)
1083
1084    def test_v2_repeated_500_retries_and_fails(self):
1085        http = HttpMockSequence(
1086            [
1087                ({"status": "404"}, "Not found"),  # last v1 discovery call
1088                ({"status": "500"}, read_datafile("500.json", "rb")),
1089                ({"status": "503"}, read_datafile("503.json", "rb")),
1090            ]
1091        )
1092        with self.assertRaises(HttpError):
1093            with mock.patch("time.sleep") as mocked_sleep:
1094                build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
1095
1096        mocked_sleep.assert_called_once()
1097        # We also want to verify that we switched to v2 discovery
1098        validate_discovery_requests(self, http, "zoo", "v1", V2_DISCOVERY_URI)
1099
1100    def test_single_500_retries_and_succeeds(self):
1101        http = HttpMockSequence(
1102            [
1103                ({"status": "500"}, read_datafile("500.json", "rb")),
1104                ({"status": "200"}, read_datafile("zoo.json", "rb")),
1105            ]
1106        )
1107        with mock.patch("time.sleep") as mocked_sleep:
1108            zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
1109
1110        self.assertTrue(hasattr(zoo, "animals"))
1111        mocked_sleep.assert_called_once()
1112        # We also want to verify that we stayed with v1 discovery
1113        validate_discovery_requests(self, http, "zoo", "v1", V1_DISCOVERY_URI)
1114
1115    def test_single_500_then_404_retries_and_succeeds(self):
1116        http = HttpMockSequence(
1117            [
1118                ({"status": "500"}, read_datafile("500.json", "rb")),
1119                ({"status": "404"}, "Not found"),  # last v1 discovery call
1120                ({"status": "200"}, read_datafile("zoo.json", "rb")),
1121            ]
1122        )
1123        with mock.patch("time.sleep") as mocked_sleep:
1124            zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
1125
1126        self.assertTrue(hasattr(zoo, "animals"))
1127        mocked_sleep.assert_called_once()
1128        # We also want to verify that we switched to v2 discovery
1129        validate_discovery_requests(self, http, "zoo", "v1", V2_DISCOVERY_URI)
1130
1131
1132class DiscoveryFromAppEngineCache(unittest.TestCase):
1133    def setUp(self):
1134        self.old_environ = os.environ.copy()
1135        os.environ["APPENGINE_RUNTIME"] = "python27"
1136
1137    def tearDown(self):
1138        os.environ = self.old_environ
1139
1140    def test_appengine_memcache(self):
1141        # Hack module import
1142        self.orig_import = __import__
1143        self.mocked_api = mock.MagicMock()
1144
1145        def import_mock(name, *args, **kwargs):
1146            if name == "google.appengine.api":
1147                return self.mocked_api
1148            return self.orig_import(name, *args, **kwargs)
1149
1150        import_fullname = "__builtin__.__import__"
1151        if sys.version_info[0] >= 3:
1152            import_fullname = "builtins.__import__"
1153
1154        with mock.patch(import_fullname, side_effect=import_mock):
1155            namespace = "google-api-client"
1156            self.http = HttpMock(datafile("plus.json"), {"status": "200"})
1157
1158            self.mocked_api.memcache.get.return_value = None
1159
1160            plus = build("plus", "v1", http=self.http, static_discovery=False)
1161
1162            # memcache.get is called once
1163            url = "https://www.googleapis.com/discovery/v1/apis/plus/v1/rest"
1164            self.mocked_api.memcache.get.assert_called_once_with(
1165                url, namespace=namespace
1166            )
1167
1168            # memcache.set is called once
1169            content = read_datafile("plus.json")
1170            self.mocked_api.memcache.set.assert_called_once_with(
1171                url, content, time=DISCOVERY_DOC_MAX_AGE, namespace=namespace
1172            )
1173
1174            # Returns the cached content this time.
1175            self.mocked_api.memcache.get.return_value = content
1176
1177            # Make sure the contents are returned from the cache.
1178            # (Otherwise it should through an error)
1179            self.http = HttpMock(None, {"status": "200"})
1180
1181            plus = build("plus", "v1", http=self.http, static_discovery=False)
1182
1183            # memcache.get is called twice
1184            self.mocked_api.memcache.get.assert_has_calls(
1185                [
1186                    mock.call(url, namespace=namespace),
1187                    mock.call(url, namespace=namespace),
1188                ]
1189            )
1190
1191            # memcahce.set is called just once
1192            self.mocked_api.memcache.set.assert_called_once_with(
1193                url, content, time=DISCOVERY_DOC_MAX_AGE, namespace=namespace
1194            )
1195
1196
1197class DiscoveryFromStaticDocument(unittest.TestCase):
1198    def test_retrieve_from_local_when_static_discovery_true(self):
1199        http = HttpMockSequence([({"status": "400"}, "")])
1200        drive = build("drive", "v3", http=http, cache_discovery=False,
1201                          static_discovery=True)
1202        self.assertIsNotNone(drive)
1203        self.assertTrue(hasattr(drive, "files"))
1204
1205    def test_retrieve_from_internet_when_static_discovery_false(self):
1206        http = HttpMockSequence([({"status": "400"}, "")])
1207        with self.assertRaises(HttpError):
1208            build("drive", "v3", http=http, cache_discovery=False,
1209                      static_discovery=False)
1210
1211    def test_unknown_api_when_static_discovery_true(self):
1212        with self.assertRaises(UnknownApiNameOrVersion):
1213            build("doesnotexist", "v3", cache_discovery=False,
1214                      static_discovery=True)
1215
1216
1217class DictCache(Cache):
1218    def __init__(self):
1219        self.d = {}
1220
1221    def get(self, url):
1222        return self.d.get(url, None)
1223
1224    def set(self, url, content):
1225        self.d[url] = content
1226
1227    def contains(self, url):
1228        return url in self.d
1229
1230
1231class DiscoveryFromFileCache(unittest.TestCase):
1232    def test_file_based_cache(self):
1233        cache = mock.Mock(wraps=DictCache())
1234        with mock.patch(
1235            "googleapiclient.discovery_cache.autodetect", return_value=cache
1236        ):
1237            self.http = HttpMock(datafile("plus.json"), {"status": "200"})
1238
1239            plus = build("plus", "v1", http=self.http, static_discovery=False)
1240
1241            # cache.get is called once
1242            url = "https://www.googleapis.com/discovery/v1/apis/plus/v1/rest"
1243            cache.get.assert_called_once_with(url)
1244
1245            # cache.set is called once
1246            content = read_datafile("plus.json")
1247            cache.set.assert_called_once_with(url, content)
1248
1249            # Make sure there is a cache entry for the plus v1 discovery doc.
1250            self.assertTrue(cache.contains(url))
1251
1252            # Make sure the contents are returned from the cache.
1253            # (Otherwise it should through an error)
1254            self.http = HttpMock(None, {"status": "200"})
1255
1256            plus = build("plus", "v1", http=self.http, static_discovery=False)
1257
1258            # cache.get is called twice
1259            cache.get.assert_has_calls([mock.call(url), mock.call(url)])
1260
1261            # cahce.set is called just once
1262            cache.set.assert_called_once_with(url, content)
1263
1264
1265class Discovery(unittest.TestCase):
1266    def test_method_error_checking(self):
1267        self.http = HttpMock(datafile("plus.json"), {"status": "200"})
1268        plus = build("plus", "v1", http=self.http, static_discovery=False)
1269
1270        # Missing required parameters
1271        try:
1272            plus.activities().list()
1273            self.fail()
1274        except TypeError as e:
1275            self.assertTrue("Missing" in str(e))
1276
1277        # Missing required parameters even if supplied as None.
1278        try:
1279            plus.activities().list(collection=None, userId=None)
1280            self.fail()
1281        except TypeError as e:
1282            self.assertTrue("Missing" in str(e))
1283
1284        # Parameter doesn't match regex
1285        try:
1286            plus.activities().list(collection="not_a_collection_name", userId="me")
1287            self.fail()
1288        except TypeError as e:
1289            self.assertTrue("not an allowed value" in str(e))
1290
1291        # Unexpected parameter
1292        try:
1293            plus.activities().list(flubber=12)
1294            self.fail()
1295        except TypeError as e:
1296            self.assertTrue("unexpected" in str(e))
1297
1298    def _check_query_types(self, request):
1299        parsed = urllib.parse.urlparse(request.uri)
1300        q = urllib.parse.parse_qs(parsed.query)
1301        self.assertEqual(q["q"], ["foo"])
1302        self.assertEqual(q["i"], ["1"])
1303        self.assertEqual(q["n"], ["1.0"])
1304        self.assertEqual(q["b"], ["false"])
1305        self.assertEqual(q["a"], ["[1, 2, 3]"])
1306        self.assertEqual(q["o"], ["{'a': 1}"])
1307        self.assertEqual(q["e"], ["bar"])
1308
1309    def test_type_coercion(self):
1310        http = HttpMock(datafile("zoo.json"), {"status": "200"})
1311        zoo = build("zoo", "v1", http=http, static_discovery=False)
1312
1313        request = zoo.query(
1314            q="foo", i=1.0, n=1.0, b=0, a=[1, 2, 3], o={"a": 1}, e="bar"
1315        )
1316        self._check_query_types(request)
1317        request = zoo.query(
1318            q="foo", i=1, n=1, b=False, a=[1, 2, 3], o={"a": 1}, e="bar"
1319        )
1320        self._check_query_types(request)
1321
1322        request = zoo.query(
1323            q="foo", i="1", n="1", b="", a=[1, 2, 3], o={"a": 1}, e="bar", er="two"
1324        )
1325
1326        request = zoo.query(
1327            q="foo",
1328            i="1",
1329            n="1",
1330            b="",
1331            a=[1, 2, 3],
1332            o={"a": 1},
1333            e="bar",
1334            er=["one", "three"],
1335            rr=["foo", "bar"],
1336        )
1337        self._check_query_types(request)
1338
1339        # Five is right out.
1340        self.assertRaises(TypeError, zoo.query, er=["one", "five"])
1341
1342    def test_optional_stack_query_parameters(self):
1343        http = HttpMock(datafile("zoo.json"), {"status": "200"})
1344        zoo = build("zoo", "v1", http=http, static_discovery=False)
1345        request = zoo.query(trace="html", fields="description")
1346
1347        parsed = urllib.parse.urlparse(request.uri)
1348        q = urllib.parse.parse_qs(parsed.query)
1349        self.assertEqual(q["trace"], ["html"])
1350        self.assertEqual(q["fields"], ["description"])
1351
1352    def test_string_params_value_of_none_get_dropped(self):
1353        http = HttpMock(datafile("zoo.json"), {"status": "200"})
1354        zoo = build("zoo", "v1", http=http, static_discovery=False)
1355        request = zoo.query(trace=None, fields="description")
1356
1357        parsed = urllib.parse.urlparse(request.uri)
1358        q = urllib.parse.parse_qs(parsed.query)
1359        self.assertFalse("trace" in q)
1360
1361    def test_model_added_query_parameters(self):
1362        http = HttpMock(datafile("zoo.json"), {"status": "200"})
1363        zoo = build("zoo", "v1", http=http, static_discovery=False)
1364        request = zoo.animals().get(name="Lion")
1365
1366        parsed = urllib.parse.urlparse(request.uri)
1367        q = urllib.parse.parse_qs(parsed.query)
1368        self.assertEqual(q["alt"], ["json"])
1369        self.assertEqual(request.headers["accept"], "application/json")
1370
1371    def test_fallback_to_raw_model(self):
1372        http = HttpMock(datafile("zoo.json"), {"status": "200"})
1373        zoo = build("zoo", "v1", http=http, static_discovery=False)
1374        request = zoo.animals().getmedia(name="Lion")
1375
1376        parsed = urllib.parse.urlparse(request.uri)
1377        q = urllib.parse.parse_qs(parsed.query)
1378        self.assertTrue("alt" not in q)
1379        self.assertEqual(request.headers["accept"], "*/*")
1380
1381    def test_patch(self):
1382        http = HttpMock(datafile("zoo.json"), {"status": "200"})
1383        zoo = build("zoo", "v1", http=http, static_discovery=False)
1384        request = zoo.animals().patch(name="lion", body='{"description": "foo"}')
1385
1386        self.assertEqual(request.method, "PATCH")
1387
1388    def test_batch_request_from_discovery(self):
1389        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1390        # zoo defines a batchPath
1391        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1392        batch_request = zoo.new_batch_http_request()
1393        self.assertEqual(
1394            batch_request._batch_uri, "https://www.googleapis.com/batchZoo"
1395        )
1396
1397    def test_batch_request_from_default(self):
1398        self.http = HttpMock(datafile("plus.json"), {"status": "200"})
1399        # plus does not define a batchPath
1400        plus = build("plus", "v1", http=self.http, cache_discovery=False, static_discovery=False)
1401        batch_request = plus.new_batch_http_request()
1402        self.assertEqual(batch_request._batch_uri, "https://www.googleapis.com/batch")
1403
1404    def test_tunnel_patch(self):
1405        http = HttpMockSequence(
1406            [
1407                ({"status": "200"}, read_datafile("zoo.json", "rb")),
1408                ({"status": "200"}, "echo_request_headers_as_json"),
1409            ]
1410        )
1411        http = tunnel_patch(http)
1412        zoo = build("zoo", "v1", http=http, cache_discovery=False, static_discovery=False)
1413        resp = zoo.animals().patch(name="lion", body='{"description": "foo"}').execute()
1414
1415        self.assertTrue("x-http-method-override" in resp)
1416
1417    def test_plus_resources(self):
1418        self.http = HttpMock(datafile("plus.json"), {"status": "200"})
1419        plus = build("plus", "v1", http=self.http, static_discovery=False)
1420        self.assertTrue(getattr(plus, "activities"))
1421        self.assertTrue(getattr(plus, "people"))
1422
1423    def test_oauth2client_credentials(self):
1424        credentials = mock.Mock(spec=GoogleCredentials)
1425        credentials.create_scoped_required.return_value = False
1426
1427        discovery = read_datafile("plus.json")
1428        service = build_from_document(discovery, credentials=credentials)
1429        self.assertEqual(service._http, credentials.authorize.return_value)
1430
1431    def test_google_auth_credentials(self):
1432        credentials = mock.Mock(spec=google.auth.credentials.Credentials)
1433        discovery = read_datafile("plus.json")
1434        service = build_from_document(discovery, credentials=credentials)
1435
1436        self.assertIsInstance(service._http, google_auth_httplib2.AuthorizedHttp)
1437        self.assertEqual(service._http.credentials, credentials)
1438
1439    def test_no_scopes_no_credentials(self):
1440        # Zoo doesn't have scopes
1441        discovery = read_datafile("zoo.json")
1442        service = build_from_document(discovery)
1443        # Should be an ordinary httplib2.Http instance and not AuthorizedHttp.
1444        self.assertIsInstance(service._http, httplib2.Http)
1445
1446    def test_full_featured(self):
1447        # Zoo should exercise all discovery facets
1448        # and should also have no future.json file.
1449        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1450        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1451        self.assertTrue(getattr(zoo, "animals"))
1452
1453        request = zoo.animals().list(name="bat", projection="full")
1454        parsed = urllib.parse.urlparse(request.uri)
1455        q = urllib.parse.parse_qs(parsed.query)
1456        self.assertEqual(q["name"], ["bat"])
1457        self.assertEqual(q["projection"], ["full"])
1458
1459    def test_nested_resources(self):
1460        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1461        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1462        self.assertTrue(getattr(zoo, "animals"))
1463        request = zoo.my().favorites().list(max_results="5")
1464        parsed = urllib.parse.urlparse(request.uri)
1465        q = urllib.parse.parse_qs(parsed.query)
1466        self.assertEqual(q["max-results"], ["5"])
1467
1468    def test_top_level_functions(self):
1469        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1470        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1471        self.assertTrue(getattr(zoo, "query"))
1472        request = zoo.query(q="foo")
1473        parsed = urllib.parse.urlparse(request.uri)
1474        q = urllib.parse.parse_qs(parsed.query)
1475        self.assertEqual(q["q"], ["foo"])
1476
1477    def test_simple_media_uploads(self):
1478        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1479        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1480        doc = getattr(zoo.animals().insert, "__doc__")
1481        self.assertTrue("media_body" in doc)
1482
1483    def test_simple_media_upload_no_max_size_provided(self):
1484        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1485        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1486        request = zoo.animals().crossbreed(media_body=datafile("small.png"))
1487        self.assertEqual("image/png", request.headers["content-type"])
1488        self.assertEqual(b"PNG", request.body[1:4])
1489
1490    def test_simple_media_raise_correct_exceptions(self):
1491        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1492        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1493
1494        try:
1495            zoo.animals().insert(media_body=datafile("smiley.png"))
1496            self.fail("should throw exception if media is too large.")
1497        except MediaUploadSizeError:
1498            pass
1499
1500        try:
1501            zoo.animals().insert(media_body=datafile("small.jpg"))
1502            self.fail("should throw exception if mimetype is unacceptable.")
1503        except UnacceptableMimeTypeError:
1504            pass
1505
1506    def test_simple_media_good_upload(self):
1507        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1508        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1509
1510        request = zoo.animals().insert(media_body=datafile("small.png"))
1511        self.assertEqual("image/png", request.headers["content-type"])
1512        self.assertEqual(b"PNG", request.body[1:4])
1513        assertUrisEqual(
1514            self,
1515            "https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json",
1516            request.uri,
1517        )
1518
1519    def test_simple_media_unknown_mimetype(self):
1520        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1521        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1522
1523        try:
1524            zoo.animals().insert(media_body=datafile("small-png"))
1525            self.fail("should throw exception if mimetype is unknown.")
1526        except UnknownFileType:
1527            pass
1528
1529        request = zoo.animals().insert(
1530            media_body=datafile("small-png"), media_mime_type="image/png"
1531        )
1532        self.assertEqual("image/png", request.headers["content-type"])
1533        self.assertEqual(b"PNG", request.body[1:4])
1534        assertUrisEqual(
1535            self,
1536            "https://www.googleapis.com/upload/zoo/v1/animals?uploadType=media&alt=json",
1537            request.uri,
1538        )
1539
1540    def test_multipart_media_raise_correct_exceptions(self):
1541        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1542        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1543
1544        try:
1545            zoo.animals().insert(media_body=datafile("smiley.png"), body={})
1546            self.fail("should throw exception if media is too large.")
1547        except MediaUploadSizeError:
1548            pass
1549
1550        try:
1551            zoo.animals().insert(media_body=datafile("small.jpg"), body={})
1552            self.fail("should throw exception if mimetype is unacceptable.")
1553        except UnacceptableMimeTypeError:
1554            pass
1555
1556    def test_multipart_media_good_upload(self, static_discovery=False):
1557        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1558        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1559
1560        request = zoo.animals().insert(media_body=datafile("small.png"), body={})
1561        self.assertTrue(request.headers["content-type"].startswith("multipart/related"))
1562        contents = read_datafile("small.png", "rb")
1563        boundary = re.match(b"--=+([^=]+)", request.body).group(1)
1564        self.assertEqual(
1565            request.body.rstrip(b"\n"),  # Python 2.6 does not add a trailing \n
1566            b"--==============="
1567            + boundary
1568            + b"==\n"
1569            + b"Content-Type: application/json\n"
1570            + b"MIME-Version: 1.0\n\n"
1571            + b'{"data": {}}\n'
1572            + b"--==============="
1573            + boundary
1574            + b"==\n"
1575            + b"Content-Type: image/png\n"
1576            + b"MIME-Version: 1.0\n"
1577            + b"Content-Transfer-Encoding: binary\n\n"
1578            + contents
1579            + b"\n--==============="
1580            + boundary
1581            + b"==--",
1582        )
1583        assertUrisEqual(
1584            self,
1585            "https://www.googleapis.com/upload/zoo/v1/animals?uploadType=multipart&alt=json",
1586            request.uri,
1587        )
1588
1589    def test_media_capable_method_without_media(self):
1590        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1591        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1592
1593        request = zoo.animals().insert(body={})
1594        self.assertTrue(request.headers["content-type"], "application/json")
1595
1596    def test_resumable_multipart_media_good_upload(self):
1597        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1598        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1599
1600        media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
1601        request = zoo.animals().insert(media_body=media_upload, body={})
1602        self.assertTrue(request.headers["content-type"].startswith("application/json"))
1603        self.assertEqual('{"data": {}}', request.body)
1604        self.assertEqual(media_upload, request.resumable)
1605
1606        self.assertEqual("image/png", request.resumable.mimetype())
1607
1608        self.assertNotEqual(request.body, None)
1609        self.assertEqual(request.resumable_uri, None)
1610
1611        http = HttpMockSequence(
1612            [
1613                ({"status": "200", "location": "http://upload.example.com"}, ""),
1614                ({"status": "308", "location": "http://upload.example.com/2"}, ""),
1615                (
1616                    {
1617                        "status": "308",
1618                        "location": "http://upload.example.com/3",
1619                        "range": "0-12",
1620                    },
1621                    "",
1622                ),
1623                (
1624                    {
1625                        "status": "308",
1626                        "location": "http://upload.example.com/4",
1627                        "range": "0-%d" % (media_upload.size() - 2),
1628                    },
1629                    "",
1630                ),
1631                ({"status": "200"}, '{"foo": "bar"}'),
1632            ]
1633        )
1634
1635        status, body = request.next_chunk(http=http)
1636        self.assertEqual(None, body)
1637        self.assertTrue(isinstance(status, MediaUploadProgress))
1638        self.assertEqual(0, status.resumable_progress)
1639
1640        # Two requests should have been made and the resumable_uri should have been
1641        # updated for each one.
1642        self.assertEqual(request.resumable_uri, "http://upload.example.com/2")
1643        self.assertEqual(media_upload, request.resumable)
1644        self.assertEqual(0, request.resumable_progress)
1645
1646        # This next chuck call should upload the first chunk
1647        status, body = request.next_chunk(http=http)
1648        self.assertEqual(request.resumable_uri, "http://upload.example.com/3")
1649        self.assertEqual(media_upload, request.resumable)
1650        self.assertEqual(13, request.resumable_progress)
1651
1652        # This call will upload the next chunk
1653        status, body = request.next_chunk(http=http)
1654        self.assertEqual(request.resumable_uri, "http://upload.example.com/4")
1655        self.assertEqual(media_upload.size() - 1, request.resumable_progress)
1656        self.assertEqual('{"data": {}}', request.body)
1657
1658        # Final call to next_chunk should complete the upload.
1659        status, body = request.next_chunk(http=http)
1660        self.assertEqual(body, {"foo": "bar"})
1661        self.assertEqual(status, None)
1662
1663    def test_resumable_media_good_upload(self):
1664        """Not a multipart upload."""
1665        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1666        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1667
1668        media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
1669        request = zoo.animals().insert(media_body=media_upload, body=None)
1670        self.assertEqual(media_upload, request.resumable)
1671
1672        self.assertEqual("image/png", request.resumable.mimetype())
1673
1674        self.assertEqual(request.body, None)
1675        self.assertEqual(request.resumable_uri, None)
1676
1677        http = HttpMockSequence(
1678            [
1679                ({"status": "200", "location": "http://upload.example.com"}, ""),
1680                (
1681                    {
1682                        "status": "308",
1683                        "location": "http://upload.example.com/2",
1684                        "range": "0-12",
1685                    },
1686                    "",
1687                ),
1688                (
1689                    {
1690                        "status": "308",
1691                        "location": "http://upload.example.com/3",
1692                        "range": "0-%d" % (media_upload.size() - 2),
1693                    },
1694                    "",
1695                ),
1696                ({"status": "200"}, '{"foo": "bar"}'),
1697            ]
1698        )
1699
1700        status, body = request.next_chunk(http=http)
1701        self.assertEqual(None, body)
1702        self.assertTrue(isinstance(status, MediaUploadProgress))
1703        self.assertEqual(13, status.resumable_progress)
1704
1705        # Two requests should have been made and the resumable_uri should have been
1706        # updated for each one.
1707        self.assertEqual(request.resumable_uri, "http://upload.example.com/2")
1708
1709        self.assertEqual(media_upload, request.resumable)
1710        self.assertEqual(13, request.resumable_progress)
1711
1712        status, body = request.next_chunk(http=http)
1713        self.assertEqual(request.resumable_uri, "http://upload.example.com/3")
1714        self.assertEqual(media_upload.size() - 1, request.resumable_progress)
1715        self.assertEqual(request.body, None)
1716
1717        # Final call to next_chunk should complete the upload.
1718        status, body = request.next_chunk(http=http)
1719        self.assertEqual(body, {"foo": "bar"})
1720        self.assertEqual(status, None)
1721
1722    def test_resumable_media_good_upload_from_execute(self):
1723        """Not a multipart upload."""
1724        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1725        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1726
1727        media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
1728        request = zoo.animals().insert(media_body=media_upload, body=None)
1729        assertUrisEqual(
1730            self,
1731            "https://www.googleapis.com/upload/zoo/v1/animals?uploadType=resumable&alt=json",
1732            request.uri,
1733        )
1734
1735        http = HttpMockSequence(
1736            [
1737                ({"status": "200", "location": "http://upload.example.com"}, ""),
1738                (
1739                    {
1740                        "status": "308",
1741                        "location": "http://upload.example.com/2",
1742                        "range": "0-12",
1743                    },
1744                    "",
1745                ),
1746                (
1747                    {
1748                        "status": "308",
1749                        "location": "http://upload.example.com/3",
1750                        "range": "0-%d" % media_upload.size(),
1751                    },
1752                    "",
1753                ),
1754                ({"status": "200"}, '{"foo": "bar"}'),
1755            ]
1756        )
1757
1758        body = request.execute(http=http)
1759        self.assertEqual(body, {"foo": "bar"})
1760
1761    def test_resumable_media_fail_unknown_response_code_first_request(self):
1762        """Not a multipart upload."""
1763        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1764        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1765
1766        media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
1767        request = zoo.animals().insert(media_body=media_upload, body=None)
1768
1769        http = HttpMockSequence(
1770            [({"status": "400", "location": "http://upload.example.com"}, "")]
1771        )
1772
1773        try:
1774            request.execute(http=http)
1775            self.fail("Should have raised ResumableUploadError.")
1776        except ResumableUploadError as e:
1777            self.assertEqual(400, e.resp.status)
1778
1779    def test_resumable_media_fail_unknown_response_code_subsequent_request(self):
1780        """Not a multipart upload."""
1781        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1782        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1783
1784        media_upload = MediaFileUpload(datafile("small.png"), resumable=True)
1785        request = zoo.animals().insert(media_body=media_upload, body=None)
1786
1787        http = HttpMockSequence(
1788            [
1789                ({"status": "200", "location": "http://upload.example.com"}, ""),
1790                ({"status": "400"}, ""),
1791            ]
1792        )
1793
1794        self.assertRaises(HttpError, request.execute, http=http)
1795        self.assertTrue(request._in_error_state)
1796
1797        http = HttpMockSequence(
1798            [
1799                ({"status": "308", "range": "0-5"}, ""),
1800                ({"status": "308", "range": "0-6"}, ""),
1801            ]
1802        )
1803
1804        status, body = request.next_chunk(http=http)
1805        self.assertEqual(
1806            status.resumable_progress,
1807            7,
1808            "Should have first checked length and then tried to PUT more.",
1809        )
1810        self.assertFalse(request._in_error_state)
1811
1812        # Put it back in an error state.
1813        http = HttpMockSequence([({"status": "400"}, "")])
1814        self.assertRaises(HttpError, request.execute, http=http)
1815        self.assertTrue(request._in_error_state)
1816
1817        # Pretend the last request that 400'd actually succeeded.
1818        http = HttpMockSequence([({"status": "200"}, '{"foo": "bar"}')])
1819        status, body = request.next_chunk(http=http)
1820        self.assertEqual(body, {"foo": "bar"})
1821
1822    def test_media_io_base_stream_unlimited_chunksize_resume(self):
1823        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1824        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1825
1826        # Set up a seekable stream and try to upload in single chunk.
1827        fd = io.BytesIO(b'01234"56789"')
1828        media_upload = MediaIoBaseUpload(
1829            fd=fd, mimetype="text/plain", chunksize=-1, resumable=True
1830        )
1831
1832        request = zoo.animals().insert(media_body=media_upload, body=None)
1833
1834        # The single chunk fails, restart at the right point.
1835        http = HttpMockSequence(
1836            [
1837                ({"status": "200", "location": "http://upload.example.com"}, ""),
1838                (
1839                    {
1840                        "status": "308",
1841                        "location": "http://upload.example.com/2",
1842                        "range": "0-4",
1843                    },
1844                    "",
1845                ),
1846                ({"status": "200"}, "echo_request_body"),
1847            ]
1848        )
1849
1850        body = request.execute(http=http)
1851        self.assertEqual("56789", body)
1852
1853    def test_media_io_base_stream_chunksize_resume(self):
1854        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1855        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1856
1857        # Set up a seekable stream and try to upload in chunks.
1858        fd = io.BytesIO(b"0123456789")
1859        media_upload = MediaIoBaseUpload(
1860            fd=fd, mimetype="text/plain", chunksize=5, resumable=True
1861        )
1862
1863        request = zoo.animals().insert(media_body=media_upload, body=None)
1864
1865        # The single chunk fails, pull the content sent out of the exception.
1866        http = HttpMockSequence(
1867            [
1868                ({"status": "200", "location": "http://upload.example.com"}, ""),
1869                ({"status": "400"}, "echo_request_body"),
1870            ]
1871        )
1872
1873        try:
1874            body = request.execute(http=http)
1875        except HttpError as e:
1876            self.assertEqual(b"01234", e.content)
1877
1878    def test_resumable_media_handle_uploads_of_unknown_size(self):
1879        http = HttpMockSequence(
1880            [
1881                ({"status": "200", "location": "http://upload.example.com"}, ""),
1882                ({"status": "200"}, "echo_request_headers_as_json"),
1883            ]
1884        )
1885
1886        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1887        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1888
1889        # Create an upload that doesn't know the full size of the media.
1890        class IoBaseUnknownLength(MediaUpload):
1891            def chunksize(self):
1892                return 10
1893
1894            def mimetype(self):
1895                return "image/png"
1896
1897            def size(self):
1898                return None
1899
1900            def resumable(self):
1901                return True
1902
1903            def getbytes(self, begin, length):
1904                return "0123456789"
1905
1906        upload = IoBaseUnknownLength()
1907
1908        request = zoo.animals().insert(media_body=upload, body=None)
1909        status, body = request.next_chunk(http=http)
1910        self.assertEqual(body, {"Content-Range": "bytes 0-9/*", "Content-Length": "10"})
1911
1912    def test_resumable_media_no_streaming_on_unsupported_platforms(self):
1913        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1914        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1915
1916        class IoBaseHasStream(MediaUpload):
1917            def chunksize(self):
1918                return 10
1919
1920            def mimetype(self):
1921                return "image/png"
1922
1923            def size(self):
1924                return None
1925
1926            def resumable(self):
1927                return True
1928
1929            def getbytes(self, begin, length):
1930                return "0123456789"
1931
1932            def has_stream(self):
1933                return True
1934
1935            def stream(self):
1936                raise NotImplementedError()
1937
1938        upload = IoBaseHasStream()
1939
1940        orig_version = sys.version_info
1941
1942        sys.version_info = (2, 6, 5, "final", 0)
1943
1944        request = zoo.animals().insert(media_body=upload, body=None)
1945
1946        # This should raise an exception because stream() will be called.
1947        http = HttpMockSequence(
1948            [
1949                ({"status": "200", "location": "http://upload.example.com"}, ""),
1950                ({"status": "200"}, "echo_request_headers_as_json"),
1951            ]
1952        )
1953
1954        self.assertRaises(NotImplementedError, request.next_chunk, http=http)
1955
1956        sys.version_info = orig_version
1957
1958    def test_resumable_media_handle_uploads_of_unknown_size_eof(self):
1959        http = HttpMockSequence(
1960            [
1961                ({"status": "200", "location": "http://upload.example.com"}, ""),
1962                ({"status": "200"}, "echo_request_headers_as_json"),
1963            ]
1964        )
1965
1966        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1967        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1968
1969        fd = io.BytesIO(b"data goes here")
1970
1971        # Create an upload that doesn't know the full size of the media.
1972        upload = MediaIoBaseUpload(
1973            fd=fd, mimetype="image/png", chunksize=15, resumable=True
1974        )
1975
1976        request = zoo.animals().insert(media_body=upload, body=None)
1977        status, body = request.next_chunk(http=http)
1978        self.assertEqual(
1979            body, {"Content-Range": "bytes 0-13/14", "Content-Length": "14"}
1980        )
1981
1982    def test_resumable_media_handle_resume_of_upload_of_unknown_size(self):
1983        http = HttpMockSequence(
1984            [
1985                ({"status": "200", "location": "http://upload.example.com"}, ""),
1986                ({"status": "400"}, ""),
1987            ]
1988        )
1989
1990        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
1991        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
1992
1993        # Create an upload that doesn't know the full size of the media.
1994        fd = io.BytesIO(b"data goes here")
1995
1996        upload = MediaIoBaseUpload(
1997            fd=fd, mimetype="image/png", chunksize=500, resumable=True
1998        )
1999
2000        request = zoo.animals().insert(media_body=upload, body=None)
2001
2002        # Put it in an error state.
2003        self.assertRaises(HttpError, request.next_chunk, http=http)
2004
2005        http = HttpMockSequence(
2006            [({"status": "400", "range": "0-5"}, "echo_request_headers_as_json")]
2007        )
2008        try:
2009            # Should resume the upload by first querying the status of the upload.
2010            request.next_chunk(http=http)
2011        except HttpError as e:
2012            expected = {"Content-Range": "bytes */14", "content-length": "0"}
2013            self.assertEqual(
2014                expected,
2015                json.loads(e.content.decode("utf-8")),
2016                "Should send an empty body when requesting the current upload status.",
2017            )
2018
2019    def test_pickle(self):
2020        sorted_resource_keys = [
2021            "_baseUrl",
2022            "_developerKey",
2023            "_dynamic_attrs",
2024            "_http",
2025            "_model",
2026            "_requestBuilder",
2027            "_resourceDesc",
2028            "_rootDesc",
2029            "_schema",
2030            "animals",
2031            "global_",
2032            "load",
2033            "loadNoTemplate",
2034            "my",
2035            "new_batch_http_request",
2036            "query",
2037            "scopedAnimals",
2038        ]
2039
2040        http = HttpMock(datafile("zoo.json"), {"status": "200"})
2041        zoo = build("zoo", "v1", http=http, static_discovery=False)
2042        self.assertEqual(sorted(zoo.__dict__.keys()), sorted_resource_keys)
2043
2044        pickled_zoo = pickle.dumps(zoo)
2045        new_zoo = pickle.loads(pickled_zoo)
2046        self.assertEqual(sorted(new_zoo.__dict__.keys()), sorted_resource_keys)
2047        self.assertTrue(hasattr(new_zoo, "animals"))
2048        self.assertTrue(callable(new_zoo.animals))
2049        self.assertTrue(hasattr(new_zoo, "global_"))
2050        self.assertTrue(callable(new_zoo.global_))
2051        self.assertTrue(hasattr(new_zoo, "load"))
2052        self.assertTrue(callable(new_zoo.load))
2053        self.assertTrue(hasattr(new_zoo, "loadNoTemplate"))
2054        self.assertTrue(callable(new_zoo.loadNoTemplate))
2055        self.assertTrue(hasattr(new_zoo, "my"))
2056        self.assertTrue(callable(new_zoo.my))
2057        self.assertTrue(hasattr(new_zoo, "query"))
2058        self.assertTrue(callable(new_zoo.query))
2059        self.assertTrue(hasattr(new_zoo, "scopedAnimals"))
2060        self.assertTrue(callable(new_zoo.scopedAnimals))
2061
2062        self.assertEqual(sorted(zoo._dynamic_attrs), sorted(new_zoo._dynamic_attrs))
2063        self.assertEqual(zoo._baseUrl, new_zoo._baseUrl)
2064        self.assertEqual(zoo._developerKey, new_zoo._developerKey)
2065        self.assertEqual(zoo._requestBuilder, new_zoo._requestBuilder)
2066        self.assertEqual(zoo._resourceDesc, new_zoo._resourceDesc)
2067        self.assertEqual(zoo._rootDesc, new_zoo._rootDesc)
2068        # _http, _model and _schema won't be equal since we will get new
2069        # instances upon un-pickling
2070
2071    def _dummy_zoo_request(self):
2072        zoo_contents = read_datafile("zoo.json")
2073
2074        zoo_uri = uritemplate.expand(DISCOVERY_URI, {"api": "zoo", "apiVersion": "v1"})
2075        if "REMOTE_ADDR" in os.environ:
2076            zoo_uri = util._add_query_parameter(
2077                zoo_uri, "userIp", os.environ["REMOTE_ADDR"]
2078            )
2079
2080        http = build_http()
2081        original_request = http.request
2082
2083        def wrapped_request(uri, method="GET", *args, **kwargs):
2084            if uri == zoo_uri:
2085                return httplib2.Response({"status": "200"}), zoo_contents
2086            return original_request(uri, method=method, *args, **kwargs)
2087
2088        http.request = wrapped_request
2089        return http
2090
2091    def _dummy_token(self):
2092        access_token = "foo"
2093        client_id = "some_client_id"
2094        client_secret = "cOuDdkfjxxnv+"
2095        refresh_token = "1/0/a.df219fjls0"
2096        token_expiry = datetime.datetime.utcnow()
2097        user_agent = "refresh_checker/1.0"
2098        return OAuth2Credentials(
2099            access_token,
2100            client_id,
2101            client_secret,
2102            refresh_token,
2103            token_expiry,
2104            GOOGLE_TOKEN_URI,
2105            user_agent,
2106        )
2107
2108    def test_pickle_with_credentials(self):
2109        credentials = self._dummy_token()
2110        http = self._dummy_zoo_request()
2111        http = credentials.authorize(http)
2112        self.assertTrue(hasattr(http.request, "credentials"))
2113
2114        zoo = build("zoo", "v1", http=http, static_discovery=False)
2115        pickled_zoo = pickle.dumps(zoo)
2116        new_zoo = pickle.loads(pickled_zoo)
2117        self.assertEqual(sorted(zoo.__dict__.keys()), sorted(new_zoo.__dict__.keys()))
2118        new_http = new_zoo._http
2119        self.assertFalse(hasattr(new_http.request, "credentials"))
2120
2121    def test_resumable_media_upload_no_content(self):
2122        self.http = HttpMock(datafile("zoo.json"), {"status": "200"})
2123        zoo = build("zoo", "v1", http=self.http, static_discovery=False)
2124
2125        media_upload = MediaFileUpload(datafile("empty"), resumable=True)
2126        request = zoo.animals().insert(media_body=media_upload, body=None)
2127
2128        self.assertEqual(media_upload, request.resumable)
2129        self.assertEqual(request.body, None)
2130        self.assertEqual(request.resumable_uri, None)
2131
2132        http = HttpMockSequence(
2133            [
2134                ({"status": "200", "location": "http://upload.example.com"}, ""),
2135                (
2136                    {
2137                        "status": "308",
2138                        "location": "http://upload.example.com/2",
2139                        "range": "0-0",
2140                    },
2141                    "",
2142                ),
2143            ]
2144        )
2145
2146        status, body = request.next_chunk(http=http)
2147        self.assertEqual(None, body)
2148        self.assertTrue(isinstance(status, MediaUploadProgress))
2149        self.assertEqual(0, status.progress())
2150
2151
2152class Next(unittest.TestCase):
2153    def test_next_successful_none_on_no_next_page_token(self):
2154        self.http = HttpMock(datafile("tasks.json"), {"status": "200"})
2155        tasks = build("tasks", "v1", http=self.http)
2156        request = tasks.tasklists().list()
2157        self.assertEqual(None, tasks.tasklists().list_next(request, {}))
2158
2159    def test_next_successful_none_on_empty_page_token(self):
2160        self.http = HttpMock(datafile("tasks.json"), {"status": "200"})
2161        tasks = build("tasks", "v1", http=self.http)
2162        request = tasks.tasklists().list()
2163        next_request = tasks.tasklists().list_next(request, {"nextPageToken": ""})
2164        self.assertEqual(None, next_request)
2165
2166    def test_next_successful_with_next_page_token(self):
2167        self.http = HttpMock(datafile("tasks.json"), {"status": "200"})
2168        tasks = build("tasks", "v1", http=self.http)
2169        request = tasks.tasklists().list()
2170        next_request = tasks.tasklists().list_next(request, {"nextPageToken": "123abc"})
2171        parsed = urllib.parse.urlparse(next_request.uri)
2172        q = urllib.parse.parse_qs(parsed.query)
2173        self.assertEqual(q["pageToken"][0], "123abc")
2174
2175    def test_next_successful_with_next_page_token_alternate_name(self):
2176        self.http = HttpMock(datafile("bigquery.json"), {"status": "200"})
2177        bigquery = build("bigquery", "v2", http=self.http)
2178        request = bigquery.tabledata().list(datasetId="", projectId="", tableId="")
2179        next_request = bigquery.tabledata().list_next(request, {"pageToken": "123abc"})
2180        parsed = urllib.parse.urlparse(next_request.uri)
2181        q = urllib.parse.parse_qs(parsed.query)
2182        self.assertEqual(q["pageToken"][0], "123abc")
2183
2184    def test_next_successful_with_next_page_token_in_body(self):
2185        self.http = HttpMock(datafile("logging.json"), {"status": "200"})
2186        logging = build("logging", "v2", http=self.http)
2187        request = logging.entries().list(body={})
2188        next_request = logging.entries().list_next(request, {"nextPageToken": "123abc"})
2189        body = JsonModel().deserialize(next_request.body)
2190        self.assertEqual(body["pageToken"], "123abc")
2191        # The body is changed, make sure that body_length is changed too (see
2192        # github #1403)
2193        self.assertEqual(next_request.body_size, len(next_request.body))
2194
2195    def test_next_with_method_with_no_properties(self):
2196        self.http = HttpMock(datafile("latitude.json"), {"status": "200"})
2197        service = build("latitude", "v1", http=self.http, static_discovery=False)
2198        service.currentLocation().get()
2199
2200    def test_next_nonexistent_with_no_next_page_token(self):
2201        self.http = HttpMock(datafile("drive.json"), {"status": "200"})
2202        drive = build("drive", "v3", http=self.http)
2203        drive.changes().watch(body={})
2204        self.assertFalse(callable(getattr(drive.changes(), "watch_next", None)))
2205
2206    def test_next_successful_with_next_page_token_required(self):
2207        self.http = HttpMock(datafile("drive.json"), {"status": "200"})
2208        drive = build("drive", "v3", http=self.http)
2209        request = drive.changes().list(pageToken="startPageToken")
2210        next_request = drive.changes().list_next(request, {"nextPageToken": "123abc"})
2211        parsed = urllib.parse.urlparse(next_request.uri)
2212        q = urllib.parse.parse_qs(parsed.query)
2213        self.assertEqual(q["pageToken"][0], "123abc")
2214
2215
2216class MediaGet(unittest.TestCase):
2217    def test_get_media(self):
2218        http = HttpMock(datafile("zoo.json"), {"status": "200"})
2219        zoo = build("zoo", "v1", http=http, static_discovery=False)
2220        request = zoo.animals().get_media(name="Lion")
2221
2222        parsed = urllib.parse.urlparse(request.uri)
2223        q = urllib.parse.parse_qs(parsed.query)
2224        self.assertEqual(q["alt"], ["media"])
2225        self.assertEqual(request.headers["accept"], "*/*")
2226
2227        http = HttpMockSequence([({"status": "200"}, "standing in for media")])
2228        response = request.execute(http=http)
2229        self.assertEqual(b"standing in for media", response)
2230
2231
2232if __name__ == "__main__":
2233    unittest.main()
2234