Codebase list google-auth-oauthlib / 588b261
Update upstream source from tag 'upstream/0.4.2' Update to upstream version '0.4.2' with Debian dir eae626092763c1b3f3c8b08e0fcd1890a773d0f0 Shayan Doust 3 years ago
20 changed file(s) with 907 addition(s) and 814 deletion(s). Raw diff Collapse all Expand all
0 Apache License
0 Apache License
11 Version 2.0, January 2004
2 http://www.apache.org/licenses/
2 https://www.apache.org/licenses/
33
44 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
55
191191 you may not use this file except in compliance with the License.
192192 You may obtain a copy of the License at
193193
194 http://www.apache.org/licenses/LICENSE-2.0
194 https://www.apache.org/licenses/LICENSE-2.0
195195
196196 Unless required by applicable law or agreed to in writing, software
197197 distributed under the License is distributed on an "AS IS" BASIS,
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright 2020 Google LLC
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # https://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 # Generated by synthtool. DO NOT EDIT!
017 include README.rst LICENSE
18 recursive-include google *.json *.proto
119 recursive-include tests *
20 global-exclude *.py[co]
21 global-exclude __pycache__
22
23 # Exclude scripts for samples readmegen
24 prune scripts/readme-gen
00 Metadata-Version: 2.1
11 Name: google-auth-oauthlib
2 Version: 0.4.1
2 Version: 0.4.2
33 Summary: Google Authentication Library
44 Home-page: https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib
55 Author: Google Cloud Platform
4747
4848 Keywords: google auth oauth client oauthlib
4949 Platform: UNKNOWN
50 Classifier: Programming Language :: Python :: 2
51 Classifier: Programming Language :: Python :: 2.7
5250 Classifier: Programming Language :: Python :: 3
53 Classifier: Programming Language :: Python :: 3.4
54 Classifier: Programming Language :: Python :: 3.5
5551 Classifier: Programming Language :: Python :: 3.6
52 Classifier: Programming Language :: Python :: 3.7
53 Classifier: Programming Language :: Python :: 3.8
54 Classifier: Programming Language :: Python :: 3.9
5655 Classifier: Development Status :: 3 - Alpha
5756 Classifier: Intended Audience :: Developers
5857 Classifier: License :: OSI Approved :: Apache Software License
6160 Classifier: Operating System :: MacOS :: MacOS X
6261 Classifier: Operating System :: OS Independent
6362 Classifier: Topic :: Internet :: WWW/HTTP
63 Requires-Python: >=3.6
6464 Provides-Extra: tool
5454 import hashlib
5555 import json
5656 import logging
57
5758 try:
5859 from secrets import SystemRandom
5960 except ImportError: # pragma: NO COVER
9394 """
9495
9596 def __init__(
96 self, oauth2session, client_type, client_config,
97 redirect_uri=None, code_verifier=None,
98 autogenerate_code_verifier=False):
97 self,
98 oauth2session,
99 client_type,
100 client_config,
101 redirect_uri=None,
102 code_verifier=None,
103 autogenerate_code_verifier=False,
104 ):
99105 """
100106 Args:
101107 oauth2session (requests_oauthlib.OAuth2Session):
149155 https://developers.google.com/api-client-library/python/guide
150156 /aaa_client_secrets
151157 """
152 if 'web' in client_config:
153 client_type = 'web'
154 elif 'installed' in client_config:
155 client_type = 'installed'
158 if "web" in client_config:
159 client_type = "web"
160 elif "installed" in client_config:
161 client_type = "installed"
156162 else:
157 raise ValueError(
158 'Client secrets must be for a web or installed app.')
163 raise ValueError("Client secrets must be for a web or installed app.")
159164
160165 # these args cannot be passed to requests_oauthlib.OAuth2Session
161 code_verifier = kwargs.pop('code_verifier', None)
162 autogenerate_code_verifier = kwargs.pop(
163 'autogenerate_code_verifier', None)
164
165 session, client_config = (
166 google_auth_oauthlib.helpers.session_from_client_config(
167 client_config, scopes, **kwargs))
168
169 redirect_uri = kwargs.get('redirect_uri', None)
166 code_verifier = kwargs.pop("code_verifier", None)
167 autogenerate_code_verifier = kwargs.pop("autogenerate_code_verifier", None)
168
169 (
170 session,
171 client_config,
172 ) = google_auth_oauthlib.helpers.session_from_client_config(
173 client_config, scopes, **kwargs
174 )
175
176 redirect_uri = kwargs.get("redirect_uri", None)
170177
171178 return cls(
172179 session,
174181 client_config,
175182 redirect_uri,
176183 code_verifier,
177 autogenerate_code_verifier
184 autogenerate_code_verifier,
178185 )
179186
180187 @classmethod
192199 Returns:
193200 Flow: The constructed Flow instance.
194201 """
195 with open(client_secrets_file, 'r') as json_file:
202 with open(client_secrets_file, "r") as json_file:
196203 client_config = json.load(json_file)
197204
198205 return cls.from_client_config(client_config, scopes=scopes, **kwargs)
231238 :class:`Flow` instance to obtain the token, you will need to
232239 specify the ``state`` when constructing the :class:`Flow`.
233240 """
234 kwargs.setdefault('access_type', 'offline')
241 kwargs.setdefault("access_type", "offline")
235242 if self.autogenerate_code_verifier:
236 chars = ascii_letters+digits+'-._~'
243 chars = ascii_letters + digits + "-._~"
237244 rnd = SystemRandom()
238245 random_verifier = [rnd.choice(chars) for _ in range(0, 128)]
239 self.code_verifier = ''.join(random_verifier)
246 self.code_verifier = "".join(random_verifier)
240247
241248 if self.code_verifier:
242249 code_hash = hashlib.sha256()
243250 code_hash.update(str.encode(self.code_verifier))
244251 unencoded_challenge = code_hash.digest()
245252 b64_challenge = urlsafe_b64encode(unencoded_challenge)
246 code_challenge = b64_challenge.decode().split('=')[0]
247 kwargs.setdefault('code_challenge', code_challenge)
248 kwargs.setdefault('code_challenge_method', 'S256')
253 code_challenge = b64_challenge.decode().split("=")[0]
254 kwargs.setdefault("code_challenge", code_challenge)
255 kwargs.setdefault("code_challenge_method", "S256")
249256 url, state = self.oauth2session.authorization_url(
250 self.client_config['auth_uri'], **kwargs)
257 self.client_config["auth_uri"], **kwargs
258 )
251259
252260 return url, state
253261
274282 :meth:`credentials` to obtain a
275283 :class:`~google.auth.credentials.Credentials` instance.
276284 """
277 kwargs.setdefault('client_secret', self.client_config['client_secret'])
278 kwargs.setdefault('code_verifier', self.code_verifier)
279 return self.oauth2session.fetch_token(
280 self.client_config['token_uri'], **kwargs)
285 kwargs.setdefault("client_secret", self.client_config["client_secret"])
286 kwargs.setdefault("code_verifier", self.code_verifier)
287 return self.oauth2session.fetch_token(self.client_config["token_uri"], **kwargs)
281288
282289 @property
283290 def credentials(self):
294301 ValueError: If there is no access token in the session.
295302 """
296303 return google_auth_oauthlib.helpers.credentials_from_session(
297 self.oauth2session, self.client_config)
304 self.oauth2session, self.client_config
305 )
298306
299307 def authorized_session(self):
300308 """Returns a :class:`requests.Session` authorized with credentials.
307315 google.auth.transport.requests.AuthorizedSession: The constructed
308316 session.
309317 """
310 return google.auth.transport.requests.AuthorizedSession(
311 self.credentials)
318 return google.auth.transport.requests.AuthorizedSession(self.credentials)
312319
313320
314321 class InstalledAppFlow(Flow):
352359 https://developers.google.com/api-client-library/python/auth
353360 /installed-app
354361 """
355 _OOB_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
362
363 _OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
356364
357365 _DEFAULT_AUTH_PROMPT_MESSAGE = (
358 'Please visit this URL to authorize this application: {url}')
366 "Please visit this URL to authorize this application: {url}"
367 )
359368 """str: The message to display when prompting the user for
360369 authorization."""
361 _DEFAULT_AUTH_CODE_MESSAGE = (
362 'Enter the authorization code: ')
370 _DEFAULT_AUTH_CODE_MESSAGE = "Enter the authorization code: "
363371 """str: The message to display when prompting the user for the
364372 authorization code. Used only by the console strategy."""
365373
366374 _DEFAULT_WEB_SUCCESS_MESSAGE = (
367 'The authentication flow has completed. You may close this window.')
375 "The authentication flow has completed. You may close this window."
376 )
368377
369378 def run_console(
370 self,
371 authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
372 authorization_code_message=_DEFAULT_AUTH_CODE_MESSAGE,
373 **kwargs):
379 self,
380 authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
381 authorization_code_message=_DEFAULT_AUTH_CODE_MESSAGE,
382 **kwargs
383 ):
374384 """Run the flow using the console strategy.
375385
376386 The console strategy instructs the user to open the authorization URL
390400 google.oauth2.credentials.Credentials: The OAuth 2.0 credentials
391401 for the user.
392402 """
393 kwargs.setdefault('prompt', 'consent')
403 kwargs.setdefault("prompt", "consent")
394404
395405 self.redirect_uri = self._OOB_REDIRECT_URI
396406
405415 return self.credentials
406416
407417 def run_local_server(
408 self, host='localhost', port=8080,
409 authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
410 success_message=_DEFAULT_WEB_SUCCESS_MESSAGE,
411 open_browser=True,
412 **kwargs):
418 self,
419 host="localhost",
420 port=8080,
421 authorization_prompt_message=_DEFAULT_AUTH_PROMPT_MESSAGE,
422 success_message=_DEFAULT_WEB_SUCCESS_MESSAGE,
423 open_browser=True,
424 **kwargs
425 ):
413426 """Run the flow using the server strategy.
414427
415428 The server strategy instructs the user to open the authorization URL in
438451 for the user.
439452 """
440453 wsgi_app = _RedirectWSGIApp(success_message)
454 # Fail fast if the address is occupied
455 wsgiref.simple_server.WSGIServer.allow_reuse_address = False
441456 local_server = wsgiref.simple_server.make_server(
442 host, port, wsgi_app, handler_class=_WSGIRequestHandler)
443
444 self.redirect_uri = 'http://{}:{}/'.format(
445 host, local_server.server_port)
457 host, port, wsgi_app, handler_class=_WSGIRequestHandler
458 )
459
460 self.redirect_uri = "http://{}:{}/".format(host, local_server.server_port)
446461 auth_url, _ = self.authorization_url(**kwargs)
447462
448463 if open_browser:
454469
455470 # Note: using https here because oauthlib is very picky that
456471 # OAuth 2.0 should only occur over https.
457 authorization_response = wsgi_app.last_request_uri.replace(
458 'http', 'https')
472 authorization_response = wsgi_app.last_request_uri.replace("http", "https")
459473 self.fetch_token(authorization_response=authorization_response)
474
475 # This closes the socket
476 local_server.server_close()
460477
461478 return self.credentials
462479
466483
467484 Uses a named logger instead of printing to stderr.
468485 """
486
469487 def log_message(self, format, *args):
470488 # pylint: disable=redefined-builtin
471489 # (format is the argument name defined in the superclass.)
498516 Returns:
499517 Iterable[bytes]: The response body.
500518 """
501 start_response('200 OK', [('Content-type', 'text/plain')])
519 start_response("200 OK", [("Content-type", "text/plain")])
502520 self.last_request_uri = wsgiref.util.request_uri(environ)
503 return [self._success_message.encode('utf-8')]
521 return [self._success_message.encode("utf-8")]
2626 import google.oauth2.credentials
2727 import requests_oauthlib
2828
29 _REQUIRED_CONFIG_KEYS = frozenset(('auth_uri', 'token_uri', 'client_id'))
29 _REQUIRED_CONFIG_KEYS = frozenset(("auth_uri", "token_uri", "client_id"))
3030
3131
3232 def session_from_client_config(client_config, scopes, **kwargs):
5454 /aaa_client_secrets
5555 """
5656
57 if 'web' in client_config:
58 config = client_config['web']
59 elif 'installed' in client_config:
60 config = client_config['installed']
57 if "web" in client_config:
58 config = client_config["web"]
59 elif "installed" in client_config:
60 config = client_config["installed"]
6161 else:
62 raise ValueError(
63 'Client secrets must be for a web or installed app.')
62 raise ValueError("Client secrets must be for a web or installed app.")
6463
6564 if not _REQUIRED_CONFIG_KEYS.issubset(config.keys()):
66 raise ValueError('Client secrets is not in the correct format.')
65 raise ValueError("Client secrets is not in the correct format.")
6766
6867 session = requests_oauthlib.OAuth2Session(
69 client_id=config['client_id'],
70 scope=scopes,
71 **kwargs)
68 client_id=config["client_id"], scope=scopes, **kwargs
69 )
7270
7371 return session, client_config
7472
9391 https://developers.google.com/api-client-library/python/guide
9492 /aaa_client_secrets
9593 """
96 with open(client_secrets_file, 'r') as json_file:
94 with open(client_secrets_file, "r") as json_file:
9795 client_config = json.load(json_file)
9896
9997 return session_from_client_config(client_config, scopes, **kwargs)
125123
126124 if not session.token:
127125 raise ValueError(
128 'There is no access token for this session, did you call '
129 'fetch_token?')
126 "There is no access token for this session, did you call " "fetch_token?"
127 )
130128
131129 credentials = google.oauth2.credentials.Credentials(
132 session.token['access_token'],
133 refresh_token=session.token.get('refresh_token'),
134 id_token=session.token.get('id_token'),
135 token_uri=client_config.get('token_uri'),
136 client_id=client_config.get('client_id'),
137 client_secret=client_config.get('client_secret'),
138 scopes=session.scope)
139 credentials.expiry = datetime.datetime.utcfromtimestamp(
140 session.token['expires_at'])
130 session.token["access_token"],
131 refresh_token=session.token.get("refresh_token"),
132 id_token=session.token.get("id_token"),
133 token_uri=client_config.get("token_uri"),
134 client_id=client_config.get("client_id"),
135 client_secret=client_config.get("client_secret"),
136 scopes=session.scope,
137 )
138 credentials.expiry = datetime.datetime.utcfromtimestamp(session.token["expires_at"])
141139 return credentials
3838 import google_auth_oauthlib.flow
3939
4040
41 APP_NAME = 'google-oauthlib-tool'
42 DEFAULT_CREDENTIALS_FILENAME = 'credentials.json'
41 APP_NAME = "google-oauthlib-tool"
42 DEFAULT_CREDENTIALS_FILENAME = "credentials.json"
4343
4444
4545 @click.command()
4646 @click.option(
47 '--client-secrets',
48 metavar='<client_secret_json_file>',
47 "--client-secrets",
48 metavar="<client_secret_json_file>",
4949 required=True,
50 help='Path to OAuth2 client secret JSON file.')
50 help="Path to OAuth2 client secret JSON file.",
51 )
5152 @click.option(
52 '--scope',
53 "--scope",
5354 multiple=True,
54 metavar='<oauth2 scope>',
55 metavar="<oauth2 scope>",
5556 required=True,
56 help='API scopes to authorize access for.')
57 help="API scopes to authorize access for.",
58 )
5759 @click.option(
58 '--save',
60 "--save",
5961 is_flag=True,
60 metavar='<save_mode>',
62 metavar="<save_mode>",
6163 show_default=True,
6264 default=False,
63 help='Save the credentials to file.')
65 help="Save the credentials to file.",
66 )
6467 @click.option(
65 '--credentials',
66 metavar='<oauth2_credentials>',
68 "--credentials",
69 metavar="<oauth2_credentials>",
6770 show_default=True,
68 default=os.path.join(
69 click.get_app_dir(APP_NAME),
70 DEFAULT_CREDENTIALS_FILENAME
71 ),
72 help='Path to store OAuth2 credentials.')
71 default=os.path.join(click.get_app_dir(APP_NAME), DEFAULT_CREDENTIALS_FILENAME),
72 help="Path to store OAuth2 credentials.",
73 )
7374 @click.option(
74 '--headless',
75 "--headless",
7576 is_flag=True,
76 metavar='<headless_mode>',
77 show_default=True, default=False,
78 help='Run a console based flow.')
77 metavar="<headless_mode>",
78 show_default=True,
79 default=False,
80 help="Run a console based flow.",
81 )
7982 def main(client_secrets, scope, save, credentials, headless):
8083 """Command-line tool for obtaining authorization and credentials from a user.
8184
9598 """
9699
97100 flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file(
98 client_secrets,
99 scopes=scope
101 client_secrets, scopes=scope
100102 )
101103
102104 if not headless:
105107 creds = flow.run_console()
106108
107109 creds_data = {
108 'token': creds.token,
109 'refresh_token': creds.refresh_token,
110 'token_uri': creds.token_uri,
111 'client_id': creds.client_id,
112 'client_secret': creds.client_secret,
113 'scopes': creds.scopes
110 "token": creds.token,
111 "refresh_token": creds.refresh_token,
112 "token_uri": creds.token_uri,
113 "client_id": creds.client_id,
114 "client_secret": creds.client_secret,
115 "scopes": creds.scopes,
114116 }
115117
116118 if save:
117 del creds_data['token']
119 del creds_data["token"]
118120
119121 config_path = os.path.dirname(credentials)
120122 if config_path and not os.path.isdir(config_path):
121123 os.makedirs(config_path)
122124
123 with open(credentials, 'w') as outfile:
125 with open(credentials, "w") as outfile:
124126 json.dump(creds_data, outfile)
125127
126 click.echo('credentials saved: %s' % credentials)
128 click.echo("credentials saved: %s" % credentials)
127129
128130 else:
129131 click.echo(json.dumps(creds_data))
130132
131133
132 if __name__ == '__main__':
134 if __name__ == "__main__":
133135 # pylint doesn't realize that click has changed the function signature.
134136 main() # pylint: disable=no-value-for-parameter
00 Metadata-Version: 2.1
11 Name: google-auth-oauthlib
2 Version: 0.4.1
2 Version: 0.4.2
33 Summary: Google Authentication Library
44 Home-page: https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib
55 Author: Google Cloud Platform
4747
4848 Keywords: google auth oauth client oauthlib
4949 Platform: UNKNOWN
50 Classifier: Programming Language :: Python :: 2
51 Classifier: Programming Language :: Python :: 2.7
5250 Classifier: Programming Language :: Python :: 3
53 Classifier: Programming Language :: Python :: 3.4
54 Classifier: Programming Language :: Python :: 3.5
5551 Classifier: Programming Language :: Python :: 3.6
52 Classifier: Programming Language :: Python :: 3.7
53 Classifier: Programming Language :: Python :: 3.8
54 Classifier: Programming Language :: Python :: 3.9
5655 Classifier: Development Status :: 3 - Alpha
5756 Classifier: Intended Audience :: Developers
5857 Classifier: License :: OSI Approved :: Apache Software License
6160 Classifier: Operating System :: MacOS :: MacOS X
6261 Classifier: Operating System :: OS Independent
6362 Classifier: Topic :: Internet :: WWW/HTTP
63 Requires-Python: >=3.6
6464 Provides-Extra: tool
1414 google_auth_oauthlib.egg-info/top_level.txt
1515 google_auth_oauthlib/tool/__init__.py
1616 google_auth_oauthlib/tool/__main__.py
17 tests/__init__.py
18 tests/test_flow.py
19 tests/test_helpers.py
20 tests/test_interactive.py
21 tests/test_tool.py
22 tests/data/client_secrets.json
17 tests/unit/test_flow.py
18 tests/unit/test_helpers.py
19 tests/unit/test_interactive.py
20 tests/unit/test_tool.py
21 tests/unit/data/client_secrets.json
1717 from setuptools import setup
1818
1919
20 TOOL_DEPENDENCIES = (
21 'click'
22 )
20 TOOL_DEPENDENCIES = "click"
2321
24 DEPENDENCIES = (
25 'google-auth',
26 'requests-oauthlib>=0.7.0',
27 )
22 DEPENDENCIES = ("google-auth", "requests-oauthlib>=0.7.0")
2823
2924
30 with io.open('README.rst', 'r') as fh:
25 with io.open("README.rst", "r") as fh:
3126 long_description = fh.read()
3227
3328
29 version = "0.4.2"
30
3431 setup(
35 name='google-auth-oauthlib',
36 version = '0.4.1',
37 author='Google Cloud Platform',
38 author_email='jonwayne+google-auth@google.com',
39 description='Google Authentication Library',
32 name="google-auth-oauthlib",
33 version=version,
34 author="Google Cloud Platform",
35 author_email="jonwayne+google-auth@google.com",
36 description="Google Authentication Library",
4037 long_description=long_description,
41 url='https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib',
42 packages=find_packages(exclude=('tests*',)),
38 url="https://github.com/GoogleCloudPlatform/google-auth-library-python-oauthlib",
39 packages=find_packages(exclude=("tests*",)),
4340 install_requires=DEPENDENCIES,
44 extras_require={
45 'tool': TOOL_DEPENDENCIES,
41 extras_require={"tool": TOOL_DEPENDENCIES},
42 entry_points={
43 "console_scripts": [
44 "google-oauthlib-tool" "=google_auth_oauthlib.tool.__main__:main [tool]"
45 ]
4646 },
47 entry_points={
48 'console_scripts': [
49 'google-oauthlib-tool'
50 '=google_auth_oauthlib.tool.__main__:main [tool]',
51 ],
52 },
53 license='Apache 2.0',
54 keywords='google auth oauth client oauthlib',
55 classifiers=(
56 'Programming Language :: Python :: 2',
57 'Programming Language :: Python :: 2.7',
58 'Programming Language :: Python :: 3',
59 'Programming Language :: Python :: 3.4',
60 'Programming Language :: Python :: 3.5',
61 'Programming Language :: Python :: 3.6',
62 'Development Status :: 3 - Alpha',
63 'Intended Audience :: Developers',
64 'License :: OSI Approved :: Apache Software License',
65 'Operating System :: POSIX',
66 'Operating System :: Microsoft :: Windows',
67 'Operating System :: MacOS :: MacOS X',
68 'Operating System :: OS Independent',
69 'Topic :: Internet :: WWW/HTTP',
70 ),
47 python_requires=">=3.6",
48 license="Apache 2.0",
49 keywords="google auth oauth client oauthlib",
50 classifiers=[
51 "Programming Language :: Python :: 3",
52 "Programming Language :: Python :: 3.6",
53 "Programming Language :: Python :: 3.7",
54 "Programming Language :: Python :: 3.8",
55 "Programming Language :: Python :: 3.9",
56 "Development Status :: 3 - Alpha",
57 "Intended Audience :: Developers",
58 "License :: OSI Approved :: Apache Software License",
59 "Operating System :: POSIX",
60 "Operating System :: Microsoft :: Windows",
61 "Operating System :: MacOS :: MacOS X",
62 "Operating System :: OS Independent",
63 "Topic :: Internet :: WWW/HTTP",
64 ],
7165 )
+0
-0
tests/__init__.py less more
(Empty file)
+0
-14
tests/data/client_secrets.json less more
0 {
1 "web": {
2 "client_id": "example.apps.googleusercontent.com",
3 "project_id": "example",
4 "auth_uri": "https://accounts.google.com/o/oauth2/auth",
5 "token_uri": "https://accounts.google.com/o/oauth2/token",
6 "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
7 "client_secret": "itsasecrettoeveryone",
8 "redirect_uris": [
9 "urn:ietf:wg:oauth:2.0:oob",
10 "http://localhost"
11 ]
12 }
13 }
+0
-336
tests/test_flow.py less more
0 # Copyright 2017 Google Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 import concurrent.futures
15 import datetime
16 from functools import partial
17 import json
18 import os
19 import re
20
21 import mock
22 import pytest
23 import requests
24 from six.moves import urllib
25
26 from google_auth_oauthlib import flow
27
28 DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
29 CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, 'client_secrets.json')
30
31 with open(CLIENT_SECRETS_FILE, 'r') as fh:
32 CLIENT_SECRETS_INFO = json.load(fh)
33
34
35 class TestFlow(object):
36 def test_from_client_secrets_file(self):
37 instance = flow.Flow.from_client_secrets_file(
38 CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes)
39 assert instance.client_config == CLIENT_SECRETS_INFO['web']
40 assert (instance.oauth2session.client_id ==
41 CLIENT_SECRETS_INFO['web']['client_id'])
42 assert instance.oauth2session.scope == mock.sentinel.scopes
43
44 def test_from_client_secrets_file_with_redirect_uri(self):
45 instance = flow.Flow.from_client_secrets_file(
46 CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes,
47 redirect_uri=mock.sentinel.redirect_uri
48 )
49 assert (instance.redirect_uri ==
50 instance.oauth2session.redirect_uri ==
51 mock.sentinel.redirect_uri)
52
53 def test_from_client_config_installed(self):
54 client_config = {'installed': CLIENT_SECRETS_INFO['web']}
55 instance = flow.Flow.from_client_config(
56 client_config, scopes=mock.sentinel.scopes)
57 assert instance.client_config == client_config['installed']
58 assert (instance.oauth2session.client_id ==
59 client_config['installed']['client_id'])
60 assert instance.oauth2session.scope == mock.sentinel.scopes
61
62 def test_from_client_config_with_redirect_uri(self):
63 client_config = {'installed': CLIENT_SECRETS_INFO['web']}
64 instance = flow.Flow.from_client_config(
65 client_config, scopes=mock.sentinel.scopes,
66 redirect_uri=mock.sentinel.redirect_uri
67 )
68 assert (instance.redirect_uri ==
69 instance.oauth2session.redirect_uri ==
70 mock.sentinel.redirect_uri)
71
72 def test_from_client_config_bad_format(self):
73 with pytest.raises(ValueError):
74 flow.Flow.from_client_config({}, scopes=mock.sentinel.scopes)
75
76 @pytest.fixture
77 def instance(self):
78 yield flow.Flow.from_client_config(
79 CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes)
80
81 def test_redirect_uri(self, instance):
82 instance.redirect_uri = mock.sentinel.redirect_uri
83 assert (instance.redirect_uri ==
84 instance.oauth2session.redirect_uri ==
85 mock.sentinel.redirect_uri)
86
87 def test_authorization_url(self, instance):
88 scope = 'scope_one'
89 instance.oauth2session.scope = [scope]
90 authorization_url_patch = mock.patch.object(
91 instance.oauth2session, 'authorization_url',
92 wraps=instance.oauth2session.authorization_url)
93
94 with authorization_url_patch as authorization_url_spy:
95 url, _ = instance.authorization_url(prompt='consent')
96
97 assert CLIENT_SECRETS_INFO['web']['auth_uri'] in url
98 assert scope in url
99 authorization_url_spy.assert_called_with(
100 CLIENT_SECRETS_INFO['web']['auth_uri'],
101 access_type='offline',
102 prompt='consent')
103
104 def test_authorization_url_code_verifier(self, instance):
105 scope = 'scope_one'
106 instance.oauth2session.scope = [scope]
107 instance.code_verifier = 'amanaplanacanalpanama'
108 authorization_url_patch = mock.patch.object(
109 instance.oauth2session, 'authorization_url',
110 wraps=instance.oauth2session.authorization_url)
111
112 with authorization_url_patch as authorization_url_spy:
113 url, _ = instance.authorization_url(prompt='consent')
114
115 assert CLIENT_SECRETS_INFO['web']['auth_uri'] in url
116 assert scope in url
117 authorization_url_spy.assert_called_with(
118 CLIENT_SECRETS_INFO['web']['auth_uri'],
119 access_type='offline',
120 prompt='consent',
121 code_challenge='2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q',
122 code_challenge_method='S256')
123
124 def test_authorization_url_access_type(self, instance):
125 scope = 'scope_one'
126 instance.oauth2session.scope = [scope]
127 instance.code_verifier = 'amanaplanacanalpanama'
128 authorization_url_patch = mock.patch.object(
129 instance.oauth2session, 'authorization_url',
130 wraps=instance.oauth2session.authorization_url)
131
132 with authorization_url_patch as authorization_url_spy:
133 url, _ = instance.authorization_url(access_type='meep')
134
135 assert CLIENT_SECRETS_INFO['web']['auth_uri'] in url
136 assert scope in url
137 authorization_url_spy.assert_called_with(
138 CLIENT_SECRETS_INFO['web']['auth_uri'],
139 access_type='meep',
140 code_challenge='2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q',
141 code_challenge_method='S256')
142
143 def test_authorization_url_generated_verifier(self):
144 scope = 'scope_one'
145 instance = flow.Flow.from_client_config(
146 CLIENT_SECRETS_INFO, scopes=[scope],
147 autogenerate_code_verifier=True)
148 authorization_url_path = mock.patch.object(
149 instance.oauth2session, 'authorization_url',
150 wraps=instance.oauth2session.authorization_url)
151
152 with authorization_url_path as authorization_url_spy:
153 instance.authorization_url()
154
155 _, kwargs = authorization_url_spy.call_args_list[0]
156 assert kwargs['code_challenge_method'] == 'S256'
157 assert len(instance.code_verifier) == 128
158 assert len(kwargs['code_challenge']) == 43
159 valid_verifier = r'^[A-Za-z0-9-._~]*$'
160 valid_challenge = r'^[A-Za-z0-9-_]*$'
161 assert re.match(valid_verifier, instance.code_verifier)
162 assert re.match(valid_challenge, kwargs['code_challenge'])
163
164 def test_fetch_token(self, instance):
165 instance.code_verifier = 'amanaplanacanalpanama'
166 fetch_token_patch = mock.patch.object(
167 instance.oauth2session, 'fetch_token', autospec=True,
168 return_value=mock.sentinel.token)
169
170 with fetch_token_patch as fetch_token_mock:
171 token = instance.fetch_token(code=mock.sentinel.code)
172
173 assert token == mock.sentinel.token
174 fetch_token_mock.assert_called_with(
175 CLIENT_SECRETS_INFO['web']['token_uri'],
176 client_secret=CLIENT_SECRETS_INFO['web']['client_secret'],
177 code=mock.sentinel.code,
178 code_verifier='amanaplanacanalpanama')
179
180 def test_credentials(self, instance):
181 instance.oauth2session.token = {
182 'access_token': mock.sentinel.access_token,
183 'refresh_token': mock.sentinel.refresh_token,
184 'id_token': mock.sentinel.id_token,
185 'expires_at': 643969200.0
186 }
187
188 credentials = instance.credentials
189
190 assert credentials.token == mock.sentinel.access_token
191 assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0)
192 assert credentials._refresh_token == mock.sentinel.refresh_token
193 assert credentials.id_token == mock.sentinel.id_token
194 assert (credentials._client_id ==
195 CLIENT_SECRETS_INFO['web']['client_id'])
196 assert (credentials._client_secret ==
197 CLIENT_SECRETS_INFO['web']['client_secret'])
198 assert (credentials._token_uri ==
199 CLIENT_SECRETS_INFO['web']['token_uri'])
200
201 def test_authorized_session(self, instance):
202 instance.oauth2session.token = {
203 'access_token': mock.sentinel.access_token,
204 'refresh_token': mock.sentinel.refresh_token,
205 'id_token': mock.sentinel.id_token,
206 'expires_at': 643969200.0
207 }
208
209 session = instance.authorized_session()
210
211 assert session.credentials.token == mock.sentinel.access_token
212
213
214 class TestInstalledAppFlow(object):
215 SCOPES = ['email', 'profile']
216 REDIRECT_REQUEST_PATH = '/?code=code&state=state'
217
218 @pytest.fixture
219 def instance(self):
220 yield flow.InstalledAppFlow.from_client_config(
221 CLIENT_SECRETS_INFO, scopes=self.SCOPES)
222
223 @pytest.fixture
224 def mock_fetch_token(self, instance):
225 def set_token(*args, **kwargs):
226 instance.oauth2session.token = {
227 'access_token': mock.sentinel.access_token,
228 'refresh_token': mock.sentinel.refresh_token,
229 'id_token': mock.sentinel.id_token,
230 'expires_at': 643969200.0
231 }
232
233 fetch_token_patch = mock.patch.object(
234 instance.oauth2session, 'fetch_token', autospec=True,
235 side_effect=set_token)
236
237 with fetch_token_patch as fetch_token_mock:
238 yield fetch_token_mock
239
240 @mock.patch('google_auth_oauthlib.flow.input', autospec=True)
241 def test_run_console(self, input_mock, instance, mock_fetch_token):
242 input_mock.return_value = mock.sentinel.code
243 instance.code_verifier = 'amanaplanacanalpanama'
244 credentials = instance.run_console()
245
246 assert credentials.token == mock.sentinel.access_token
247 assert credentials._refresh_token == mock.sentinel.refresh_token
248 assert credentials.id_token == mock.sentinel.id_token
249
250 mock_fetch_token.assert_called_with(
251 CLIENT_SECRETS_INFO['web']['token_uri'],
252 client_secret=CLIENT_SECRETS_INFO['web']['client_secret'],
253 code=mock.sentinel.code,
254 code_verifier='amanaplanacanalpanama')
255
256 @pytest.mark.webtest
257 @mock.patch('google_auth_oauthlib.flow.webbrowser', autospec=True)
258 def test_run_local_server(
259 self, webbrowser_mock, instance, mock_fetch_token):
260 auth_redirect_url = urllib.parse.urljoin(
261 'http://localhost:60452',
262 self.REDIRECT_REQUEST_PATH)
263
264 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
265 future = pool.submit(partial(
266 instance.run_local_server, port=60452))
267
268 while not future.done():
269 try:
270 requests.get(auth_redirect_url)
271 except requests.ConnectionError: # pragma: NO COVER
272 pass
273
274 credentials = future.result()
275
276 assert credentials.token == mock.sentinel.access_token
277 assert credentials._refresh_token == mock.sentinel.refresh_token
278 assert credentials.id_token == mock.sentinel.id_token
279 assert webbrowser_mock.open.called
280
281 expected_auth_response = auth_redirect_url.replace('http', 'https')
282 mock_fetch_token.assert_called_with(
283 CLIENT_SECRETS_INFO['web']['token_uri'],
284 client_secret=CLIENT_SECRETS_INFO['web']['client_secret'],
285 authorization_response=expected_auth_response,
286 code_verifier=None)
287
288 @pytest.mark.webtest
289 @mock.patch('google_auth_oauthlib.flow.webbrowser', autospec=True)
290 def test_run_local_server_code_verifier(
291 self, webbrowser_mock, instance, mock_fetch_token):
292 auth_redirect_url = urllib.parse.urljoin(
293 'http://localhost:60452',
294 self.REDIRECT_REQUEST_PATH)
295 instance.code_verifier = 'amanaplanacanalpanama'
296
297 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
298 future = pool.submit(partial(
299 instance.run_local_server, port=60452))
300
301 while not future.done():
302 try:
303 requests.get(auth_redirect_url)
304 except requests.ConnectionError: # pragma: NO COVER
305 pass
306
307 credentials = future.result()
308
309 assert credentials.token == mock.sentinel.access_token
310 assert credentials._refresh_token == mock.sentinel.refresh_token
311 assert credentials.id_token == mock.sentinel.id_token
312 assert webbrowser_mock.open.called
313
314 expected_auth_response = auth_redirect_url.replace('http', 'https')
315 mock_fetch_token.assert_called_with(
316 CLIENT_SECRETS_INFO['web']['token_uri'],
317 client_secret=CLIENT_SECRETS_INFO['web']['client_secret'],
318 authorization_response=expected_auth_response,
319 code_verifier='amanaplanacanalpanama')
320
321 @mock.patch('google_auth_oauthlib.flow.webbrowser', autospec=True)
322 @mock.patch('wsgiref.simple_server.make_server', autospec=True)
323 def test_run_local_server_no_browser(
324 self, make_server_mock, webbrowser_mock, instance,
325 mock_fetch_token):
326
327 def assign_last_request_uri(host, port, wsgi_app, **kwargs):
328 wsgi_app.last_request_uri = self.REDIRECT_REQUEST_PATH
329 return mock.Mock()
330
331 make_server_mock.side_effect = assign_last_request_uri
332
333 instance.run_local_server(open_browser=False)
334
335 assert not webbrowser_mock.open.called
+0
-97
tests/test_helpers.py less more
0 # Copyright 2017 Google Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 import datetime
15 import json
16 import os
17
18 import mock
19 import pytest
20
21 from google_auth_oauthlib import helpers
22
23 DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
24 CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, 'client_secrets.json')
25
26 with open(CLIENT_SECRETS_FILE, 'r') as fh:
27 CLIENT_SECRETS_INFO = json.load(fh)
28
29
30 def test_session_from_client_config_web():
31 session, config = helpers.session_from_client_config(
32 CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes)
33
34 assert config == CLIENT_SECRETS_INFO
35 assert session.client_id == CLIENT_SECRETS_INFO['web']['client_id']
36 assert session.scope == mock.sentinel.scopes
37
38
39 def test_session_from_client_config_installed():
40 info = {'installed': CLIENT_SECRETS_INFO['web']}
41 session, config = helpers.session_from_client_config(
42 info, scopes=mock.sentinel.scopes)
43 assert config == info
44 assert session.client_id == info['installed']['client_id']
45 assert session.scope == mock.sentinel.scopes
46
47
48 def test_session_from_client_config_bad_format():
49 with pytest.raises(ValueError):
50 helpers.session_from_client_config({}, scopes=[])
51
52
53 def test_session_from_client_config_missing_keys():
54 with pytest.raises(ValueError):
55 helpers.session_from_client_config({'web': {}}, scopes=[])
56
57
58 def test_session_from_client_secrets_file():
59 session, config = helpers.session_from_client_secrets_file(
60 CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes)
61 assert config == CLIENT_SECRETS_INFO
62 assert session.client_id == CLIENT_SECRETS_INFO['web']['client_id']
63 assert session.scope == mock.sentinel.scopes
64
65
66 @pytest.fixture
67 def session():
68 session, _ = helpers.session_from_client_config(
69 CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes)
70 yield session
71
72
73 def test_credentials_from_session(session):
74 session.token = {
75 'access_token': mock.sentinel.access_token,
76 'refresh_token': mock.sentinel.refresh_token,
77 'id_token': mock.sentinel.id_token,
78 'expires_at': 643969200.0
79 }
80
81 credentials = helpers.credentials_from_session(
82 session, CLIENT_SECRETS_INFO['web'])
83
84 assert credentials.token == mock.sentinel.access_token
85 assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0)
86 assert credentials._refresh_token == mock.sentinel.refresh_token
87 assert credentials.id_token == mock.sentinel.id_token
88 assert credentials._client_id == CLIENT_SECRETS_INFO['web']['client_id']
89 assert (credentials._client_secret ==
90 CLIENT_SECRETS_INFO['web']['client_secret'])
91 assert credentials._token_uri == CLIENT_SECRETS_INFO['web']['token_uri']
92
93
94 def test_bad_credentials(session):
95 with pytest.raises(ValueError):
96 helpers.credentials_from_session(session)
+0
-44
tests/test_interactive.py less more
0 # Copyright 2019 Google LLC
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # https://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 import mock
15
16
17 def test_get_user_credentials():
18 from google_auth_oauthlib import flow
19 from google_auth_oauthlib import interactive as module_under_test
20
21 mock_flow_instance = mock.create_autospec(
22 flow.InstalledAppFlow,
23 instance=True
24 )
25
26 with mock.patch(
27 "google_auth_oauthlib.flow.InstalledAppFlow", autospec=True
28 ) as mock_flow:
29 mock_flow.from_client_config.return_value = mock_flow_instance
30 module_under_test.get_user_credentials(
31 ["scopes"],
32 "some-client-id",
33 "shh-secret",
34 )
35
36 mock_flow.from_client_config.assert_called_once_with(
37 mock.ANY,
38 scopes=["scopes"],
39 )
40 actual_client_config = mock_flow.from_client_config.call_args[0][0]
41 assert actual_client_config["installed"]["client_id"] == "some-client-id"
42 assert actual_client_config["installed"]["client_secret"] == "shh-secret"
43 mock_flow_instance.run_console.assert_called_once()
+0
-149
tests/test_tool.py less more
0 # Copyright 2017 Google Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 import io
15 import json
16 import os.path
17 import tempfile
18
19 import click.testing
20 import google.oauth2.credentials
21 import mock
22 import pytest
23
24 import google_auth_oauthlib.flow
25 import google_auth_oauthlib.tool.__main__ as cli
26
27 DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
28 CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, 'client_secrets.json')
29
30
31 class TestMain(object):
32 @pytest.fixture
33 def runner(self):
34 return click.testing.CliRunner()
35
36 @pytest.fixture
37 def dummy_credentials(self):
38 return google.oauth2.credentials.Credentials(
39 token='dummy_access_token',
40 refresh_token='dummy_refresh_token',
41 token_uri='dummy_token_uri',
42 client_id='dummy_client_id',
43 client_secret='dummy_client_secret',
44 scopes=['dummy_scope1', 'dummy_scope2']
45 )
46
47 @pytest.fixture
48 def local_server_mock(self, dummy_credentials):
49 run_local_server_patch = mock.patch.object(
50 google_auth_oauthlib.flow.InstalledAppFlow,
51 'run_local_server',
52 autospec=True)
53
54 with run_local_server_patch as flow:
55 flow.return_value = dummy_credentials
56 yield flow
57
58 @pytest.fixture
59 def console_mock(self, dummy_credentials):
60 run_console_patch = mock.patch.object(
61 google_auth_oauthlib.flow.InstalledAppFlow,
62 'run_console',
63 autospec=True)
64
65 with run_console_patch as flow:
66 flow.return_value = dummy_credentials
67 yield flow
68
69 def test_help(self, runner):
70 result = runner.invoke(cli.main, ['--help'])
71 assert not result.exception
72 assert 'RFC6749' in result.output
73 assert 'OAuth 2.0 authorization flow' in result.output
74 assert 'not intended for production use' in result.output
75 assert result.exit_code == 0
76
77 def test_defaults(self, runner, dummy_credentials, local_server_mock):
78 result = runner.invoke(cli.main, [
79 '--client-secrets', CLIENT_SECRETS_FILE,
80 '--scope', 'somescope',
81 ])
82 local_server_mock.assert_called_with(mock.ANY)
83 assert not result.exception
84 assert result.exit_code == 0
85 creds_data = json.loads(result.output)
86 creds = google.oauth2.credentials.Credentials(**creds_data)
87 assert creds.token == dummy_credentials.token
88 assert creds.refresh_token == dummy_credentials.refresh_token
89 assert creds.token_uri == dummy_credentials.token_uri
90 assert creds.client_id == dummy_credentials.client_id
91 assert creds.client_secret == dummy_credentials.client_secret
92 assert creds.scopes == dummy_credentials.scopes
93
94 def test_headless(self, runner, dummy_credentials, console_mock):
95 result = runner.invoke(cli.main, [
96 '--client-secrets', CLIENT_SECRETS_FILE,
97 '--scope', 'somescope',
98 '--headless'
99 ])
100 console_mock.assert_called_with(mock.ANY)
101 assert not result.exception
102 assert dummy_credentials.refresh_token in result.output
103 assert result.exit_code == 0
104
105 def test_save_new_dir(self, runner, dummy_credentials, local_server_mock):
106 credentials_tmpdir = tempfile.mkdtemp()
107 credentials_path = os.path.join(
108 credentials_tmpdir,
109 'new-directory',
110 'credentials.json'
111 )
112 result = runner.invoke(cli.main, [
113 '--client-secrets', CLIENT_SECRETS_FILE,
114 '--scope', 'somescope',
115 '--credentials', credentials_path,
116 '--save'
117 ])
118 local_server_mock.assert_called_with(mock.ANY)
119 assert not result.exception
120 assert 'saved' in result.output
121 assert result.exit_code == 0
122 with io.open(credentials_path) as f: # pylint: disable=invalid-name
123 creds_data = json.load(f)
124 assert 'access_token' not in creds_data
125
126 creds = google.oauth2.credentials.Credentials(
127 token=None, **creds_data)
128 assert creds.token is None
129 assert creds.refresh_token == dummy_credentials.refresh_token
130 assert creds.token_uri == dummy_credentials.token_uri
131 assert creds.client_id == dummy_credentials.client_id
132 assert creds.client_secret == dummy_credentials.client_secret
133
134 def test_save_existing_dir(self, runner, local_server_mock):
135 credentials_tmpdir = tempfile.mkdtemp()
136 result = runner.invoke(cli.main, [
137 '--client-secrets', CLIENT_SECRETS_FILE,
138 '--scope', 'somescope',
139 '--credentials', os.path.join(
140 credentials_tmpdir,
141 'credentials.json'
142 ),
143 '--save'
144 ])
145 local_server_mock.assert_called_with(mock.ANY)
146 assert not result.exception
147 assert 'saved' in result.output
148 assert result.exit_code == 0
0 {
1 "web": {
2 "client_id": "example.apps.googleusercontent.com",
3 "project_id": "example",
4 "auth_uri": "https://accounts.google.com/o/oauth2/auth",
5 "token_uri": "https://accounts.google.com/o/oauth2/token",
6 "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
7 "client_secret": "itsasecrettoeveryone",
8 "redirect_uris": [
9 "urn:ietf:wg:oauth:2.0:oob",
10 "http://localhost"
11 ]
12 }
13 }
0 # Copyright 2017 Google Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 import concurrent.futures
15 import datetime
16 from functools import partial
17 import json
18 import os
19 import re
20 import random
21 import socket
22
23 import mock
24 import pytest
25 import requests
26 from six.moves import urllib
27
28 from google_auth_oauthlib import flow
29
30 DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
31 CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json")
32
33 with open(CLIENT_SECRETS_FILE, "r") as fh:
34 CLIENT_SECRETS_INFO = json.load(fh)
35
36
37 class TestFlow(object):
38 def test_from_client_secrets_file(self):
39 instance = flow.Flow.from_client_secrets_file(
40 CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes
41 )
42 assert instance.client_config == CLIENT_SECRETS_INFO["web"]
43 assert (
44 instance.oauth2session.client_id == CLIENT_SECRETS_INFO["web"]["client_id"]
45 )
46 assert instance.oauth2session.scope == mock.sentinel.scopes
47
48 def test_from_client_secrets_file_with_redirect_uri(self):
49 instance = flow.Flow.from_client_secrets_file(
50 CLIENT_SECRETS_FILE,
51 scopes=mock.sentinel.scopes,
52 redirect_uri=mock.sentinel.redirect_uri,
53 )
54 assert (
55 instance.redirect_uri
56 == instance.oauth2session.redirect_uri
57 == mock.sentinel.redirect_uri
58 )
59
60 def test_from_client_config_installed(self):
61 client_config = {"installed": CLIENT_SECRETS_INFO["web"]}
62 instance = flow.Flow.from_client_config(
63 client_config, scopes=mock.sentinel.scopes
64 )
65 assert instance.client_config == client_config["installed"]
66 assert (
67 instance.oauth2session.client_id == client_config["installed"]["client_id"]
68 )
69 assert instance.oauth2session.scope == mock.sentinel.scopes
70
71 def test_from_client_config_with_redirect_uri(self):
72 client_config = {"installed": CLIENT_SECRETS_INFO["web"]}
73 instance = flow.Flow.from_client_config(
74 client_config,
75 scopes=mock.sentinel.scopes,
76 redirect_uri=mock.sentinel.redirect_uri,
77 )
78 assert (
79 instance.redirect_uri
80 == instance.oauth2session.redirect_uri
81 == mock.sentinel.redirect_uri
82 )
83
84 def test_from_client_config_bad_format(self):
85 with pytest.raises(ValueError):
86 flow.Flow.from_client_config({}, scopes=mock.sentinel.scopes)
87
88 @pytest.fixture
89 def instance(self):
90 yield flow.Flow.from_client_config(
91 CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes
92 )
93
94 def test_redirect_uri(self, instance):
95 instance.redirect_uri = mock.sentinel.redirect_uri
96 assert (
97 instance.redirect_uri
98 == instance.oauth2session.redirect_uri
99 == mock.sentinel.redirect_uri
100 )
101
102 def test_authorization_url(self, instance):
103 scope = "scope_one"
104 instance.oauth2session.scope = [scope]
105 authorization_url_patch = mock.patch.object(
106 instance.oauth2session,
107 "authorization_url",
108 wraps=instance.oauth2session.authorization_url,
109 )
110
111 with authorization_url_patch as authorization_url_spy:
112 url, _ = instance.authorization_url(prompt="consent")
113
114 assert CLIENT_SECRETS_INFO["web"]["auth_uri"] in url
115 assert scope in url
116 authorization_url_spy.assert_called_with(
117 CLIENT_SECRETS_INFO["web"]["auth_uri"],
118 access_type="offline",
119 prompt="consent",
120 )
121
122 def test_authorization_url_code_verifier(self, instance):
123 scope = "scope_one"
124 instance.oauth2session.scope = [scope]
125 instance.code_verifier = "amanaplanacanalpanama"
126 authorization_url_patch = mock.patch.object(
127 instance.oauth2session,
128 "authorization_url",
129 wraps=instance.oauth2session.authorization_url,
130 )
131
132 with authorization_url_patch as authorization_url_spy:
133 url, _ = instance.authorization_url(prompt="consent")
134
135 assert CLIENT_SECRETS_INFO["web"]["auth_uri"] in url
136 assert scope in url
137 authorization_url_spy.assert_called_with(
138 CLIENT_SECRETS_INFO["web"]["auth_uri"],
139 access_type="offline",
140 prompt="consent",
141 code_challenge="2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q",
142 code_challenge_method="S256",
143 )
144
145 def test_authorization_url_access_type(self, instance):
146 scope = "scope_one"
147 instance.oauth2session.scope = [scope]
148 instance.code_verifier = "amanaplanacanalpanama"
149 authorization_url_patch = mock.patch.object(
150 instance.oauth2session,
151 "authorization_url",
152 wraps=instance.oauth2session.authorization_url,
153 )
154
155 with authorization_url_patch as authorization_url_spy:
156 url, _ = instance.authorization_url(access_type="meep")
157
158 assert CLIENT_SECRETS_INFO["web"]["auth_uri"] in url
159 assert scope in url
160 authorization_url_spy.assert_called_with(
161 CLIENT_SECRETS_INFO["web"]["auth_uri"],
162 access_type="meep",
163 code_challenge="2yN0TOdl0gkGwFOmtfx3f913tgEaLM2d2S0WlmG1Z6Q",
164 code_challenge_method="S256",
165 )
166
167 def test_authorization_url_generated_verifier(self):
168 scope = "scope_one"
169 instance = flow.Flow.from_client_config(
170 CLIENT_SECRETS_INFO, scopes=[scope], autogenerate_code_verifier=True
171 )
172 authorization_url_path = mock.patch.object(
173 instance.oauth2session,
174 "authorization_url",
175 wraps=instance.oauth2session.authorization_url,
176 )
177
178 with authorization_url_path as authorization_url_spy:
179 instance.authorization_url()
180
181 _, kwargs = authorization_url_spy.call_args_list[0]
182 assert kwargs["code_challenge_method"] == "S256"
183 assert len(instance.code_verifier) == 128
184 assert len(kwargs["code_challenge"]) == 43
185 valid_verifier = r"^[A-Za-z0-9-._~]*$"
186 valid_challenge = r"^[A-Za-z0-9-_]*$"
187 assert re.match(valid_verifier, instance.code_verifier)
188 assert re.match(valid_challenge, kwargs["code_challenge"])
189
190 def test_fetch_token(self, instance):
191 instance.code_verifier = "amanaplanacanalpanama"
192 fetch_token_patch = mock.patch.object(
193 instance.oauth2session,
194 "fetch_token",
195 autospec=True,
196 return_value=mock.sentinel.token,
197 )
198
199 with fetch_token_patch as fetch_token_mock:
200 token = instance.fetch_token(code=mock.sentinel.code)
201
202 assert token == mock.sentinel.token
203 fetch_token_mock.assert_called_with(
204 CLIENT_SECRETS_INFO["web"]["token_uri"],
205 client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"],
206 code=mock.sentinel.code,
207 code_verifier="amanaplanacanalpanama",
208 )
209
210 def test_credentials(self, instance):
211 instance.oauth2session.token = {
212 "access_token": mock.sentinel.access_token,
213 "refresh_token": mock.sentinel.refresh_token,
214 "id_token": mock.sentinel.id_token,
215 "expires_at": 643969200.0,
216 }
217
218 credentials = instance.credentials
219
220 assert credentials.token == mock.sentinel.access_token
221 assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0)
222 assert credentials._refresh_token == mock.sentinel.refresh_token
223 assert credentials.id_token == mock.sentinel.id_token
224 assert credentials._client_id == CLIENT_SECRETS_INFO["web"]["client_id"]
225 assert credentials._client_secret == CLIENT_SECRETS_INFO["web"]["client_secret"]
226 assert credentials._token_uri == CLIENT_SECRETS_INFO["web"]["token_uri"]
227
228 def test_authorized_session(self, instance):
229 instance.oauth2session.token = {
230 "access_token": mock.sentinel.access_token,
231 "refresh_token": mock.sentinel.refresh_token,
232 "id_token": mock.sentinel.id_token,
233 "expires_at": 643969200.0,
234 }
235
236 session = instance.authorized_session()
237
238 assert session.credentials.token == mock.sentinel.access_token
239
240
241 class TestInstalledAppFlow(object):
242 SCOPES = ["email", "profile"]
243 REDIRECT_REQUEST_PATH = "/?code=code&state=state"
244
245 @pytest.fixture
246 def instance(self):
247 yield flow.InstalledAppFlow.from_client_config(
248 CLIENT_SECRETS_INFO, scopes=self.SCOPES
249 )
250
251 @pytest.fixture
252 def port(self):
253 # Creating a new server at the same port will result in
254 # a 'Address already in use' error for a brief
255 # period of time after the socket has been closed.
256 # Work around this in the tests by choosing a random port.
257 # https://stackoverflow.com/questions/6380057/python-binding-socket-address-already-in-use
258 yield random.randrange(60400, 60900)
259
260 @pytest.fixture
261 def socket(self, port):
262 s = socket.socket()
263 s.bind(("localhost", port))
264 yield s
265 s.close()
266
267 @pytest.fixture
268 def mock_fetch_token(self, instance):
269 def set_token(*args, **kwargs):
270 instance.oauth2session.token = {
271 "access_token": mock.sentinel.access_token,
272 "refresh_token": mock.sentinel.refresh_token,
273 "id_token": mock.sentinel.id_token,
274 "expires_at": 643969200.0,
275 }
276
277 fetch_token_patch = mock.patch.object(
278 instance.oauth2session, "fetch_token", autospec=True, side_effect=set_token
279 )
280
281 with fetch_token_patch as fetch_token_mock:
282 yield fetch_token_mock
283
284 @mock.patch("google_auth_oauthlib.flow.input", autospec=True)
285 def test_run_console(self, input_mock, instance, mock_fetch_token):
286 input_mock.return_value = mock.sentinel.code
287 instance.code_verifier = "amanaplanacanalpanama"
288 credentials = instance.run_console()
289
290 assert credentials.token == mock.sentinel.access_token
291 assert credentials._refresh_token == mock.sentinel.refresh_token
292 assert credentials.id_token == mock.sentinel.id_token
293
294 mock_fetch_token.assert_called_with(
295 CLIENT_SECRETS_INFO["web"]["token_uri"],
296 client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"],
297 code=mock.sentinel.code,
298 code_verifier="amanaplanacanalpanama",
299 )
300
301 @pytest.mark.webtest
302 @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True)
303 def test_run_local_server(self, webbrowser_mock, instance, mock_fetch_token, port):
304 auth_redirect_url = urllib.parse.urljoin(
305 f"http://localhost:{port}", self.REDIRECT_REQUEST_PATH
306 )
307
308 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
309 future = pool.submit(partial(instance.run_local_server, port=port))
310
311 while not future.done():
312 try:
313 requests.get(auth_redirect_url)
314 except requests.ConnectionError: # pragma: NO COVER
315 pass
316
317 credentials = future.result()
318
319 assert credentials.token == mock.sentinel.access_token
320 assert credentials._refresh_token == mock.sentinel.refresh_token
321 assert credentials.id_token == mock.sentinel.id_token
322 assert webbrowser_mock.open.called
323
324 expected_auth_response = auth_redirect_url.replace("http", "https")
325 mock_fetch_token.assert_called_with(
326 CLIENT_SECRETS_INFO["web"]["token_uri"],
327 client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"],
328 authorization_response=expected_auth_response,
329 code_verifier=None,
330 )
331
332 @pytest.mark.webtest
333 @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True)
334 def test_run_local_server_code_verifier(
335 self, webbrowser_mock, instance, mock_fetch_token, port
336 ):
337 auth_redirect_url = urllib.parse.urljoin(
338 f"http://localhost:{port}", self.REDIRECT_REQUEST_PATH
339 )
340 instance.code_verifier = "amanaplanacanalpanama"
341
342 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
343 future = pool.submit(partial(instance.run_local_server, port=port))
344
345 while not future.done():
346 try:
347 requests.get(auth_redirect_url)
348 except requests.ConnectionError: # pragma: NO COVER
349 pass
350
351 credentials = future.result()
352
353 assert credentials.token == mock.sentinel.access_token
354 assert credentials._refresh_token == mock.sentinel.refresh_token
355 assert credentials.id_token == mock.sentinel.id_token
356 assert webbrowser_mock.open.called
357
358 expected_auth_response = auth_redirect_url.replace("http", "https")
359 mock_fetch_token.assert_called_with(
360 CLIENT_SECRETS_INFO["web"]["token_uri"],
361 client_secret=CLIENT_SECRETS_INFO["web"]["client_secret"],
362 authorization_response=expected_auth_response,
363 code_verifier="amanaplanacanalpanama",
364 )
365
366 @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True)
367 @mock.patch("wsgiref.simple_server.make_server", autospec=True)
368 def test_run_local_server_no_browser(
369 self, make_server_mock, webbrowser_mock, instance, mock_fetch_token
370 ):
371 def assign_last_request_uri(host, port, wsgi_app, **kwargs):
372 wsgi_app.last_request_uri = self.REDIRECT_REQUEST_PATH
373 return mock.Mock()
374
375 make_server_mock.side_effect = assign_last_request_uri
376
377 instance.run_local_server(open_browser=False)
378
379 assert not webbrowser_mock.open.called
380
381 @pytest.mark.webtest
382 @mock.patch("google_auth_oauthlib.flow.webbrowser", autospec=True)
383 def test_run_local_server_occupied_port(
384 self, webbrowser_mock, instance, mock_fetch_token, port, socket
385 ):
386 # socket fixture is already bound to http://localhost:port
387 instance.run_local_server
388 with pytest.raises(OSError) as exc:
389 instance.run_local_server(port=port)
390 assert "address already in use" in exc.strerror.lower()
0 # Copyright 2017 Google Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 import datetime
15 import json
16 import os
17
18 import mock
19 import pytest
20
21 from google_auth_oauthlib import helpers
22
23 DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
24 CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json")
25
26 with open(CLIENT_SECRETS_FILE, "r") as fh:
27 CLIENT_SECRETS_INFO = json.load(fh)
28
29
30 def test_session_from_client_config_web():
31 session, config = helpers.session_from_client_config(
32 CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes
33 )
34
35 assert config == CLIENT_SECRETS_INFO
36 assert session.client_id == CLIENT_SECRETS_INFO["web"]["client_id"]
37 assert session.scope == mock.sentinel.scopes
38
39
40 def test_session_from_client_config_installed():
41 info = {"installed": CLIENT_SECRETS_INFO["web"]}
42 session, config = helpers.session_from_client_config(
43 info, scopes=mock.sentinel.scopes
44 )
45 assert config == info
46 assert session.client_id == info["installed"]["client_id"]
47 assert session.scope == mock.sentinel.scopes
48
49
50 def test_session_from_client_config_bad_format():
51 with pytest.raises(ValueError):
52 helpers.session_from_client_config({}, scopes=[])
53
54
55 def test_session_from_client_config_missing_keys():
56 with pytest.raises(ValueError):
57 helpers.session_from_client_config({"web": {}}, scopes=[])
58
59
60 def test_session_from_client_secrets_file():
61 session, config = helpers.session_from_client_secrets_file(
62 CLIENT_SECRETS_FILE, scopes=mock.sentinel.scopes
63 )
64 assert config == CLIENT_SECRETS_INFO
65 assert session.client_id == CLIENT_SECRETS_INFO["web"]["client_id"]
66 assert session.scope == mock.sentinel.scopes
67
68
69 @pytest.fixture
70 def session():
71 session, _ = helpers.session_from_client_config(
72 CLIENT_SECRETS_INFO, scopes=mock.sentinel.scopes
73 )
74 yield session
75
76
77 def test_credentials_from_session(session):
78 session.token = {
79 "access_token": mock.sentinel.access_token,
80 "refresh_token": mock.sentinel.refresh_token,
81 "id_token": mock.sentinel.id_token,
82 "expires_at": 643969200.0,
83 }
84
85 credentials = helpers.credentials_from_session(session, CLIENT_SECRETS_INFO["web"])
86
87 assert credentials.token == mock.sentinel.access_token
88 assert credentials.expiry == datetime.datetime(1990, 5, 29, 8, 20, 0)
89 assert credentials._refresh_token == mock.sentinel.refresh_token
90 assert credentials.id_token == mock.sentinel.id_token
91 assert credentials._client_id == CLIENT_SECRETS_INFO["web"]["client_id"]
92 assert credentials._client_secret == CLIENT_SECRETS_INFO["web"]["client_secret"]
93 assert credentials._token_uri == CLIENT_SECRETS_INFO["web"]["token_uri"]
94
95
96 def test_bad_credentials(session):
97 with pytest.raises(ValueError):
98 helpers.credentials_from_session(session)
0 # Copyright 2019 Google LLC
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # https://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 import mock
15
16
17 def test_get_user_credentials():
18 from google_auth_oauthlib import flow
19 from google_auth_oauthlib import interactive as module_under_test
20
21 mock_flow_instance = mock.create_autospec(flow.InstalledAppFlow, instance=True)
22
23 with mock.patch(
24 "google_auth_oauthlib.flow.InstalledAppFlow", autospec=True
25 ) as mock_flow:
26 mock_flow.from_client_config.return_value = mock_flow_instance
27 module_under_test.get_user_credentials(
28 ["scopes"], "some-client-id", "shh-secret"
29 )
30
31 mock_flow.from_client_config.assert_called_once_with(mock.ANY, scopes=["scopes"])
32 actual_client_config = mock_flow.from_client_config.call_args[0][0]
33 assert actual_client_config["installed"]["client_id"] == "some-client-id"
34 assert actual_client_config["installed"]["client_secret"] == "shh-secret"
35 mock_flow_instance.run_console.assert_called_once()
0 # Copyright 2017 Google Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 import io
15 import json
16 import os.path
17 import tempfile
18
19 import click.testing
20 import google.oauth2.credentials
21 import mock
22 import pytest
23
24 import google_auth_oauthlib.flow
25 import google_auth_oauthlib.tool.__main__ as cli
26
27 DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
28 CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json")
29
30
31 class TestMain(object):
32 @pytest.fixture
33 def runner(self):
34 return click.testing.CliRunner()
35
36 @pytest.fixture
37 def dummy_credentials(self):
38 return google.oauth2.credentials.Credentials(
39 token="dummy_access_token",
40 refresh_token="dummy_refresh_token",
41 token_uri="dummy_token_uri",
42 client_id="dummy_client_id",
43 client_secret="dummy_client_secret",
44 scopes=["dummy_scope1", "dummy_scope2"],
45 )
46
47 @pytest.fixture
48 def local_server_mock(self, dummy_credentials):
49 run_local_server_patch = mock.patch.object(
50 google_auth_oauthlib.flow.InstalledAppFlow,
51 "run_local_server",
52 autospec=True,
53 )
54
55 with run_local_server_patch as flow:
56 flow.return_value = dummy_credentials
57 yield flow
58
59 @pytest.fixture
60 def console_mock(self, dummy_credentials):
61 run_console_patch = mock.patch.object(
62 google_auth_oauthlib.flow.InstalledAppFlow, "run_console", autospec=True
63 )
64
65 with run_console_patch as flow:
66 flow.return_value = dummy_credentials
67 yield flow
68
69 def test_help(self, runner):
70 result = runner.invoke(cli.main, ["--help"])
71 assert not result.exception
72 assert "RFC6749" in result.output
73 assert "OAuth 2.0 authorization flow" in result.output
74 assert "not intended for production use" in result.output
75 assert result.exit_code == 0
76
77 def test_defaults(self, runner, dummy_credentials, local_server_mock):
78 result = runner.invoke(
79 cli.main, ["--client-secrets", CLIENT_SECRETS_FILE, "--scope", "somescope"]
80 )
81 local_server_mock.assert_called_with(mock.ANY)
82 assert not result.exception
83 assert result.exit_code == 0
84 creds_data = json.loads(result.output)
85 creds = google.oauth2.credentials.Credentials(**creds_data)
86 assert creds.token == dummy_credentials.token
87 assert creds.refresh_token == dummy_credentials.refresh_token
88 assert creds.token_uri == dummy_credentials.token_uri
89 assert creds.client_id == dummy_credentials.client_id
90 assert creds.client_secret == dummy_credentials.client_secret
91 assert creds.scopes == dummy_credentials.scopes
92
93 def test_headless(self, runner, dummy_credentials, console_mock):
94 result = runner.invoke(
95 cli.main,
96 [
97 "--client-secrets",
98 CLIENT_SECRETS_FILE,
99 "--scope",
100 "somescope",
101 "--headless",
102 ],
103 )
104 console_mock.assert_called_with(mock.ANY)
105 assert not result.exception
106 assert dummy_credentials.refresh_token in result.output
107 assert result.exit_code == 0
108
109 def test_save_new_dir(self, runner, dummy_credentials, local_server_mock):
110 credentials_tmpdir = tempfile.mkdtemp()
111 credentials_path = os.path.join(
112 credentials_tmpdir, "new-directory", "credentials.json"
113 )
114 result = runner.invoke(
115 cli.main,
116 [
117 "--client-secrets",
118 CLIENT_SECRETS_FILE,
119 "--scope",
120 "somescope",
121 "--credentials",
122 credentials_path,
123 "--save",
124 ],
125 )
126 local_server_mock.assert_called_with(mock.ANY)
127 assert not result.exception
128 assert "saved" in result.output
129 assert result.exit_code == 0
130 with io.open(credentials_path) as f: # pylint: disable=invalid-name
131 creds_data = json.load(f)
132 assert "access_token" not in creds_data
133
134 creds = google.oauth2.credentials.Credentials(token=None, **creds_data)
135 assert creds.token is None
136 assert creds.refresh_token == dummy_credentials.refresh_token
137 assert creds.token_uri == dummy_credentials.token_uri
138 assert creds.client_id == dummy_credentials.client_id
139 assert creds.client_secret == dummy_credentials.client_secret
140
141 def test_save_existing_dir(self, runner, local_server_mock):
142 credentials_tmpdir = tempfile.mkdtemp()
143 result = runner.invoke(
144 cli.main,
145 [
146 "--client-secrets",
147 CLIENT_SECRETS_FILE,
148 "--scope",
149 "somescope",
150 "--credentials",
151 os.path.join(credentials_tmpdir, "credentials.json"),
152 "--save",
153 ],
154 )
155 local_server_mock.assert_called_with(mock.ANY)
156 assert not result.exception
157 assert "saved" in result.output
158 assert result.exit_code == 0