aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2019-02-05 13:12:22 -0800
committerandroid-build-merger <android-build-merger@google.com>2019-02-05 13:12:22 -0800
commit6d9ea50ac0a52d4cf8405d1e6a187ffeca09efdc (patch)
tree317c2b40271ca4aabfd8e2a2454727353928e9e6
parent876fb74ec66f981e60e76de4adedab6b7cb56789 (diff)
parentf39f212da02ff1fbcecc54e76a42f77ae4f31665 (diff)
downloadoauth2client-6d9ea50ac0a52d4cf8405d1e6a187ffeca09efdc.tar.gz
Upgrade oauth2client to v4.1.3 am: 5acb77a57d am: cb79ec1592
am: f39f212da0 Change-Id: I78f9b1da6abee9e406689673398506ebafdb37d3
-rw-r--r--.coveragerc4
-rw-r--r--.github/ISSUE_TEMPLATE.md3
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md3
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml39
-rw-r--r--CHANGELOG.md79
-rw-r--r--CODE_OF_CONDUCT.md43
-rw-r--r--CONTRIBUTING.md6
-rw-r--r--CONTRIBUTORS.md95
-rw-r--r--LICENSE210
-rw-r--r--MANIFEST.in4
-rw-r--r--METADATA12
-rw-r--r--README.md6
-rw-r--r--docs/index.rst18
-rw-r--r--docs/source/oauth2client.client.rst4
-rw-r--r--docs/source/oauth2client.clientsecrets.rst4
-rw-r--r--docs/source/oauth2client.contrib.appengine.rst4
-rw-r--r--docs/source/oauth2client.contrib.devshell.rst4
-rw-r--r--docs/source/oauth2client.contrib.dictionary_storage.rst4
-rw-r--r--docs/source/oauth2client.contrib.django_util.apps.rst4
-rw-r--r--docs/source/oauth2client.contrib.django_util.decorators.rst4
-rw-r--r--docs/source/oauth2client.contrib.django_util.models.rst4
-rw-r--r--docs/source/oauth2client.contrib.django_util.rst4
-rw-r--r--docs/source/oauth2client.contrib.django_util.signals.rst4
-rw-r--r--docs/source/oauth2client.contrib.django_util.site.rst4
-rw-r--r--docs/source/oauth2client.contrib.django_util.storage.rst4
-rw-r--r--docs/source/oauth2client.contrib.django_util.views.rst4
-rw-r--r--docs/source/oauth2client.contrib.flask_util.rst4
-rw-r--r--docs/source/oauth2client.contrib.gce.rst4
-rw-r--r--docs/source/oauth2client.contrib.keyring_storage.rst4
-rw-r--r--docs/source/oauth2client.contrib.locked_file.rst7
-rw-r--r--docs/source/oauth2client.contrib.multiprocess_file_storage.rst4
-rw-r--r--docs/source/oauth2client.contrib.multistore_file.rst7
-rw-r--r--docs/source/oauth2client.contrib.rst6
-rw-r--r--docs/source/oauth2client.contrib.sqlalchemy.rst4
-rw-r--r--docs/source/oauth2client.contrib.xsrfutil.rst4
-rw-r--r--docs/source/oauth2client.crypt.rst4
-rw-r--r--docs/source/oauth2client.file.rst4
-rw-r--r--docs/source/oauth2client.rst1
-rw-r--r--docs/source/oauth2client.service_account.rst4
-rw-r--r--docs/source/oauth2client.tools.rst4
-rw-r--r--docs/source/oauth2client.transport.rst4
-rw-r--r--docs/source/oauth2client.util.rst7
-rw-r--r--oauth2client/__init__.py11
-rw-r--r--oauth2client/_helpers.py236
-rw-r--r--oauth2client/_pkce.py67
-rw-r--r--oauth2client/client.py277
-rw-r--r--oauth2client/clientsecrets.py1
-rw-r--r--oauth2client/contrib/_fcntl_opener.py81
-rw-r--r--oauth2client/contrib/_metadata.py45
-rw-r--r--oauth2client/contrib/_win32_opener.py106
-rw-r--r--oauth2client/contrib/appengine.py33
-rw-r--r--oauth2client/contrib/devshell.py8
-rw-r--r--oauth2client/contrib/django_util/__init__.py36
-rw-r--r--oauth2client/contrib/django_util/models.py11
-rw-r--r--oauth2client/contrib/django_util/views.py21
-rw-r--r--oauth2client/contrib/flask_util.py9
-rw-r--r--oauth2client/contrib/gce.py32
-rw-r--r--oauth2client/contrib/keyring_storage.py3
-rw-r--r--oauth2client/contrib/locked_file.py234
-rw-r--r--oauth2client/contrib/multistore_file.py505
-rw-r--r--oauth2client/contrib/xsrfutil.py9
-rw-r--r--oauth2client/file.py21
-rw-r--r--oauth2client/service_account.py18
-rw-r--r--oauth2client/tools.py18
-rw-r--r--oauth2client/transport.py74
-rw-r--r--oauth2client/util.py206
-rw-r--r--samples/django/README.md21
-rwxr-xr-xsamples/django/django_user/manage.py23
-rw-r--r--samples/django/django_user/myoauth/__init__.py0
-rw-r--r--samples/django/django_user/myoauth/settings.py115
-rw-r--r--samples/django/django_user/myoauth/urls.py30
-rw-r--r--samples/django/django_user/myoauth/wsgi.py21
-rw-r--r--samples/django/django_user/polls/__init__.py0
-rw-r--r--samples/django/django_user/polls/models.py23
-rw-r--r--samples/django/django_user/polls/templates/registration/login.html45
-rw-r--r--samples/django/django_user/polls/views.py41
-rw-r--r--samples/django/django_user/requirements.txt3
-rwxr-xr-xsamples/django/google_user/manage.py23
-rw-r--r--samples/django/google_user/myoauth/__init__.py0
-rw-r--r--samples/django/google_user/myoauth/settings.py107
-rw-r--r--samples/django/google_user/myoauth/urls.py26
-rw-r--r--samples/django/google_user/myoauth/wsgi.py21
-rw-r--r--samples/django/google_user/polls/__init__.py0
-rw-r--r--samples/django/google_user/polls/views.py41
-rw-r--r--samples/django/google_user/requirements.txt3
-rwxr-xr-xscripts/fetch_gae_sdk.py85
-rwxr-xr-xscripts/install.sh22
-rwxr-xr-xscripts/run.sh9
-rw-r--r--scripts/run_gce_system_tests.py26
-rw-r--r--scripts/run_system_tests.py14
-rwxr-xr-xscripts/run_system_tests.sh11
-rw-r--r--setup.cfg2
-rw-r--r--setup.py37
-rw-r--r--tests/__init__.py22
-rw-r--r--tests/conftest.py31
-rw-r--r--tests/contrib/appengine/__init__.py0
-rw-r--r--tests/contrib/appengine/conftest.py53
-rw-r--r--tests/contrib/appengine/test__appengine_ndb.py (renamed from tests/contrib/test__appengine_ndb.py)8
-rw-r--r--tests/contrib/appengine/test_appengine.py (renamed from tests/contrib/test_appengine.py)191
-rw-r--r--tests/contrib/django_util/test_decorators.py37
-rw-r--r--tests/contrib/django_util/test_django_models.py44
-rw-r--r--tests/contrib/django_util/test_django_storage.py4
-rw-r--r--tests/contrib/django_util/test_django_util.py41
-rw-r--r--tests/contrib/django_util/test_views.py102
-rw-r--r--tests/contrib/test_devshell.py26
-rw-r--r--tests/contrib/test_dictionary_storage.py4
-rw-r--r--tests/contrib/test_flask_util.py94
-rw-r--r--tests/contrib/test_gce.py69
-rw-r--r--tests/contrib/test_keyring_storage.py17
-rw-r--r--tests/contrib/test_locked_file.py244
-rw-r--r--tests/contrib/test_metadata.py83
-rw-r--r--tests/contrib/test_multiprocess_file_storage.py71
-rw-r--r--tests/contrib/test_multistore_file.py383
-rw-r--r--tests/contrib/test_sqlalchemy.py30
-rw-r--r--tests/contrib/test_xsrfutil.py27
-rw-r--r--tests/data/client_secrets.json4
-rw-r--r--tests/data/unfilled_client_secrets.json2
-rw-r--r--tests/data/user-key.json.encbin240 -> 256 bytes
-rw-r--r--tests/http_mock.py44
-rw-r--r--tests/test__helpers.py190
-rw-r--r--tests/test__pkce.py54
-rw-r--r--tests/test__pure_python_crypt.py6
-rw-r--r--tests/test__pycrypto_crypt.py7
-rw-r--r--tests/test_client.py627
-rw-r--r--tests/test_clientsecrets.py14
-rw-r--r--tests/test_crypt.py62
-rw-r--r--tests/test_file.py171
-rw-r--r--tests/test_jwt.py85
-rw-r--r--tests/test_service_account.py226
-rw-r--r--tests/test_tools.py14
-rw-r--r--tests/test_transport.py67
-rw-r--r--tests/test_util.py122
-rw-r--r--tox.ini112
134 files changed, 3435 insertions, 3411 deletions
diff --git a/.coveragerc b/.coveragerc
index 0151e07..3a3e2cd 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,6 +1,10 @@
+[run]
+branch = True
+
[report]
omit =
*/samples/*
+ */conftest.py
# Don't report coverage over platform-specific modules.
oauth2client/contrib/_fcntl_opener.py
oauth2client/contrib/_win32_opener.py
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..2ce3395
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,3 @@
+**Note**: oauth2client is now deprecated. As such, it is unlikely that we will
+address or respond to your issue. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..1fbd4d2
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,3 @@
+**Note**: oauth2client is now deprecated. As such, it is unlikely that we will
+review or merge to your pull request. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
diff --git a/.gitignore b/.gitignore
index 89c1121..0bc898c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ docs/_build
# Test files
.tox/
+.cache/
# Django test database
db.sqlite3
diff --git a/.travis.yml b/.travis.yml
index a8c01fa..47570be 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,41 +1,46 @@
language: python
-python: 2.7
sudo: false
-# TODO(issue 532): Fix syntax when 3.5 is natively available upstream
+
matrix:
include:
- - python: 3.5
- env:
- - TOX_ENV=py35
+ - python: 2.7
+ env: TOX_ENV=flake8
+ - python: 2.7
+ env: TOX_ENV=docs
+ - python: 2.7
+ env: TOX_ENV=gae
+ - python: 2.7
+ env: TOX_ENV=py27
+ - python: 3.4
+ env: TOX_ENV=py34
+ - python: 3.5
+ env: TOX_ENV=py35
+ - python: 2.7
+ env: TOX_ENV=system-tests
+ - python: 3.4
+ env: TOX_ENV=system-tests3
+ - python: 2.7
+ env: TOX_ENV=cover
env:
- matrix:
- - TOX_ENV=py26
- - TOX_ENV=py27
- - TOX_ENV=py33
- - TOX_ENV=py34
- - TOX_ENV=pypy
- - TOX_ENV=docs
- - TOX_ENV=system-tests
- - TOX_ENV=system-tests3
- - TOX_ENV=gae
- - TOX_ENV=flake8
global:
- GAE_PYTHONPATH=${HOME}/.cache/google_appengine
cache:
directories:
- ${HOME}/.cache
+ - ${HOME}/.pyenv
install:
- ./scripts/install.sh
script:
- ./scripts/run.sh
after_success:
-- if [[ "${TOX_ENV}" == "gae" ]]; then tox -e coveralls; fi
+- if [[ "${TOX_ENV}" == "cover" ]]; then coveralls; fi
notifications:
email: false
deploy:
provider: pypi
user: gcloudpypi
+ distributions: sdist bdist_wheel
password:
secure: "C9ImNa5kbdnrQNfX9ww4PUtQIr3tN+nfxl7eDkP1B8Qr0QNYjrjov7x+DLImkKvmoJd3dxYtYIpLE9esObUHu0gKHYxqymNHtuAAyoBOUfPtmp0vIEse9brNKMtaey5Ngk7ZWz9EHKBBqRHxqgN+Giby+K9Ta3K3urJIq6urYhE="
on:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf33dea..2523d29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,84 @@
# CHANGELOG
+## v4.1.3
+
+**Note**: oauth2client is deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
+
+* Changed OAuth2 endpoints to use oauth2.googleapis.com variants. (#742)
+
+## v4.1.2
+
+**Note**: oauth2client is deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
+
+Bug fixes:
+* Fix packaging issue had erroneously installed the test package. (#688)
+
+## v4.1.1
+
+**Note**: oauth2client is deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
+
+New features:
+* Allow passing prompt='consent' via the flow_from_clientsecrets. (#717)
+
+## v4.1.0
+
+**Note**: oauth2client is now deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/).
+
+New features:
+* Allow customizing the GCE metadata service address via an env var. (#704)
+* Store original encoded and signed identity JWT in OAuth2Credentials. (#680)
+* Use jsonpickle in django contrib, if available. (#676)
+
+Bug fixes:
+* Typo fixes. (#668, #697)
+* Remove b64 padding from PKCE values, per RFC7636. (#683)
+* Include LICENSE in Manifest.in. (#694)
+* Fix tests and CI. (#705, #712, #713)
+* Escape callback error code in flask_util. (#710)
+
+## v4.0.0
+
+New features:
+* New Django samples. (#636)
+* Add support for RFC7636 PKCE. (#588)
+* Release as a universal wheel. (#665)
+
+Bug fixes:
+* Fix django authorization redirect by correctly checking validity of credentials. (#651)
+* Correct query loss when using parse_qsl to dict. (#622)
+* Switch django models from pickle to jsonpickle. (#614)
+* Support new MIDDLEWARE Django 1.10 setting. (#623)
+* Remove usage of os.environ.setdefault. (#621)
+* Handle missing storage files correctly. (#576)
+* Try to revoke token with POST when getting a 405. (#662)
+
+Internal changes:
+* Use transport module for GCE environment check. (#612)
+* Remove __author__ lines and add contributors.md. (#627)
+* Clean up imports. (#625)
+* Use transport.request in tests. (#607)
+* Drop unittest2 dependency (#610)
+* Remove backslash line continuations. (#608)
+* Use transport helpers in system tests. (#606)
+* Clean up usage of HTTP mocks in tests. (#605)
+* Remove all uses of MagicMock. (#598)
+* Migrate test runner to pytest. (#569)
+* Merge util.py and _helpers.py. (#579)
+* Remove httplib2 imports from non-transport modules. (#577)
+
+Breaking changes:
+* Drop Python 3.3 support. (#603)
+* Drop Python 2.6 support. (#590)
+* Remove multistore_file. (#589)
+
## v3.0.0
* Populate `token_expiry` for GCE credentials. (#473)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..46b2a08
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,43 @@
+# Contributor Code of Conduct
+
+As contributors and maintainers of this project,
+and in the interest of fostering an open and welcoming community,
+we pledge to respect all people who contribute through reporting issues,
+posting feature requests, updating documentation,
+submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project
+a harassment-free experience for everyone,
+regardless of level of experience, gender, gender identity and expression,
+sexual orientation, disability, personal appearance,
+body size, race, ethnicity, age, religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information,
+such as physical or electronic
+addresses, without explicit permission
+* Other unethical or unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct.
+By adopting this Code of Conduct,
+project maintainers commit themselves to fairly and consistently
+applying these principles to every aspect of managing this project.
+Project maintainers who do not follow or enforce the Code of Conduct
+may be permanently removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior
+may be reported by opening an issue
+or contacting one or more of the project maintainers.
+
+This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0,
+available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 15b9455..990c534 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -127,10 +127,6 @@ Running Tests
least version 2.6 of `pypy` installed. See the [docs][13] for
more information.
-- **Note** that `django` related tests are turned off for Python 2.6
- and 3.3. This is because `django` dropped support for
- [2.6 in `django==1.7`][14] and for [3.3 in `django==1.9`][15].
-
Running System Tests
--------------------
@@ -202,7 +198,5 @@ we'll be able to accept your pull requests.
[11]: #include-tests
[12]: #make-the-pull-request
[13]: https://oauth2client.readthedocs.io/en/latest/#using-pypy
-[14]: https://docs.djangoproject.com/en/1.7/faq/install/#what-python-version-can-i-use-with-django
-[15]: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django
[GooglePythonStyle]: https://google.github.io/styleguide/pyguide.html
[GitCommitRules]: http://chris.beams.io/posts/git-commit/#seven-rules
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
new file mode 100644
index 0000000..00bd09f
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,95 @@
+# Contribors to oauth2client
+
+## Maintainers
+
+* [Nathaniel Manista](https://github.com/nathanielmanistaatgoogle)
+* [Jon Wayne Parrott](https://github.com/jonparrott)
+* [Danny Hermes](https://github.com/dhermes)
+
+Previous maintainers:
+
+* [Craig Citro](https://github.com/craigcitro)
+* [Joe Gregorio](https://github.com/jcgregorio)
+
+## Contributors
+
+This list is generated from git commit authors.
+
+* aalexand <aalexand@google.com>
+* Aaron <aaronwinter@users.noreply.github.com>
+* Adam Chainz <me@adamj.eu>
+* ade@google.com
+* Alexandre Vivien <alx.vivien@gmail.com>
+* Ali Afshar <afshar@google.com>
+* Andrzej Pragacz <apragacz@o2.pl>
+* api.nickm@gmail.com
+* Ben Demaree <bendemaree@gmail.com>
+* Bill Prin <waprin@gmail.com, waprin@google.com>
+* Brendan McCollam <brendan@mccoll.am, bmccollam@uchicago.edu>
+* Craig Citro <craigcitro@gmail.com, craigcitro@google.com>
+* Dan Ring <dfring@gmail.com>
+* Daniel Hermes <dhermes@google.com, daniel.j.hermes@gmail.com>
+* Danilo Akamine <danilowz@gmail.com>
+* daryl herzmann <akrherz@iastate.edu>
+* dlorenc <lorenc.d@gmail.com>
+* Dominik Miedziński <dominik@mdzn.pl>
+* dr. Kertész Csaba-Zoltán <cskertesz@gmail.com>
+* Dustin Farris <dustin@dustinfarris.com>
+* Eddie Warner <happyspace@gmail.com>
+* Edwin Amsler <EdwinGuy@GMail.com>
+* elibixby <elibixby@google.com>
+* Emanuele Pucciarelli <ep@acm.org>
+* Eric Koleda <eric.koleda@google.com>
+* Frederik Creemers <frederikcreemers@gmail.com>
+* Guido van Rossum <guido@google.com>
+* Harsh Vardhan <harshvd95@gmail.com>
+* Herr Kaste <thdz.x@gmx.net>
+* INADA Naoki <inada-n@klab.com>
+* JacobMoshenko <moshenko@google.com>
+* Jay Lee <jay0lee@gmail.com>
+* Jed Hartman <jhartman@google.com>
+* Jeff Terrace <jterrace@gmail.com, jterrace@google.com>
+* Jeffrey Sorensen <sorensenjs@users.noreply.github.com>
+* Jeremi Joslin <jeremi@collabspot.com>
+* Jin Liu <liujin@google.com>
+* Joe Beda <jbeda@google.com>
+* Joe Gregorio <jcgregorio@google.com, joe.gregorio@gmail.com>
+* Johan Euphrosine <proppy@google.com>
+* John Asmuth <jasmuth@gmail.com, jasmuth@google.com>
+* John Vandenberg <jayvdb@gmail.com>
+* Jon Wayne Parrott <jon.wayne.parrott@gmail.com, jonwayne@google.com>
+* Jose Alcerreca <jalc@google.com>
+* KCs <cskertesz@gmail.com>
+* Keith Maxwell <keith.maxwell@gmail.com>
+* Ken Payson <kpayson@google.com>
+* Kevin Regan <regank@google.com>
+* lraccomando <lraccomando@gmail.com>
+* Luar Roji <cyberplant@users.noreply.github.com>
+* Luke Blanshard <leadpipe@google.com>
+* Marc Cohen <marccohen@google.com>
+* Mark Pellegrini <markpell@google.com>
+* Martin Trigaux <mat@odoo.com>
+* Matt McDonald <mmcdonald@google.com>
+* Nathan Naze <nanaze@gmail.com>
+* Nathaniel Manista <nathaniel@google.com>
+* Orest Bolohan <orest@google.com>
+* Pat Ferate <pferate@gmail.com>
+* Patrick Costello <pcostello@google.com>
+* Rafe Kaplan <rafek@google.com>
+* rahulpaul@google.com <rahulpaul@google.com>
+* RM Saksida <rsaksida@gmail.com>
+* Robert Kaplow <rkaplow@google.com>
+* Robert Spies <wilford@google.com>
+* Sergei Trofimovich <siarheit@google.com>
+* sgomes@google.com <sgomes@google.com>
+* Simon Cadman <src@niftiestsoftware.com>
+* soltanmm <soltanmm@users.noreply.github.com>
+* Sébastien de Melo <sebastien.de-melo@ubicast.eu>
+* takuya sato <sato-taku@klab.com>
+* thobrla <thobrla@google.com>
+* Tom Miller <tom.h.miller@gmail.com>
+* Tony Aiuto <aiuto@google.com>
+* Travis Hobrla <thobrla@google.com>
+* Veres Lajos <vlajos@gmail.com>
+* Vivek Seth <vivekseth.m@gmail.com>
+* Éamonn McManus <eamonn@mcmanus.net>
diff --git a/LICENSE b/LICENSE
index b506d50..c8d76df 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,16 +1,205 @@
- Copyright 2014 Google Inc.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
- http://www.apache.org/licenses/LICENSE-2.0
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
Dependent Modules
=================
@@ -18,5 +207,4 @@ Dependent Modules
This code has the following dependencies
above and beyond the Python standard library:
-uritemplates - Apache License 2.0
httplib2 - MIT License
diff --git a/MANIFEST.in b/MANIFEST.in
index 39f5637..4f2ba45 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,2 @@
-include README.md
-recursive-exclude tests *
+include README.md LICENSE CHANGELOG.md
+recursive-include tests *
diff --git a/METADATA b/METADATA
index 6a258aa..97c5852 100644
--- a/METADATA
+++ b/METADATA
@@ -1,7 +1,5 @@
name: "oauth2client"
-description:
- "This is a client library for accessing resources protected by OAuth 2.0."
-
+description: "This is a client library for accessing resources protected by OAuth 2.0."
third_party {
url {
type: HOMEPAGE
@@ -11,6 +9,10 @@ third_party {
type: GIT
value: "https://github.com/google/oauth2client"
}
- version: "v3.0.0"
- last_upgrade_date { year: 2018 month: 6 day: 6 }
+ version: "v4.1.3"
+ last_upgrade_date {
+ year: 2019
+ month: 2
+ day: 1
+ }
}
diff --git a/README.md b/README.md
index 17e69fc..5e7aade 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,10 @@
This is a client library for accessing resources protected by OAuth 2.0.
+**Note**: oauth2client is now deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+[google-auth](https://google-auth.readthedocs.io) and [oauthlib](http://oauthlib.readthedocs.io/). For more details on the deprecation, see [oauth2client deprecation](https://google-auth.readthedocs.io/en/latest/oauth2client-deprecation.html).
+
Installation
============
@@ -23,7 +27,7 @@ agreement.
Supported Python Versions
=========================
-We support Python 2.6, 2.7, 3.3+. More information [in the docs][2].
+We support Python 2.7 and 3.4+. More information [in the docs][2].
[1]: https://github.com/google/oauth2client/blob/master/CONTRIBUTING.md
[2]: https://oauth2client.readthedocs.io/#supported-python-versions
diff --git a/docs/index.rst b/docs/index.rst
index 0543e1a..4b9f38a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,6 +1,14 @@
oauth2client
============
+.. note:: oauth2client is now deprecated. No more features will be added to the
+libraries and the core team is turning down support. We recommend you use
+`google-auth`_ and `oauthlib`_. For more details on the deprecation, see `oauth2client deprecation`_.
+
+.. _google-auth: https://google-auth.readthedocs.io
+.. _oauthlib: http://oauthlib.readthedocs.io/
+.. _oauth2client deprecation: https://google-auth.readthedocs.io/en/latest/oauth2client-deprecation.html
+
*making OAuth2 just a little less painful*
``oauth2client`` makes it easy to interact with OAuth2-protected resources,
@@ -107,18 +115,24 @@ contributor license agreement.
Supported Python Versions
-------------------------
-We support Python 2.6, 2.7, 3.3+. (Whatever this file says, the truth is
+We support Python 2.7 and 3.4+. (Whatever this file says, the truth is
always represented by our `tox.ini`_).
.. _tox.ini: https://github.com/google/oauth2client/blob/master/tox.ini
We explicitly decided to support Python 3 beginning with version
-3.3. Reasons for this include:
+3.4. Reasons for this include:
* Encouraging use of newest versions of Python 3
* Following the lead of prominent `open-source projects`_
* Unicode literal support which
allows for a cleaner codebase that works in both Python 2 and Python 3
+* Prominent projects like `django`_ have `dropped support`_ for earlier
+ versions (3.3 support dropped in December 2015, and 2.6 support
+ `dropped`_ in September 2014)
.. _open-source projects: http://docs.python-requests.org/en/latest/
.. _Unicode literal support: https://www.python.org/dev/peps/pep-0414/
+.. _django: https://docs.djangoproject.com/
+.. _dropped support: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django
+.. _dropped: https://docs.djangoproject.com/en/1.7/faq/install/#what-python-version-can-i-use-with-django
diff --git a/docs/source/oauth2client.client.rst b/docs/source/oauth2client.client.rst
index f3b1832..edb2f97 100644
--- a/docs/source/oauth2client.client.rst
+++ b/docs/source/oauth2client.client.rst
@@ -1,5 +1,5 @@
-oauth2client.client module
-==========================
+oauth2client\.client module
+===========================
.. automodule:: oauth2client.client
:members:
diff --git a/docs/source/oauth2client.clientsecrets.rst b/docs/source/oauth2client.clientsecrets.rst
index d666564..a839444 100644
--- a/docs/source/oauth2client.clientsecrets.rst
+++ b/docs/source/oauth2client.clientsecrets.rst
@@ -1,5 +1,5 @@
-oauth2client.clientsecrets module
-=================================
+oauth2client\.clientsecrets module
+==================================
.. automodule:: oauth2client.clientsecrets
:members:
diff --git a/docs/source/oauth2client.contrib.appengine.rst b/docs/source/oauth2client.contrib.appengine.rst
index 7f3d5e2..5051495 100644
--- a/docs/source/oauth2client.contrib.appengine.rst
+++ b/docs/source/oauth2client.contrib.appengine.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.appengine module
-=====================================
+oauth2client\.contrib\.appengine module
+=======================================
.. automodule:: oauth2client.contrib.appengine
:members:
diff --git a/docs/source/oauth2client.contrib.devshell.rst b/docs/source/oauth2client.contrib.devshell.rst
index 20d5c41..66691bd 100644
--- a/docs/source/oauth2client.contrib.devshell.rst
+++ b/docs/source/oauth2client.contrib.devshell.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.devshell module
-====================================
+oauth2client\.contrib\.devshell module
+======================================
.. automodule:: oauth2client.contrib.devshell
:members:
diff --git a/docs/source/oauth2client.contrib.dictionary_storage.rst b/docs/source/oauth2client.contrib.dictionary_storage.rst
index 1b59a2c..b94a079 100644
--- a/docs/source/oauth2client.contrib.dictionary_storage.rst
+++ b/docs/source/oauth2client.contrib.dictionary_storage.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.dictionary_storage module
-==============================================
+oauth2client\.contrib\.dictionary\_storage module
+=================================================
.. automodule:: oauth2client.contrib.dictionary_storage
:members:
diff --git a/docs/source/oauth2client.contrib.django_util.apps.rst b/docs/source/oauth2client.contrib.django_util.apps.rst
index b7c91ae..1ffe1af 100644
--- a/docs/source/oauth2client.contrib.django_util.apps.rst
+++ b/docs/source/oauth2client.contrib.django_util.apps.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.django_util.apps module
-============================================
+oauth2client\.contrib\.django\_util\.apps module
+================================================
.. automodule:: oauth2client.contrib.django_util.apps
:members:
diff --git a/docs/source/oauth2client.contrib.django_util.decorators.rst b/docs/source/oauth2client.contrib.django_util.decorators.rst
index 07350bc..2eb9dcf 100644
--- a/docs/source/oauth2client.contrib.django_util.decorators.rst
+++ b/docs/source/oauth2client.contrib.django_util.decorators.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.django_util.decorators module
-==================================================
+oauth2client\.contrib\.django\_util\.decorators module
+======================================================
.. automodule:: oauth2client.contrib.django_util.decorators
:members:
diff --git a/docs/source/oauth2client.contrib.django_util.models.rst b/docs/source/oauth2client.contrib.django_util.models.rst
index 4be59d3..91d3b8d 100644
--- a/docs/source/oauth2client.contrib.django_util.models.rst
+++ b/docs/source/oauth2client.contrib.django_util.models.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.django_util.models module
-==============================================
+oauth2client\.contrib\.django\_util\.models module
+==================================================
.. automodule:: oauth2client.contrib.django_util.models
:members:
diff --git a/docs/source/oauth2client.contrib.django_util.rst b/docs/source/oauth2client.contrib.django_util.rst
index f60195a..8247134 100644
--- a/docs/source/oauth2client.contrib.django_util.rst
+++ b/docs/source/oauth2client.contrib.django_util.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.django_util package
-========================================
+oauth2client\.contrib\.django\_util package
+===========================================
Submodules
----------
diff --git a/docs/source/oauth2client.contrib.django_util.signals.rst b/docs/source/oauth2client.contrib.django_util.signals.rst
index 70b5d2d..9a18252 100644
--- a/docs/source/oauth2client.contrib.django_util.signals.rst
+++ b/docs/source/oauth2client.contrib.django_util.signals.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.django_util.signals module
-===============================================
+oauth2client\.contrib\.django\_util\.signals module
+===================================================
.. automodule:: oauth2client.contrib.django_util.signals
:members:
diff --git a/docs/source/oauth2client.contrib.django_util.site.rst b/docs/source/oauth2client.contrib.django_util.site.rst
index a271b98..5f5dae0 100644
--- a/docs/source/oauth2client.contrib.django_util.site.rst
+++ b/docs/source/oauth2client.contrib.django_util.site.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.django_util.site module
-============================================
+oauth2client\.contrib\.django\_util\.site module
+================================================
.. automodule:: oauth2client.contrib.django_util.site
:members:
diff --git a/docs/source/oauth2client.contrib.django_util.storage.rst b/docs/source/oauth2client.contrib.django_util.storage.rst
index 393e738..4340a4c 100644
--- a/docs/source/oauth2client.contrib.django_util.storage.rst
+++ b/docs/source/oauth2client.contrib.django_util.storage.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.django_util.storage module
-===============================================
+oauth2client\.contrib\.django\_util\.storage module
+===================================================
.. automodule:: oauth2client.contrib.django_util.storage
:members:
diff --git a/docs/source/oauth2client.contrib.django_util.views.rst b/docs/source/oauth2client.contrib.django_util.views.rst
index 4cbbea0..dfba37f 100644
--- a/docs/source/oauth2client.contrib.django_util.views.rst
+++ b/docs/source/oauth2client.contrib.django_util.views.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.django_util.views module
-=============================================
+oauth2client\.contrib\.django\_util\.views module
+=================================================
.. automodule:: oauth2client.contrib.django_util.views
:members:
diff --git a/docs/source/oauth2client.contrib.flask_util.rst b/docs/source/oauth2client.contrib.flask_util.rst
index 8ff2355..c11c9ba 100644
--- a/docs/source/oauth2client.contrib.flask_util.rst
+++ b/docs/source/oauth2client.contrib.flask_util.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.flask_util module
-======================================
+oauth2client\.contrib\.flask\_util module
+=========================================
.. automodule:: oauth2client.contrib.flask_util
:members:
diff --git a/docs/source/oauth2client.contrib.gce.rst b/docs/source/oauth2client.contrib.gce.rst
index a3748b6..d0b7a15 100644
--- a/docs/source/oauth2client.contrib.gce.rst
+++ b/docs/source/oauth2client.contrib.gce.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.gce module
-===============================
+oauth2client\.contrib\.gce module
+=================================
.. automodule:: oauth2client.contrib.gce
:members:
diff --git a/docs/source/oauth2client.contrib.keyring_storage.rst b/docs/source/oauth2client.contrib.keyring_storage.rst
index 0fd7476..286e84a 100644
--- a/docs/source/oauth2client.contrib.keyring_storage.rst
+++ b/docs/source/oauth2client.contrib.keyring_storage.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.keyring_storage module
-===========================================
+oauth2client\.contrib\.keyring\_storage module
+==============================================
.. automodule:: oauth2client.contrib.keyring_storage
:members:
diff --git a/docs/source/oauth2client.contrib.locked_file.rst b/docs/source/oauth2client.contrib.locked_file.rst
deleted file mode 100644
index 1076e29..0000000
--- a/docs/source/oauth2client.contrib.locked_file.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-oauth2client.contrib.locked_file module
-=======================================
-
-.. automodule:: oauth2client.contrib.locked_file
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.multiprocess_file_storage.rst b/docs/source/oauth2client.contrib.multiprocess_file_storage.rst
index 6f683a0..eb6c0c0 100644
--- a/docs/source/oauth2client.contrib.multiprocess_file_storage.rst
+++ b/docs/source/oauth2client.contrib.multiprocess_file_storage.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.multiprocess_file_storage module
-=====================================================
+oauth2client\.contrib\.multiprocess\_file\_storage module
+=========================================================
.. automodule:: oauth2client.contrib.multiprocess_file_storage
:members:
diff --git a/docs/source/oauth2client.contrib.multistore_file.rst b/docs/source/oauth2client.contrib.multistore_file.rst
deleted file mode 100644
index 2787b10..0000000
--- a/docs/source/oauth2client.contrib.multistore_file.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-oauth2client.contrib.multistore_file module
-===========================================
-
-.. automodule:: oauth2client.contrib.multistore_file
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/source/oauth2client.contrib.rst b/docs/source/oauth2client.contrib.rst
index 44be6f9..644278d 100644
--- a/docs/source/oauth2client.contrib.rst
+++ b/docs/source/oauth2client.contrib.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib package
-============================
+oauth2client\.contrib package
+=============================
Subpackages
-----------
@@ -19,9 +19,7 @@ Submodules
oauth2client.contrib.flask_util
oauth2client.contrib.gce
oauth2client.contrib.keyring_storage
- oauth2client.contrib.locked_file
oauth2client.contrib.multiprocess_file_storage
- oauth2client.contrib.multistore_file
oauth2client.contrib.sqlalchemy
oauth2client.contrib.xsrfutil
diff --git a/docs/source/oauth2client.contrib.sqlalchemy.rst b/docs/source/oauth2client.contrib.sqlalchemy.rst
index 94eeeec..c4a634e 100644
--- a/docs/source/oauth2client.contrib.sqlalchemy.rst
+++ b/docs/source/oauth2client.contrib.sqlalchemy.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.sqlalchemy module
-======================================
+oauth2client\.contrib\.sqlalchemy module
+========================================
.. automodule:: oauth2client.contrib.sqlalchemy
:members:
diff --git a/docs/source/oauth2client.contrib.xsrfutil.rst b/docs/source/oauth2client.contrib.xsrfutil.rst
index dd5e8d6..eec497d 100644
--- a/docs/source/oauth2client.contrib.xsrfutil.rst
+++ b/docs/source/oauth2client.contrib.xsrfutil.rst
@@ -1,5 +1,5 @@
-oauth2client.contrib.xsrfutil module
-====================================
+oauth2client\.contrib\.xsrfutil module
+======================================
.. automodule:: oauth2client.contrib.xsrfutil
:members:
diff --git a/docs/source/oauth2client.crypt.rst b/docs/source/oauth2client.crypt.rst
index c3b6acc..d03cf50 100644
--- a/docs/source/oauth2client.crypt.rst
+++ b/docs/source/oauth2client.crypt.rst
@@ -1,5 +1,5 @@
-oauth2client.crypt module
-=========================
+oauth2client\.crypt module
+==========================
.. automodule:: oauth2client.crypt
:members:
diff --git a/docs/source/oauth2client.file.rst b/docs/source/oauth2client.file.rst
index 52a9e94..bf804ff 100644
--- a/docs/source/oauth2client.file.rst
+++ b/docs/source/oauth2client.file.rst
@@ -1,5 +1,5 @@
-oauth2client.file module
-========================
+oauth2client\.file module
+=========================
.. automodule:: oauth2client.file
:members:
diff --git a/docs/source/oauth2client.rst b/docs/source/oauth2client.rst
index 65de8ac..11f64e4 100644
--- a/docs/source/oauth2client.rst
+++ b/docs/source/oauth2client.rst
@@ -20,7 +20,6 @@ Submodules
oauth2client.service_account
oauth2client.tools
oauth2client.transport
- oauth2client.util
Module contents
---------------
diff --git a/docs/source/oauth2client.service_account.rst b/docs/source/oauth2client.service_account.rst
index 0d3b382..c370246 100644
--- a/docs/source/oauth2client.service_account.rst
+++ b/docs/source/oauth2client.service_account.rst
@@ -1,5 +1,5 @@
-oauth2client.service_account module
-===================================
+oauth2client\.service\_account module
+=====================================
.. automodule:: oauth2client.service_account
:members:
diff --git a/docs/source/oauth2client.tools.rst b/docs/source/oauth2client.tools.rst
index 240ad52..86be326 100644
--- a/docs/source/oauth2client.tools.rst
+++ b/docs/source/oauth2client.tools.rst
@@ -1,5 +1,5 @@
-oauth2client.tools module
-=========================
+oauth2client\.tools module
+==========================
.. automodule:: oauth2client.tools
:members:
diff --git a/docs/source/oauth2client.transport.rst b/docs/source/oauth2client.transport.rst
index 1c6dbb0..c5440f1 100644
--- a/docs/source/oauth2client.transport.rst
+++ b/docs/source/oauth2client.transport.rst
@@ -1,5 +1,5 @@
-oauth2client.transport module
-=============================
+oauth2client\.transport module
+==============================
.. automodule:: oauth2client.transport
:members:
diff --git a/docs/source/oauth2client.util.rst b/docs/source/oauth2client.util.rst
deleted file mode 100644
index 21dc8c8..0000000
--- a/docs/source/oauth2client.util.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-oauth2client.util module
-========================
-
-.. automodule:: oauth2client.util
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/oauth2client/__init__.py b/oauth2client/__init__.py
index 28384bb..92bc191 100644
--- a/oauth2client/__init__.py
+++ b/oauth2client/__init__.py
@@ -14,10 +14,11 @@
"""Client library for using OAuth2, especially with Google APIs."""
-__version__ = '3.0.0'
+__version__ = '4.1.3'
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/v2/auth'
-GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
-GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
-GOOGLE_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token'
-GOOGLE_TOKEN_INFO_URI = 'https://www.googleapis.com/oauth2/v3/tokeninfo'
+GOOGLE_DEVICE_URI = 'https://oauth2.googleapis.com/device/code'
+GOOGLE_REVOKE_URI = 'https://oauth2.googleapis.com/revoke'
+GOOGLE_TOKEN_URI = 'https://oauth2.googleapis.com/token'
+GOOGLE_TOKEN_INFO_URI = 'https://oauth2.googleapis.com/tokeninfo'
+
diff --git a/oauth2client/_helpers.py b/oauth2client/_helpers.py
index cb959c5..e912397 100644
--- a/oauth2client/_helpers.py
+++ b/oauth2client/_helpers.py
@@ -11,12 +11,248 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+
"""Helper functions for commonly used utilities."""
import base64
+import functools
+import inspect
import json
+import logging
+import os
+import warnings
import six
+from six.moves import urllib
+
+
+logger = logging.getLogger(__name__)
+
+POSITIONAL_WARNING = 'WARNING'
+POSITIONAL_EXCEPTION = 'EXCEPTION'
+POSITIONAL_IGNORE = 'IGNORE'
+POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
+ POSITIONAL_IGNORE])
+
+positional_parameters_enforcement = POSITIONAL_WARNING
+
+_SYM_LINK_MESSAGE = 'File: {0}: Is a symbolic link.'
+_IS_DIR_MESSAGE = '{0}: Is a directory'
+_MISSING_FILE_MESSAGE = 'Cannot access {0}: No such file or directory'
+
+
+def positional(max_positional_args):
+ """A decorator to declare that only the first N arguments my be positional.
+
+ This decorator makes it easy to support Python 3 style keyword-only
+ parameters. For example, in Python 3 it is possible to write::
+
+ def fn(pos1, *, kwonly1=None, kwonly1=None):
+ ...
+
+ All named parameters after ``*`` must be a keyword::
+
+ fn(10, 'kw1', 'kw2') # Raises exception.
+ fn(10, kwonly1='kw1') # Ok.
+
+ Example
+ ^^^^^^^
+
+ To define a function like above, do::
+
+ @positional(1)
+ def fn(pos1, kwonly1=None, kwonly2=None):
+ ...
+
+ If no default value is provided to a keyword argument, it becomes a
+ required keyword argument::
+
+ @positional(0)
+ def fn(required_kw):
+ ...
+
+ This must be called with the keyword parameter::
+
+ fn() # Raises exception.
+ fn(10) # Raises exception.
+ fn(required_kw=10) # Ok.
+
+ When defining instance or class methods always remember to account for
+ ``self`` and ``cls``::
+
+ class MyClass(object):
+
+ @positional(2)
+ def my_method(self, pos1, kwonly1=None):
+ ...
+
+ @classmethod
+ @positional(2)
+ def my_method(cls, pos1, kwonly1=None):
+ ...
+
+ The positional decorator behavior is controlled by
+ ``_helpers.positional_parameters_enforcement``, which may be set to
+ ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
+ ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
+ nothing, respectively, if a declaration is violated.
+
+ Args:
+ max_positional_arguments: Maximum number of positional arguments. All
+ parameters after the this index must be
+ keyword only.
+
+ Returns:
+ A decorator that prevents using arguments after max_positional_args
+ from being used as positional parameters.
+
+ Raises:
+ TypeError: if a key-word only argument is provided as a positional
+ parameter, but only if
+ _helpers.positional_parameters_enforcement is set to
+ POSITIONAL_EXCEPTION.
+ """
+
+ def positional_decorator(wrapped):
+ @functools.wraps(wrapped)
+ def positional_wrapper(*args, **kwargs):
+ if len(args) > max_positional_args:
+ plural_s = ''
+ if max_positional_args != 1:
+ plural_s = 's'
+ message = ('{function}() takes at most {args_max} positional '
+ 'argument{plural} ({args_given} given)'.format(
+ function=wrapped.__name__,
+ args_max=max_positional_args,
+ args_given=len(args),
+ plural=plural_s))
+ if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
+ raise TypeError(message)
+ elif positional_parameters_enforcement == POSITIONAL_WARNING:
+ logger.warning(message)
+ return wrapped(*args, **kwargs)
+ return positional_wrapper
+
+ if isinstance(max_positional_args, six.integer_types):
+ return positional_decorator
+ else:
+ args, _, _, defaults = inspect.getargspec(max_positional_args)
+ return positional(len(args) - len(defaults))(max_positional_args)
+
+
+def scopes_to_string(scopes):
+ """Converts scope value to a string.
+
+ If scopes is a string then it is simply passed through. If scopes is an
+ iterable then a string is returned that is all the individual scopes
+ concatenated with spaces.
+
+ Args:
+ scopes: string or iterable of strings, the scopes.
+
+ Returns:
+ The scopes formatted as a single string.
+ """
+ if isinstance(scopes, six.string_types):
+ return scopes
+ else:
+ return ' '.join(scopes)
+
+
+def string_to_scopes(scopes):
+ """Converts stringifed scope value to a list.
+
+ If scopes is a list then it is simply passed through. If scopes is an
+ string then a list of each individual scope is returned.
+
+ Args:
+ scopes: a string or iterable of strings, the scopes.
+
+ Returns:
+ The scopes in a list.
+ """
+ if not scopes:
+ return []
+ elif isinstance(scopes, six.string_types):
+ return scopes.split(' ')
+ else:
+ return scopes
+
+
+def parse_unique_urlencoded(content):
+ """Parses unique key-value parameters from urlencoded content.
+
+ Args:
+ content: string, URL-encoded key-value pairs.
+
+ Returns:
+ dict, The key-value pairs from ``content``.
+
+ Raises:
+ ValueError: if one of the keys is repeated.
+ """
+ urlencoded_params = urllib.parse.parse_qs(content)
+ params = {}
+ for key, value in six.iteritems(urlencoded_params):
+ if len(value) != 1:
+ msg = ('URL-encoded content contains a repeated value:'
+ '%s -> %s' % (key, ', '.join(value)))
+ raise ValueError(msg)
+ params[key] = value[0]
+ return params
+
+
+def update_query_params(uri, params):
+ """Updates a URI with new query parameters.
+
+ If a given key from ``params`` is repeated in the ``uri``, then
+ the URI will be considered invalid and an error will occur.
+
+ If the URI is valid, then each value from ``params`` will
+ replace the corresponding value in the query parameters (if
+ it exists).
+
+ Args:
+ uri: string, A valid URI, with potential existing query parameters.
+ params: dict, A dictionary of query parameters.
+
+ Returns:
+ The same URI but with the new query parameters added.
+ """
+ parts = urllib.parse.urlparse(uri)
+ query_params = parse_unique_urlencoded(parts.query)
+ query_params.update(params)
+ new_query = urllib.parse.urlencode(query_params)
+ new_parts = parts._replace(query=new_query)
+ return urllib.parse.urlunparse(new_parts)
+
+
+def _add_query_parameter(url, name, value):
+ """Adds a query parameter to a url.
+
+ Replaces the current value if it already exists in the URL.
+
+ Args:
+ url: string, url to add the query parameter to.
+ name: string, query parameter name.
+ value: string, query parameter value.
+
+ Returns:
+ Updated query parameter. Does not update the url if value is None.
+ """
+ if value is None:
+ return url
+ else:
+ return update_query_params(url, {name: value})
+
+
+def validate_file(filename):
+ if os.path.islink(filename):
+ raise IOError(_SYM_LINK_MESSAGE.format(filename))
+ elif os.path.isdir(filename):
+ raise IOError(_IS_DIR_MESSAGE.format(filename))
+ elif not os.path.isfile(filename):
+ warnings.warn(_MISSING_FILE_MESSAGE.format(filename))
def _parse_pem_key(raw_key_input):
diff --git a/oauth2client/_pkce.py b/oauth2client/_pkce.py
new file mode 100644
index 0000000..e4952d8
--- /dev/null
+++ b/oauth2client/_pkce.py
@@ -0,0 +1,67 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Utility functions for implementing Proof Key for Code Exchange (PKCE) by OAuth
+Public Clients
+
+See RFC7636.
+"""
+
+import base64
+import hashlib
+import os
+
+
+def code_verifier(n_bytes=64):
+ """
+ Generates a 'code_verifier' as described in section 4.1 of RFC 7636.
+
+ This is a 'high-entropy cryptographic random string' that will be
+ impractical for an attacker to guess.
+
+ Args:
+ n_bytes: integer between 31 and 96, inclusive. default: 64
+ number of bytes of entropy to include in verifier.
+
+ Returns:
+ Bytestring, representing urlsafe base64-encoded random data.
+ """
+ verifier = base64.urlsafe_b64encode(os.urandom(n_bytes)).rstrip(b'=')
+ # https://tools.ietf.org/html/rfc7636#section-4.1
+ # minimum length of 43 characters and a maximum length of 128 characters.
+ if len(verifier) < 43:
+ raise ValueError("Verifier too short. n_bytes must be > 30.")
+ elif len(verifier) > 128:
+ raise ValueError("Verifier too long. n_bytes must be < 97.")
+ else:
+ return verifier
+
+
+def code_challenge(verifier):
+ """
+ Creates a 'code_challenge' as described in section 4.2 of RFC 7636
+ by taking the sha256 hash of the verifier and then urlsafe
+ base64-encoding it.
+
+ Args:
+ verifier: bytestring, representing a code_verifier as generated by
+ code_verifier().
+
+ Returns:
+ Bytestring, representing a urlsafe base64-encoded sha256 hash digest,
+ without '=' padding.
+ """
+ digest = hashlib.sha256(verifier).digest()
+ return base64.urlsafe_b64encode(digest).rstrip(b'=')
diff --git a/oauth2client/client.py b/oauth2client/client.py
index 8956443..7618960 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -34,13 +34,11 @@ from six.moves import urllib
import oauth2client
from oauth2client import _helpers
+from oauth2client import _pkce
from oauth2client import clientsecrets
from oauth2client import transport
-from oauth2client import util
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
HAS_OPENSSL = False
HAS_CRYPTO = False
try:
@@ -100,20 +98,20 @@ AccessTokenInfo = collections.namedtuple(
DEFAULT_ENV_NAME = 'UNKNOWN'
# If set to True _get_environment avoid GCE check (_detect_gce_environment)
-NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False')
+NO_GCE_CHECK = os.getenv('NO_GCE_CHECK', 'False')
# Timeout in seconds to wait for the GCE metadata server when detecting the
# GCE environment.
try:
- GCE_METADATA_TIMEOUT = int(
- os.environ.setdefault('GCE_METADATA_TIMEOUT', '3'))
+ GCE_METADATA_TIMEOUT = int(os.getenv('GCE_METADATA_TIMEOUT', 3))
except ValueError: # pragma: NO COVER
GCE_METADATA_TIMEOUT = 3
_SERVER_SOFTWARE = 'SERVER_SOFTWARE'
-_GCE_METADATA_HOST = '169.254.169.254'
-_METADATA_FLAVOR_HEADER = 'Metadata-Flavor'
+_GCE_METADATA_URI = 'http://' + os.getenv('GCE_METADATA_IP', '169.254.169.254')
+_METADATA_FLAVOR_HEADER = 'metadata-flavor' # lowercase header
_DESIRED_METADATA_FLAVOR = 'Google'
+_GCE_HEADERS = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
# Expose utcnow() at module level to allow for
# easier testing (by replacing with a stub).
@@ -440,23 +438,6 @@ class Storage(object):
self.release_lock()
-def _update_query_params(uri, params):
- """Updates a URI with new query parameters.
-
- Args:
- uri: string, A valid URI, with potential existing query parameters.
- params: dict, A dictionary of query parameters.
-
- Returns:
- The same URI but with the new query parameters added.
- """
- parts = urllib.parse.urlparse(uri)
- query_params = dict(urllib.parse.parse_qsl(parts.query))
- query_params.update(params)
- new_parts = parts._replace(query=urllib.parse.urlencode(query_params))
- return urllib.parse.urlunparse(new_parts)
-
-
class OAuth2Credentials(Credentials):
"""Credentials object for OAuth 2.0.
@@ -466,11 +447,11 @@ class OAuth2Credentials(Credentials):
OAuth2Credentials objects may be safely pickled and unpickled.
"""
- @util.positional(8)
+ @_helpers.positional(8)
def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, revoke_uri=None,
id_token=None, token_response=None, scopes=None,
- token_info_uri=None):
+ token_info_uri=None, id_token_jwt=None):
"""Create an instance of OAuth2Credentials.
This constructor is not usually called by the user, instead
@@ -493,8 +474,11 @@ class OAuth2Credentials(Credentials):
because some providers (e.g. wordpress.com) include
extra fields that clients may want.
scopes: list, authorized scopes for these credentials.
- token_info_uri: string, the URI for the token info endpoint. Defaults
- to None; scopes can not be refreshed if this is None.
+ token_info_uri: string, the URI for the token info endpoint.
+ Defaults to None; scopes can not be refreshed if
+ this is None.
+ id_token_jwt: string, the encoded and signed identity JWT. The
+ decoded version of this is stored in id_token.
Notes:
store: callable, A callable that when passed a Credential
@@ -512,8 +496,9 @@ class OAuth2Credentials(Credentials):
self.user_agent = user_agent
self.revoke_uri = revoke_uri
self.id_token = id_token
+ self.id_token_jwt = id_token_jwt
self.token_response = token_response
- self.scopes = set(util.string_to_scopes(scopes or []))
+ self.scopes = set(_helpers.string_to_scopes(scopes or []))
self.token_info_uri = token_info_uri
# True if the credentials have been revoked or expired and can't be
@@ -557,7 +542,7 @@ class OAuth2Credentials(Credentials):
http: httplib2.Http, an http object to be used to make the refresh
request.
"""
- self._refresh(http.request)
+ self._refresh(http)
def revoke(self, http):
"""Revokes a refresh_token and makes the credentials void.
@@ -566,7 +551,7 @@ class OAuth2Credentials(Credentials):
http: httplib2.Http, an http object to be used to make the revoke
request.
"""
- self._revoke(http.request)
+ self._revoke(http)
def apply(self, headers):
"""Add the authorization to the headers.
@@ -592,7 +577,7 @@ class OAuth2Credentials(Credentials):
not have scopes. In both cases, you can use refresh_scopes() to
obtain the canonical set of scopes.
"""
- scopes = util.string_to_scopes(scopes)
+ scopes = _helpers.string_to_scopes(scopes)
return set(scopes).issubset(self.scopes)
def retrieve_scopes(self, http):
@@ -607,7 +592,7 @@ class OAuth2Credentials(Credentials):
Returns:
A set of strings containing the canonical list of scopes.
"""
- self._retrieve_scopes(http.request)
+ self._retrieve_scopes(http)
return self.scopes
@classmethod
@@ -640,6 +625,7 @@ class OAuth2Credentials(Credentials):
data['user_agent'],
revoke_uri=data.get('revoke_uri', None),
id_token=data.get('id_token', None),
+ id_token_jwt=data.get('id_token_jwt', None),
token_response=data.get('token_response', None),
scopes=data.get('scopes', None),
token_info_uri=data.get('token_info_uri', None))
@@ -746,7 +732,7 @@ class OAuth2Credentials(Credentials):
return headers
- def _refresh(self, http_request):
+ def _refresh(self, http):
"""Refreshes the access_token.
This method first checks by reading the Storage object if available.
@@ -754,15 +740,13 @@ class OAuth2Credentials(Credentials):
refresh is completed.
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- refresh request.
+ http: an object to be used to make HTTP requests.
Raises:
HttpAccessTokenRefreshError: When the refresh fails.
"""
if not self.store:
- self._do_refresh_request(http_request)
+ self._do_refresh_request(http)
else:
self.store.acquire_lock()
try:
@@ -774,17 +758,15 @@ class OAuth2Credentials(Credentials):
logger.info('Updated access_token read from Storage')
self._updateFromCredential(new_cred)
else:
- self._do_refresh_request(http_request)
+ self._do_refresh_request(http)
finally:
self.store.release_lock()
- def _do_refresh_request(self, http_request):
+ def _do_refresh_request(self, http):
"""Refresh the access_token using the refresh_token.
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- refresh request.
+ http: an object to be used to make HTTP requests.
Raises:
HttpAccessTokenRefreshError: When the refresh fails.
@@ -793,8 +775,9 @@ class OAuth2Credentials(Credentials):
headers = self._generate_refresh_request_headers()
logger.info('Refreshing access_token')
- resp, content = http_request(
- self.token_uri, method='POST', body=body, headers=headers)
+ resp, content = transport.request(
+ http, self.token_uri, method='POST',
+ body=body, headers=headers)
content = _helpers._from_bytes(content)
if resp.status == http_client.OK:
d = json.loads(content)
@@ -808,8 +791,10 @@ class OAuth2Credentials(Credentials):
self.token_expiry = None
if 'id_token' in d:
self.id_token = _extract_id_token(d['id_token'])
+ self.id_token_jwt = d['id_token']
else:
self.id_token = None
+ self.id_token_jwt = None
# On temporary refresh errors, the user does not actually have to
# re-authorize, so we unflag here.
self.invalid = False
@@ -819,7 +804,7 @@ class OAuth2Credentials(Credentials):
# An {'error':...} response body means the token is expired or
# revoked, so we flag the credentials as such.
logger.info('Failed to retrieve access token: %s', content)
- error_msg = 'Invalid response {0}.'.format(resp['status'])
+ error_msg = 'Invalid response {0}.'.format(resp.status)
try:
d = json.loads(content)
if 'error' in d:
@@ -833,23 +818,19 @@ class OAuth2Credentials(Credentials):
pass
raise HttpAccessTokenRefreshError(error_msg, status=resp.status)
- def _revoke(self, http_request):
+ def _revoke(self, http):
"""Revokes this credential and deletes the stored copy (if it exists).
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- revoke request.
+ http: an object to be used to make HTTP requests.
"""
- self._do_revoke(http_request, self.refresh_token or self.access_token)
+ self._do_revoke(http, self.refresh_token or self.access_token)
- def _do_revoke(self, http_request, token):
+ def _do_revoke(self, http, token):
"""Revokes this credential and deletes the stored copy (if it exists).
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- refresh request.
+ http: an object to be used to make HTTP requests.
token: A string used as the token to be revoked. Can be either an
access_token or refresh_token.
@@ -859,8 +840,13 @@ class OAuth2Credentials(Credentials):
"""
logger.info('Revoking token')
query_params = {'token': token}
- token_revoke_uri = _update_query_params(self.revoke_uri, query_params)
- resp, content = http_request(token_revoke_uri)
+ token_revoke_uri = _helpers.update_query_params(
+ self.revoke_uri, query_params)
+ resp, content = transport.request(http, token_revoke_uri)
+ if resp.status == http_client.METHOD_NOT_ALLOWED:
+ body = urllib.parse.urlencode(query_params)
+ resp, content = transport.request(http, token_revoke_uri,
+ method='POST', body=body)
if resp.status == http_client.OK:
self.invalid = True
else:
@@ -876,23 +862,19 @@ class OAuth2Credentials(Credentials):
if self.store:
self.store.delete()
- def _retrieve_scopes(self, http_request):
+ def _retrieve_scopes(self, http):
"""Retrieves the list of authorized scopes from the OAuth2 provider.
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- revoke request.
+ http: an object to be used to make HTTP requests.
"""
- self._do_retrieve_scopes(http_request, self.access_token)
+ self._do_retrieve_scopes(http, self.access_token)
- def _do_retrieve_scopes(self, http_request, token):
+ def _do_retrieve_scopes(self, http, token):
"""Retrieves the list of authorized scopes from the OAuth2 provider.
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- refresh request.
+ http: an object to be used to make HTTP requests.
token: A string used as the token to identify the credentials to
the provider.
@@ -902,13 +884,13 @@ class OAuth2Credentials(Credentials):
"""
logger.info('Refreshing scopes')
query_params = {'access_token': token, 'fields': 'scope'}
- token_info_uri = _update_query_params(self.token_info_uri,
- query_params)
- resp, content = http_request(token_info_uri)
+ token_info_uri = _helpers.update_query_params(
+ self.token_info_uri, query_params)
+ resp, content = transport.request(http, token_info_uri)
content = _helpers._from_bytes(content)
if resp.status == http_client.OK:
d = json.loads(content)
- self.scopes = set(util.string_to_scopes(d.get('scope', '')))
+ self.scopes = set(_helpers.string_to_scopes(d.get('scope', '')))
else:
error_msg = 'Invalid response {0}.'.format(resp.status)
try:
@@ -977,19 +959,25 @@ class AccessTokenCredentials(OAuth2Credentials):
data['user_agent'])
return retval
- def _refresh(self, http_request):
+ def _refresh(self, http):
+ """Refreshes the access token.
+
+ Args:
+ http: unused HTTP object.
+
+ Raises:
+ AccessTokenCredentialsError: always
+ """
raise AccessTokenCredentialsError(
'The access_token is expired or invalid and can\'t be refreshed.')
- def _revoke(self, http_request):
+ def _revoke(self, http):
"""Revokes the access_token and deletes the store if available.
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- revoke request.
+ http: an object to be used to make HTTP requests.
"""
- self._do_revoke(http_request, self.access_token)
+ self._do_revoke(http, self.access_token)
def _detect_gce_environment():
@@ -1005,21 +993,16 @@ def _detect_gce_environment():
# could lead to false negatives in the event that we are on GCE, but
# the metadata resolution was particularly slow. The latter case is
# "unlikely".
- connection = six.moves.http_client.HTTPConnection(
- _GCE_METADATA_HOST, timeout=GCE_METADATA_TIMEOUT)
-
+ http = transport.get_http_object(timeout=GCE_METADATA_TIMEOUT)
try:
- headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR}
- connection.request('GET', '/', headers=headers)
- response = connection.getresponse()
- if response.status == http_client.OK:
- return (response.getheader(_METADATA_FLAVOR_HEADER) ==
- _DESIRED_METADATA_FLAVOR)
+ response, _ = transport.request(
+ http, _GCE_METADATA_URI, headers=_GCE_HEADERS)
+ return (
+ response.status == http_client.OK and
+ response.get(_METADATA_FLAVOR_HEADER) == _DESIRED_METADATA_FLAVOR)
except socket.error: # socket.timeout or socket.error(64, 'Host is down')
logger.info('Timeout attempting to reach GCE metadata service.')
return False
- finally:
- connection.close()
def _in_gae_environment():
@@ -1469,7 +1452,7 @@ class AssertionCredentials(GoogleCredentials):
AssertionCredentials objects may be safely pickled and unpickled.
"""
- @util.positional(2)
+ @_helpers.positional(2)
def __init__(self, assertion_type, user_agent=None,
token_uri=oauth2client.GOOGLE_TOKEN_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
@@ -1511,15 +1494,13 @@ class AssertionCredentials(GoogleCredentials):
"""Generate assertion string to be used in the access token request."""
raise NotImplementedError
- def _revoke(self, http_request):
+ def _revoke(self, http):
"""Revokes the access_token and deletes the store if available.
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- revoke request.
+ http: an object to be used to make HTTP requests.
"""
- self._do_revoke(http_request, self.access_token)
+ self._do_revoke(http, self.access_token)
def sign_blob(self, blob):
"""Cryptographically sign a blob (of bytes).
@@ -1545,7 +1526,7 @@ def _require_crypto_or_die():
raise CryptoUnavailableError('No crypto library available')
-@util.positional(2)
+@_helpers.positional(2)
def verify_id_token(id_token, audience, http=None,
cert_uri=ID_TOKEN_VERIFICATION_CERTS):
"""Verifies a signed JWT id_token.
@@ -1572,7 +1553,7 @@ def verify_id_token(id_token, audience, http=None,
if http is None:
http = transport.get_cached_http()
- resp, content = http.request(cert_uri)
+ resp, content = transport.request(http, cert_uri)
if resp.status == http_client.OK:
certs = json.loads(_helpers._from_bytes(content))
return crypt.verify_signed_jwt_with_certs(id_token, certs, audience)
@@ -1624,7 +1605,7 @@ def _parse_exchange_token_response(content):
except Exception:
# different JSON libs raise different exceptions,
# so we just do a catch-all here
- resp = dict(urllib.parse.parse_qsl(content))
+ resp = _helpers.parse_unique_urlencoded(content)
# some providers respond with 'expires', others with 'expires_in'
if resp and 'expires' in resp:
@@ -1633,7 +1614,7 @@ def _parse_exchange_token_response(content):
return resp
-@util.positional(4)
+@_helpers.positional(4)
def credentials_from_code(client_id, client_secret, scope, code,
redirect_uri='postmessage', http=None,
user_agent=None,
@@ -1641,7 +1622,9 @@ def credentials_from_code(client_id, client_secret, scope, code,
auth_uri=oauth2client.GOOGLE_AUTH_URI,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
device_uri=oauth2client.GOOGLE_DEVICE_URI,
- token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI):
+ token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
+ pkce=False,
+ code_verifier=None):
"""Exchanges an authorization code for an OAuth2Credentials object.
Args:
@@ -1665,6 +1648,15 @@ def credentials_from_code(client_id, client_secret, scope, code,
device_uri: string, URI for device authorization endpoint. For
convenience defaults to Google's endpoints but any OAuth
2.0 provider can be used.
+ pkce: boolean, default: False, Generate and include a "Proof Key
+ for Code Exchange" (PKCE) with your authorization and token
+ requests. This adds security for installed applications that
+ cannot protect a client_secret. See RFC 7636 for details.
+ code_verifier: bytestring or None, default: None, parameter passed
+ as part of the code exchange when pkce=True. If
+ None, a code_verifier will automatically be
+ generated as part of step1_get_authorize_url(). See
+ RFC 7636 for details.
Returns:
An OAuth2Credentials object.
@@ -1675,16 +1667,20 @@ def credentials_from_code(client_id, client_secret, scope, code,
"""
flow = OAuth2WebServerFlow(client_id, client_secret, scope,
redirect_uri=redirect_uri,
- user_agent=user_agent, auth_uri=auth_uri,
- token_uri=token_uri, revoke_uri=revoke_uri,
+ user_agent=user_agent,
+ auth_uri=auth_uri,
+ token_uri=token_uri,
+ revoke_uri=revoke_uri,
device_uri=device_uri,
- token_info_uri=token_info_uri)
+ token_info_uri=token_info_uri,
+ pkce=pkce,
+ code_verifier=code_verifier)
credentials = flow.step2_exchange(code, http=http)
return credentials
-@util.positional(3)
+@_helpers.positional(3)
def credentials_from_clientsecrets_and_code(filename, scope, code,
message=None,
redirect_uri='postmessage',
@@ -1713,6 +1709,15 @@ def credentials_from_clientsecrets_and_code(filename, scope, code,
cache: An optional cache service client that implements get() and set()
methods. See clientsecrets.loadfile() for details.
device_uri: string, OAuth 2.0 device authorization endpoint
+ pkce: boolean, default: False, Generate and include a "Proof Key
+ for Code Exchange" (PKCE) with your authorization and token
+ requests. This adds security for installed applications that
+ cannot protect a client_secret. See RFC 7636 for details.
+ code_verifier: bytestring or None, default: None, parameter passed
+ as part of the code exchange when pkce=True. If
+ None, a code_verifier will automatically be
+ generated as part of step1_get_authorize_url(). See
+ RFC 7636 for details.
Returns:
An OAuth2Credentials object.
@@ -1803,7 +1808,7 @@ class OAuth2WebServerFlow(Flow):
OAuth2WebServerFlow objects may be safely pickled and unpickled.
"""
- @util.positional(4)
+ @_helpers.positional(4)
def __init__(self, client_id,
client_secret=None,
scope=None,
@@ -1816,6 +1821,8 @@ class OAuth2WebServerFlow(Flow):
device_uri=oauth2client.GOOGLE_DEVICE_URI,
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI,
authorization_header=None,
+ pkce=False,
+ code_verifier=None,
**kwargs):
"""Constructor for OAuth2WebServerFlow.
@@ -1853,6 +1860,15 @@ class OAuth2WebServerFlow(Flow):
require a client to authenticate using a
header value instead of passing client_secret
in the POST body.
+ pkce: boolean, default: False, Generate and include a "Proof Key
+ for Code Exchange" (PKCE) with your authorization and token
+ requests. This adds security for installed applications that
+ cannot protect a client_secret. See RFC 7636 for details.
+ code_verifier: bytestring or None, default: None, parameter passed
+ as part of the code exchange when pkce=True. If
+ None, a code_verifier will automatically be
+ generated as part of step1_get_authorize_url(). See
+ RFC 7636 for details.
**kwargs: dict, The keyword arguments are all optional and required
parameters for the OAuth calls.
"""
@@ -1862,7 +1878,7 @@ class OAuth2WebServerFlow(Flow):
raise TypeError("The value of scope must not be None")
self.client_id = client_id
self.client_secret = client_secret
- self.scope = util.scopes_to_string(scope)
+ self.scope = _helpers.scopes_to_string(scope)
self.redirect_uri = redirect_uri
self.login_hint = login_hint
self.user_agent = user_agent
@@ -1872,9 +1888,11 @@ class OAuth2WebServerFlow(Flow):
self.device_uri = device_uri
self.token_info_uri = token_info_uri
self.authorization_header = authorization_header
+ self._pkce = pkce
+ self.code_verifier = code_verifier
self.params = _oauth2_web_server_flow_params(kwargs)
- @util.positional(1)
+ @_helpers.positional(1)
def step1_get_authorize_url(self, redirect_uri=None, state=None):
"""Returns a URI to redirect to the provider.
@@ -1912,10 +1930,17 @@ class OAuth2WebServerFlow(Flow):
query_params['state'] = state
if self.login_hint is not None:
query_params['login_hint'] = self.login_hint
+ if self._pkce:
+ if not self.code_verifier:
+ self.code_verifier = _pkce.code_verifier()
+ challenge = _pkce.code_challenge(self.code_verifier)
+ query_params['code_challenge'] = challenge
+ query_params['code_challenge_method'] = 'S256'
+
query_params.update(self.params)
- return _update_query_params(self.auth_uri, query_params)
+ return _helpers.update_query_params(self.auth_uri, query_params)
- @util.positional(1)
+ @_helpers.positional(1)
def step1_get_device_and_user_codes(self, http=None):
"""Returns a user code and the verification URL where to enter it
@@ -1940,8 +1965,8 @@ class OAuth2WebServerFlow(Flow):
if http is None:
http = transport.get_http_object()
- resp, content = http.request(self.device_uri, method='POST', body=body,
- headers=headers)
+ resp, content = transport.request(
+ http, self.device_uri, method='POST', body=body, headers=headers)
content = _helpers._from_bytes(content)
if resp.status == http_client.OK:
try:
@@ -1963,7 +1988,7 @@ class OAuth2WebServerFlow(Flow):
pass
raise OAuth2DeviceCodeError(error_msg)
- @util.positional(2)
+ @_helpers.positional(2)
def step2_exchange(self, code=None, http=None, device_flow_info=None):
"""Exchanges a code for OAuth2Credentials.
@@ -2006,6 +2031,8 @@ class OAuth2WebServerFlow(Flow):
}
if self.client_secret is not None:
post_data['client_secret'] = self.client_secret
+ if self._pkce:
+ post_data['code_verifier'] = self.code_verifier
if device_flow_info is not None:
post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
else:
@@ -2023,8 +2050,8 @@ class OAuth2WebServerFlow(Flow):
if http is None:
http = transport.get_http_object()
- resp, content = http.request(self.token_uri, method='POST', body=body,
- headers=headers)
+ resp, content = transport.request(
+ http, self.token_uri, method='POST', body=body, headers=headers)
d = _parse_exchange_token_response(content)
if resp.status == http_client.OK and 'access_token' in d:
access_token = d['access_token']
@@ -2039,15 +2066,17 @@ class OAuth2WebServerFlow(Flow):
token_expiry = delta + _UTCNOW()
extracted_id_token = None
+ id_token_jwt = None
if 'id_token' in d:
extracted_id_token = _extract_id_token(d['id_token'])
+ id_token_jwt = d['id_token']
logger.info('Successfully retrieved access token')
return OAuth2Credentials(
access_token, self.client_id, self.client_secret,
refresh_token, token_expiry, self.token_uri, self.user_agent,
revoke_uri=self.revoke_uri, id_token=extracted_id_token,
- token_response=d, scopes=self.scope,
+ id_token_jwt=id_token_jwt, token_response=d, scopes=self.scope,
token_info_uri=self.token_info_uri)
else:
logger.info('Failed to retrieve access token: %s', content)
@@ -2060,10 +2089,11 @@ class OAuth2WebServerFlow(Flow):
raise FlowExchangeError(error_msg)
-@util.positional(2)
+@_helpers.positional(2)
def flow_from_clientsecrets(filename, scope, redirect_uri=None,
message=None, cache=None, login_hint=None,
- device_uri=None):
+ device_uri=None, pkce=None, code_verifier=None,
+ prompt=None):
"""Create a Flow from a clientsecrets file.
Will create the right kind of Flow based on the contents of the
@@ -2112,10 +2142,17 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
'login_hint': login_hint,
}
revoke_uri = client_info.get('revoke_uri')
- if revoke_uri is not None:
- constructor_kwargs['revoke_uri'] = revoke_uri
- if device_uri is not None:
- constructor_kwargs['device_uri'] = device_uri
+ optional = (
+ 'revoke_uri',
+ 'device_uri',
+ 'pkce',
+ 'code_verifier',
+ 'prompt'
+ )
+ for param in optional:
+ if locals()[param] is not None:
+ constructor_kwargs[param] = locals()[param]
+
return OAuth2WebServerFlow(
client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs)
diff --git a/oauth2client/clientsecrets.py b/oauth2client/clientsecrets.py
index 4b43e66..1598142 100644
--- a/oauth2client/clientsecrets.py
+++ b/oauth2client/clientsecrets.py
@@ -22,7 +22,6 @@ import json
import six
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
# Properties that make a client_secrets.json file valid.
TYPE_WEB = 'web'
diff --git a/oauth2client/contrib/_fcntl_opener.py b/oauth2client/contrib/_fcntl_opener.py
deleted file mode 100644
index ae6c85b..0000000
--- a/oauth2client/contrib/_fcntl_opener.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# Copyright 2016 Google Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import errno
-import fcntl
-import time
-
-from oauth2client.contrib import locked_file
-
-
-class _FcntlOpener(locked_file._Opener):
- """Open, lock, and unlock a file using fcntl.lockf."""
-
- def open_and_lock(self, timeout, delay):
- """Open the file and lock it.
-
- Args:
- timeout: float, How long to try to lock for.
- delay: float, How long to wait between retries
-
- Raises:
- AlreadyLockedException: if the lock is already acquired.
- IOError: if the open fails.
- CredentialsFileSymbolicLinkError: if the file is a symbolic
- link.
- """
- if self._locked:
- raise locked_file.AlreadyLockedException(
- 'File {0} is already locked'.format(self._filename))
- start_time = time.time()
-
- locked_file.validate_file(self._filename)
- try:
- self._fh = open(self._filename, self._mode)
- except IOError as e:
- # If we can't access with _mode, try _fallback_mode and
- # don't lock.
- if e.errno in (errno.EPERM, errno.EACCES):
- self._fh = open(self._filename, self._fallback_mode)
- return
-
- # We opened in _mode, try to lock the file.
- while True:
- try:
- fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
- self._locked = True
- return
- except IOError as e:
- # If not retrying, then just pass on the error.
- if timeout == 0:
- raise
- if e.errno != errno.EACCES:
- raise
- # We could not acquire the lock. Try again.
- if (time.time() - start_time) >= timeout:
- locked_file.logger.warn('Could not lock %s in %s seconds',
- self._filename, timeout)
- if self._fh:
- self._fh.close()
- self._fh = open(self._filename, self._fallback_mode)
- return
- time.sleep(delay)
-
- def unlock_and_close(self):
- """Close and unlock the file using the fcntl.lockf primitive."""
- if self._locked:
- fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
- self._locked = False
- if self._fh:
- self._fh.close()
diff --git a/oauth2client/contrib/_metadata.py b/oauth2client/contrib/_metadata.py
index 10e6a69..564cd39 100644
--- a/oauth2client/contrib/_metadata.py
+++ b/oauth2client/contrib/_metadata.py
@@ -19,29 +19,28 @@ See https://cloud.google.com/compute/docs/metadata
import datetime
import json
+import os
-import httplib2
from six.moves import http_client
from six.moves.urllib import parse as urlparse
from oauth2client import _helpers
from oauth2client import client
-from oauth2client import util
+from oauth2client import transport
-METADATA_ROOT = 'http://metadata.google.internal/computeMetadata/v1/'
+METADATA_ROOT = 'http://{}/computeMetadata/v1/'.format(
+ os.getenv('GCE_METADATA_ROOT', 'metadata.google.internal'))
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}
-def get(http_request, path, root=METADATA_ROOT, recursive=None):
+def get(http, path, root=METADATA_ROOT, recursive=None):
"""Fetch a resource from the metadata server.
Args:
+ http: an object to be used to make HTTP requests.
path: A string indicating the resource to retrieve. For example,
- 'instance/service-accounts/defualt'
- http_request: A callable that matches the method
- signature of httplib2.Http.request. Used to make the request to the
- metadataserver.
+ 'instance/service-accounts/default'
root: A string indicating the full path to the metadata server root.
recursive: A boolean indicating whether to do a recursive query of
metadata. See
@@ -51,15 +50,14 @@ def get(http_request, path, root=METADATA_ROOT, recursive=None):
A dictionary if the metadata server returns JSON, otherwise a string.
Raises:
- httplib2.Httplib2Error if an error corrured while retrieving metadata.
+ http_client.HTTPException if an error corrured while
+ retrieving metadata.
"""
url = urlparse.urljoin(root, path)
- url = util._add_query_parameter(url, 'recursive', recursive)
+ url = _helpers._add_query_parameter(url, 'recursive', recursive)
- response, content = http_request(
- url,
- headers=METADATA_HEADERS
- )
+ response, content = transport.request(
+ http, url, headers=METADATA_HEADERS)
if response.status == http_client.OK:
decoded = _helpers._from_bytes(content)
@@ -68,21 +66,20 @@ def get(http_request, path, root=METADATA_ROOT, recursive=None):
else:
return decoded
else:
- raise httplib2.HttpLib2Error(
+ raise http_client.HTTPException(
'Failed to retrieve {0} from the Google Compute Engine'
'metadata service. Response:\n{1}'.format(url, response))
-def get_service_account_info(http_request, service_account='default'):
+def get_service_account_info(http, service_account='default'):
"""Get information about a service account from the metadata server.
Args:
+ http: an object to be used to make HTTP requests.
service_account: An email specifying the service account for which to
look up information. Default will be information for the "default"
service account of the current compute engine instance.
- http_request: A callable that matches the method
- signature of httplib2.Http.request. Used to make the request to the
- metadata server.
+
Returns:
A dictionary with information about the specified service account,
for example:
@@ -94,21 +91,19 @@ def get_service_account_info(http_request, service_account='default'):
}
"""
return get(
- http_request,
+ http,
'instance/service-accounts/{0}/'.format(service_account),
recursive=True)
-def get_token(http_request, service_account='default'):
+def get_token(http, service_account='default'):
"""Fetch an oauth token for the
Args:
+ http: an object to be used to make HTTP requests.
service_account: An email specifying the service account this token
should represent. Default will be a token for the "default" service
account of the current compute engine instance.
- http_request: A callable that matches the method
- signature of httplib2.Http.request. Used to make the request to the
- metadataserver.
Returns:
A tuple of (access token, token expiration), where access token is the
@@ -116,7 +111,7 @@ def get_token(http_request, service_account='default'):
that indicates when the access token will expire.
"""
token_json = get(
- http_request,
+ http,
'instance/service-accounts/{0}/token'.format(service_account))
token_expiry = client._UTCNOW() + datetime.timedelta(
seconds=token_json['expires_in'])
diff --git a/oauth2client/contrib/_win32_opener.py b/oauth2client/contrib/_win32_opener.py
deleted file mode 100644
index 34b4f48..0000000
--- a/oauth2client/contrib/_win32_opener.py
+++ /dev/null
@@ -1,106 +0,0 @@
-# Copyright 2016 Google Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import errno
-import time
-
-import pywintypes
-import win32con
-import win32file
-
-from oauth2client.contrib import locked_file
-
-
-class _Win32Opener(locked_file._Opener):
- """Open, lock, and unlock a file using windows primitives."""
-
- # Error #33:
- # 'The process cannot access the file because another process'
- FILE_IN_USE_ERROR = 33
-
- # Error #158:
- # 'The segment is already unlocked.'
- FILE_ALREADY_UNLOCKED_ERROR = 158
-
- def open_and_lock(self, timeout, delay):
- """Open the file and lock it.
-
- Args:
- timeout: float, How long to try to lock for.
- delay: float, How long to wait between retries
-
- Raises:
- AlreadyLockedException: if the lock is already acquired.
- IOError: if the open fails.
- CredentialsFileSymbolicLinkError: if the file is a symbolic
- link.
- """
- if self._locked:
- raise locked_file.AlreadyLockedException(
- 'File {0} is already locked'.format(self._filename))
- start_time = time.time()
-
- locked_file.validate_file(self._filename)
- try:
- self._fh = open(self._filename, self._mode)
- except IOError as e:
- # If we can't access with _mode, try _fallback_mode
- # and don't lock.
- if e.errno == errno.EACCES:
- self._fh = open(self._filename, self._fallback_mode)
- return
-
- # We opened in _mode, try to lock the file.
- while True:
- try:
- hfile = win32file._get_osfhandle(self._fh.fileno())
- win32file.LockFileEx(
- hfile,
- (win32con.LOCKFILE_FAIL_IMMEDIATELY |
- win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
- pywintypes.OVERLAPPED())
- self._locked = True
- return
- except pywintypes.error as e:
- if timeout == 0:
- raise
-
- # If the error is not that the file is already
- # in use, raise.
- if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
- raise
-
- # We could not acquire the lock. Try again.
- if (time.time() - start_time) >= timeout:
- locked_file.logger.warn('Could not lock %s in %s seconds',
- self._filename, timeout)
- if self._fh:
- self._fh.close()
- self._fh = open(self._filename, self._fallback_mode)
- return
- time.sleep(delay)
-
- def unlock_and_close(self):
- """Close and unlock the file using the win32 primitive."""
- if self._locked:
- try:
- hfile = win32file._get_osfhandle(self._fh.fileno())
- win32file.UnlockFileEx(hfile, 0, -0x10000,
- pywintypes.OVERLAPPED())
- except pywintypes.error as e:
- if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
- raise
- self._locked = False
- if self._fh:
- self._fh.close()
diff --git a/oauth2client/contrib/appengine.py b/oauth2client/contrib/appengine.py
index 661105e..c1326ee 100644
--- a/oauth2client/contrib/appengine.py
+++ b/oauth2client/contrib/appengine.py
@@ -29,13 +29,13 @@ from google.appengine.api import memcache
from google.appengine.api import users
from google.appengine.ext import db
from google.appengine.ext.webapp.util import login_required
-import httplib2
import webapp2 as webapp
import oauth2client
+from oauth2client import _helpers
from oauth2client import client
from oauth2client import clientsecrets
-from oauth2client import util
+from oauth2client import transport
from oauth2client.contrib import xsrfutil
# This is a temporary fix for a Google internal issue.
@@ -45,8 +45,6 @@ except ImportError: # pragma: NO COVER
_appengine_ndb = None
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
logger = logging.getLogger(__name__)
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
@@ -131,7 +129,7 @@ class AppAssertionCredentials(client.AssertionCredentials):
information to generate and refresh its own access tokens.
"""
- @util.positional(2)
+ @_helpers.positional(2)
def __init__(self, scope, **kwargs):
"""Constructor for AppAssertionCredentials
@@ -143,7 +141,7 @@ class AppAssertionCredentials(client.AssertionCredentials):
or unspecified, the default service account for
the app is used.
"""
- self.scope = util.scopes_to_string(scope)
+ self.scope = _helpers.scopes_to_string(scope)
self._kwargs = kwargs
self.service_account_id = kwargs.get('service_account_id', None)
self._service_account_email = None
@@ -157,17 +155,15 @@ class AppAssertionCredentials(client.AssertionCredentials):
data = json.loads(json_data)
return AppAssertionCredentials(data['scope'])
- def _refresh(self, http_request):
- """Refreshes the access_token.
+ def _refresh(self, http):
+ """Refreshes the access token.
Since the underlying App Engine app_identity implementation does its
own caching we can skip all the storage hoops and just to a refresh
using the API.
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- refresh request.
+ http: unused HTTP object
Raises:
AccessTokenRefreshError: When the refresh fails.
@@ -305,7 +301,7 @@ class StorageByKeyName(client.Storage):
and that entities are stored by key_name.
"""
- @util.positional(4)
+ @_helpers.positional(4)
def __init__(self, model, key_name, property_name, cache=None, user=None):
"""Constructor for Storage.
@@ -523,7 +519,7 @@ class OAuth2Decorator(object):
flow = property(get_flow, set_flow)
- @util.positional(4)
+ @_helpers.positional(4)
def __init__(self, client_id, client_secret, scope,
auth_uri=oauth2client.GOOGLE_AUTH_URI,
token_uri=oauth2client.GOOGLE_TOKEN_URI,
@@ -590,7 +586,7 @@ class OAuth2Decorator(object):
self.credentials = None
self._client_id = client_id
self._client_secret = client_secret
- self._scope = util.scopes_to_string(scope)
+ self._scope = _helpers.scopes_to_string(scope)
self._auth_uri = auth_uri
self._token_uri = token_uri
self._revoke_uri = revoke_uri
@@ -742,7 +738,8 @@ class OAuth2Decorator(object):
*args: Positional arguments passed to httplib2.Http constructor.
**kwargs: Positional arguments passed to httplib2.Http constructor.
"""
- return self.credentials.authorize(httplib2.Http(*args, **kwargs))
+ return self.credentials.authorize(
+ transport.get_http_object(*args, **kwargs))
@property
def callback_path(self):
@@ -804,7 +801,7 @@ class OAuth2Decorator(object):
if (decorator._token_response_param and
credentials.token_response):
resp_json = json.dumps(credentials.token_response)
- redirect_uri = util._add_query_parameter(
+ redirect_uri = _helpers._add_query_parameter(
redirect_uri, decorator._token_response_param,
resp_json)
@@ -848,7 +845,7 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
"""
- @util.positional(3)
+ @_helpers.positional(3)
def __init__(self, filename, scope, message=None, cache=None, **kwargs):
"""Constructor
@@ -891,7 +888,7 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
self._message = 'Please configure your application for OAuth 2.0.'
-@util.positional(2)
+@_helpers.positional(2)
def oauth2decorator_from_clientsecrets(filename, scope,
message=None, cache=None):
"""Creates an OAuth2Decorator populated from a clientsecrets file.
diff --git a/oauth2client/contrib/devshell.py b/oauth2client/contrib/devshell.py
index b8bb978..691765f 100644
--- a/oauth2client/contrib/devshell.py
+++ b/oauth2client/contrib/devshell.py
@@ -37,6 +37,7 @@ class CommunicationError(Error):
class NoDevshellServer(Error):
"""Error when no Developer Shell server can be contacted."""
+
# The request for credential information to the Developer Shell client socket
# is always an empty PBLite-formatted JSON object, so just define it as a
# constant.
@@ -117,7 +118,12 @@ class DevshellCredentials(client.GoogleCredentials):
user_agent)
self._refresh(None)
- def _refresh(self, http_request):
+ def _refresh(self, http):
+ """Refreshes the access token.
+
+ Args:
+ http: unused HTTP object
+ """
self.devshell_response = _SendRecv()
self.access_token = self.devshell_response.access_token
expires_in = self.devshell_response.expires_in
diff --git a/oauth2client/contrib/django_util/__init__.py b/oauth2client/contrib/django_util/__init__.py
index 5449e32..644a8f9 100644
--- a/oauth2client/contrib/django_util/__init__.py
+++ b/oauth2client/contrib/django_util/__init__.py
@@ -52,6 +52,9 @@ Add the helper to your INSTALLED_APPS:
This helper also requires the Django Session Middleware, so
``django.contrib.sessions.middleware`` should be in INSTALLED_APPS as well.
+MIDDLEWARE or MIDDLEWARE_CLASSES (in Django versions <1.10) should also
+contain the string 'django.contrib.sessions.middleware.SessionMiddleware'.
+
Add the client secrets created earlier to the settings. You can either
specify the path to the credentials file in JSON format
@@ -228,10 +231,10 @@ import importlib
import django.conf
from django.core import exceptions
from django.core import urlresolvers
-import httplib2
from six.moves.urllib import parse
from oauth2client import clientsecrets
+from oauth2client import transport
from oauth2client.contrib import dictionary_storage
from oauth2client.contrib.django_util import storage
@@ -335,16 +338,26 @@ class OAuth2Settings(object):
self.request_prefix = getattr(settings_instance,
'GOOGLE_OAUTH2_REQUEST_ATTRIBUTE',
GOOGLE_OAUTH2_REQUEST_ATTRIBUTE)
- self.client_id, self.client_secret = \
- _get_oauth2_client_id_and_secret(settings_instance)
+ info = _get_oauth2_client_id_and_secret(settings_instance)
+ self.client_id, self.client_secret = info
+
+ # Django 1.10 deprecated MIDDLEWARE_CLASSES in favor of MIDDLEWARE
+ middleware_settings = getattr(settings_instance, 'MIDDLEWARE', None)
+ if middleware_settings is None:
+ middleware_settings = getattr(
+ settings_instance, 'MIDDLEWARE_CLASSES', None)
+ if middleware_settings is None:
+ raise exceptions.ImproperlyConfigured(
+ 'Django settings has neither MIDDLEWARE nor MIDDLEWARE_CLASSES'
+ 'configured')
- if ('django.contrib.sessions.middleware.SessionMiddleware'
- not in settings_instance.MIDDLEWARE_CLASSES):
+ if ('django.contrib.sessions.middleware.SessionMiddleware' not in
+ middleware_settings):
raise exceptions.ImproperlyConfigured(
- 'The Google OAuth2 Helper requires session middleware to '
- 'be installed. Edit your MIDDLEWARE_CLASSES setting'
- ' to include \'django.contrib.sessions.middleware.'
- 'SessionMiddleware\'.')
+ 'The Google OAuth2 Helper requires session middleware to '
+ 'be installed. Edit your MIDDLEWARE_CLASSES or MIDDLEWARE '
+ 'setting to include \'django.contrib.sessions.middleware.'
+ 'SessionMiddleware\'.')
(self.storage_model, self.storage_model_user_property,
self.storage_model_credentials_property) = _get_storage_model()
@@ -470,8 +483,7 @@ class UserOAuth2(object):
@property
def http(self):
- """Helper method to create an HTTP client authorized with OAuth2
- credentials."""
+ """Helper: create HTTP client authorized with OAuth2 credentials."""
if self.has_credentials():
- return self.credentials.authorize(httplib2.Http())
+ return self.credentials.authorize(transport.get_http_object())
return None
diff --git a/oauth2client/contrib/django_util/models.py b/oauth2client/contrib/django_util/models.py
index 87e1da7..37cc697 100644
--- a/oauth2client/contrib/django_util/models.py
+++ b/oauth2client/contrib/django_util/models.py
@@ -19,6 +19,7 @@ import pickle
from django.db import models
from django.utils import encoding
+import jsonpickle
import oauth2client
@@ -48,7 +49,12 @@ class CredentialsField(models.Field):
elif isinstance(value, oauth2client.client.Credentials):
return value
else:
- return pickle.loads(base64.b64decode(encoding.smart_bytes(value)))
+ try:
+ return jsonpickle.decode(
+ base64.b64decode(encoding.smart_bytes(value)).decode())
+ except ValueError:
+ return pickle.loads(
+ base64.b64decode(encoding.smart_bytes(value)))
def get_prep_value(self, value):
"""Overrides ``models.Field`` method. This is used to convert
@@ -58,7 +64,8 @@ class CredentialsField(models.Field):
if value is None:
return None
else:
- return encoding.smart_text(base64.b64encode(pickle.dumps(value)))
+ return encoding.smart_text(
+ base64.b64encode(jsonpickle.encode(value).encode()))
def value_to_string(self, obj):
"""Convert the field value from the provided model to a string.
diff --git a/oauth2client/contrib/django_util/views.py b/oauth2client/contrib/django_util/views.py
index 4d8ae03..1835208 100644
--- a/oauth2client/contrib/django_util/views.py
+++ b/oauth2client/contrib/django_util/views.py
@@ -22,13 +22,14 @@ in the configured storage."""
import hashlib
import json
import os
-import pickle
from django import http
from django import shortcuts
from django.conf import settings
from django.core import urlresolvers
from django.shortcuts import redirect
+from django.utils import html
+import jsonpickle
from six.moves.urllib import parse
from oauth2client import client
@@ -71,7 +72,7 @@ def _make_flow(request, scopes, return_url=None):
urlresolvers.reverse("google_oauth:callback")))
flow_key = _FLOW_KEY.format(csrf_token)
- request.session[flow_key] = pickle.dumps(flow)
+ request.session[flow_key] = jsonpickle.encode(flow)
return flow
@@ -89,7 +90,7 @@ def _get_flow_for_token(csrf_token, request):
CSRF token.
"""
flow_pickle = request.session.get(_FLOW_KEY.format(csrf_token), None)
- return None if flow_pickle is None else pickle.loads(flow_pickle)
+ return None if flow_pickle is None else jsonpickle.decode(flow_pickle)
def oauth2_callback(request):
@@ -109,6 +110,7 @@ def oauth2_callback(request):
if 'error' in request.GET:
reason = request.GET.get(
'error_description', request.GET.get('error', ''))
+ reason = html.escape(reason)
return http.HttpResponseBadRequest(
'Authorization failed {0}'.format(reason))
@@ -170,7 +172,10 @@ def oauth2_authorize(request):
A redirect to Google OAuth2 Authorization.
"""
return_url = request.GET.get('return_url', None)
+ if not return_url:
+ return_url = request.META.get('HTTP_REFERER', '/')
+ scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
# Model storage (but not session storage) requires a logged in user
if django_util.oauth2_settings.storage_model:
if not request.user.is_authenticated():
@@ -178,13 +183,11 @@ def oauth2_authorize(request):
settings.LOGIN_URL, parse.quote(request.get_full_path())))
# This checks for the case where we ended up here because of a logged
# out user but we had credentials for it in the first place
- elif get_storage(request).get() is not None:
- return redirect(return_url)
+ else:
+ user_oauth = django_util.UserOAuth2(request, scopes, return_url)
+ if user_oauth.has_credentials():
+ return redirect(return_url)
- scopes = request.GET.getlist('scopes', django_util.oauth2_settings.scopes)
-
- if not return_url:
- return_url = request.META.get('HTTP_REFERER', '/')
flow = _make_flow(request=request, scopes=scopes, return_url=return_url)
auth_url = flow.step1_get_authorize_url()
return shortcuts.redirect(auth_url)
diff --git a/oauth2client/contrib/flask_util.py b/oauth2client/contrib/flask_util.py
index 47c3df1..fabd613 100644
--- a/oauth2client/contrib/flask_util.py
+++ b/oauth2client/contrib/flask_util.py
@@ -176,19 +176,18 @@ try:
from flask import request
from flask import session
from flask import url_for
+ import markupsafe
except ImportError: # pragma: NO COVER
raise ImportError('The flask utilities require flask 0.9 or newer.')
-import httplib2
import six.moves.http_client as httplib
from oauth2client import client
from oauth2client import clientsecrets
+from oauth2client import transport
from oauth2client.contrib import dictionary_storage
-__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
-
_DEFAULT_SCOPES = ('email',)
_CREDENTIALS_KEY = 'google_oauth2_credentials'
_FLOW_KEY = 'google_oauth2_flow_{0}'
@@ -390,6 +389,7 @@ class UserOAuth2(object):
if 'error' in request.args:
reason = request.args.get(
'error_description', request.args.get('error', ''))
+ reason = markupsafe.escape(reason)
return ('Authorization failed: {0}'.format(reason),
httplib.BAD_REQUEST)
@@ -553,4 +553,5 @@ class UserOAuth2(object):
"""
if not self.credentials:
raise ValueError('No credentials available.')
- return self.credentials.authorize(httplib2.Http(*args, **kwargs))
+ return self.credentials.authorize(
+ transport.get_http_object(*args, **kwargs))
diff --git a/oauth2client/contrib/gce.py b/oauth2client/contrib/gce.py
index f3a6ca1..aaab15f 100644
--- a/oauth2client/contrib/gce.py
+++ b/oauth2client/contrib/gce.py
@@ -20,14 +20,12 @@ Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
import logging
import warnings
-import httplib2
+from six.moves import http_client
from oauth2client import client
from oauth2client.contrib import _metadata
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
logger = logging.getLogger(__name__)
_SCOPES_WARNING = """\
@@ -98,44 +96,40 @@ class AppAssertionCredentials(client.AssertionCredentials):
Returns:
A set of strings containing the canonical list of scopes.
"""
- self._retrieve_info(http.request)
+ self._retrieve_info(http)
return self.scopes
- def _retrieve_info(self, http_request):
- """Validates invalid service accounts by retrieving service account info.
+ def _retrieve_info(self, http):
+ """Retrieves service account info for invalid credentials.
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make the
- request to the metadata server
+ http: an object to be used to make HTTP requests.
"""
if self.invalid:
info = _metadata.get_service_account_info(
- http_request,
+ http,
service_account=self.service_account_email or 'default')
self.invalid = False
self.service_account_email = info['email']
self.scopes = info['scopes']
- def _refresh(self, http_request):
- """Refreshes the access_token.
+ def _refresh(self, http):
+ """Refreshes the access token.
Skip all the storage hoops and just refresh using the API.
Args:
- http_request: callable, a callable that matches the method
- signature of httplib2.Http.request, used to make
- the refresh request.
+ http: an object to be used to make HTTP requests.
Raises:
HttpAccessTokenRefreshError: When the refresh fails.
"""
try:
- self._retrieve_info(http_request)
+ self._retrieve_info(http)
self.access_token, self.token_expiry = _metadata.get_token(
- http_request, service_account=self.service_account_email)
- except httplib2.HttpLib2Error as e:
- raise client.HttpAccessTokenRefreshError(str(e))
+ http, service_account=self.service_account_email)
+ except http_client.HTTPException as err:
+ raise client.HttpAccessTokenRefreshError(str(err))
@property
def serialization_data(self):
diff --git a/oauth2client/contrib/keyring_storage.py b/oauth2client/contrib/keyring_storage.py
index f4f2e30..4af9448 100644
--- a/oauth2client/contrib/keyring_storage.py
+++ b/oauth2client/contrib/keyring_storage.py
@@ -24,9 +24,6 @@ import keyring
from oauth2client import client
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
-
class Storage(client.Storage):
"""Store and retrieve a single credential to and from the keyring.
diff --git a/oauth2client/contrib/locked_file.py b/oauth2client/contrib/locked_file.py
deleted file mode 100644
index 0d28ebb..0000000
--- a/oauth2client/contrib/locked_file.py
+++ /dev/null
@@ -1,234 +0,0 @@
-# Copyright 2014 Google Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Locked file interface that should work on Unix and Windows pythons.
-
-This module first tries to use fcntl locking to ensure serialized access
-to a file, then falls back on a lock file if that is unavialable.
-
-Usage::
-
- f = LockedFile('filename', 'r+b', 'rb')
- f.open_and_lock()
- if f.is_locked():
- print('Acquired filename with r+b mode')
- f.file_handle().write('locked data')
- else:
- print('Acquired filename with rb mode')
- f.unlock_and_close()
-
-"""
-
-from __future__ import print_function
-
-import errno
-import logging
-import os
-import time
-
-from oauth2client import util
-
-
-__author__ = 'cache@google.com (David T McWherter)'
-
-logger = logging.getLogger(__name__)
-
-
-class CredentialsFileSymbolicLinkError(Exception):
- """Credentials files must not be symbolic links."""
-
-
-class AlreadyLockedException(Exception):
- """Trying to lock a file that has already been locked by the LockedFile."""
- pass
-
-
-def validate_file(filename):
- if os.path.islink(filename):
- raise CredentialsFileSymbolicLinkError(
- 'File: {0} is a symbolic link.'.format(filename))
-
-
-class _Opener(object):
- """Base class for different locking primitives."""
-
- def __init__(self, filename, mode, fallback_mode):
- """Create an Opener.
-
- Args:
- filename: string, The pathname of the file.
- mode: string, The preferred mode to access the file with.
- fallback_mode: string, The mode to use if locking fails.
- """
- self._locked = False
- self._filename = filename
- self._mode = mode
- self._fallback_mode = fallback_mode
- self._fh = None
- self._lock_fd = None
-
- def is_locked(self):
- """Was the file locked."""
- return self._locked
-
- def file_handle(self):
- """The file handle to the file. Valid only after opened."""
- return self._fh
-
- def filename(self):
- """The filename that is being locked."""
- return self._filename
-
- def open_and_lock(self, timeout, delay):
- """Open the file and lock it.
-
- Args:
- timeout: float, How long to try to lock for.
- delay: float, How long to wait between retries.
- """
- pass
-
- def unlock_and_close(self):
- """Unlock and close the file."""
- pass
-
-
-class _PosixOpener(_Opener):
- """Lock files using Posix advisory lock files."""
-
- def open_and_lock(self, timeout, delay):
- """Open the file and lock it.
-
- Tries to create a .lock file next to the file we're trying to open.
-
- Args:
- timeout: float, How long to try to lock for.
- delay: float, How long to wait between retries.
-
- Raises:
- AlreadyLockedException: if the lock is already acquired.
- IOError: if the open fails.
- CredentialsFileSymbolicLinkError if the file is a symbolic link.
- """
- if self._locked:
- raise AlreadyLockedException(
- 'File {0} is already locked'.format(self._filename))
- self._locked = False
-
- validate_file(self._filename)
- try:
- self._fh = open(self._filename, self._mode)
- except IOError as e:
- # If we can't access with _mode, try _fallback_mode and don't lock.
- if e.errno == errno.EACCES:
- self._fh = open(self._filename, self._fallback_mode)
- return
-
- lock_filename = self._posix_lockfile(self._filename)
- start_time = time.time()
- while True:
- try:
- self._lock_fd = os.open(lock_filename,
- os.O_CREAT | os.O_EXCL | os.O_RDWR)
- self._locked = True
- break
-
- except OSError as e:
- if e.errno != errno.EEXIST:
- raise
- if (time.time() - start_time) >= timeout:
- logger.warn('Could not acquire lock %s in %s seconds',
- lock_filename, timeout)
- # Close the file and open in fallback_mode.
- if self._fh:
- self._fh.close()
- self._fh = open(self._filename, self._fallback_mode)
- return
- time.sleep(delay)
-
- def unlock_and_close(self):
- """Unlock a file by removing the .lock file, and close the handle."""
- if self._locked:
- lock_filename = self._posix_lockfile(self._filename)
- os.close(self._lock_fd)
- os.unlink(lock_filename)
- self._locked = False
- self._lock_fd = None
- if self._fh:
- self._fh.close()
-
- def _posix_lockfile(self, filename):
- """The name of the lock file to use for posix locking."""
- return '{0}.lock'.format(filename)
-
-
-class LockedFile(object):
- """Represent a file that has exclusive access."""
-
- @util.positional(4)
- def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
- """Construct a LockedFile.
-
- Args:
- filename: string, The path of the file to open.
- mode: string, The mode to try to open the file with.
- fallback_mode: string, The mode to use if locking fails.
- use_native_locking: bool, Whether or not fcntl/win32 locking is
- used.
- """
- opener = None
- if not opener and use_native_locking:
- try:
- from oauth2client.contrib._win32_opener import _Win32Opener
- opener = _Win32Opener(filename, mode, fallback_mode)
- except ImportError:
- try:
- from oauth2client.contrib._fcntl_opener import _FcntlOpener
- opener = _FcntlOpener(filename, mode, fallback_mode)
- except ImportError:
- pass
-
- if not opener:
- opener = _PosixOpener(filename, mode, fallback_mode)
-
- self._opener = opener
-
- def filename(self):
- """Return the filename we were constructed with."""
- return self._opener._filename
-
- def file_handle(self):
- """Return the file_handle to the opened file."""
- return self._opener.file_handle()
-
- def is_locked(self):
- """Return whether we successfully locked the file."""
- return self._opener.is_locked()
-
- def open_and_lock(self, timeout=0, delay=0.05):
- """Open the file, trying to lock it.
-
- Args:
- timeout: float, The number of seconds to try to acquire the lock.
- delay: float, The number of seconds to wait between retry attempts.
-
- Raises:
- AlreadyLockedException: if the lock is already acquired.
- IOError: if the open fails.
- """
- self._opener.open_and_lock(timeout, delay)
-
- def unlock_and_close(self):
- """Unlock and close a file."""
- self._opener.unlock_and_close()
diff --git a/oauth2client/contrib/multistore_file.py b/oauth2client/contrib/multistore_file.py
deleted file mode 100644
index 10f4cb4..0000000
--- a/oauth2client/contrib/multistore_file.py
+++ /dev/null
@@ -1,505 +0,0 @@
-# Copyright 2014 Google Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Multi-credential file store with lock support.
-
-This module implements a JSON credential store where multiple
-credentials can be stored in one file. That file supports locking
-both in a single process and across processes.
-
-The credential themselves are keyed off of:
-
-* client_id
-* user_agent
-* scope
-
-The format of the stored data is like so::
-
- {
- 'file_version': 1,
- 'data': [
- {
- 'key': {
- 'clientId': '<client id>',
- 'userAgent': '<user agent>',
- 'scope': '<scope>'
- },
- 'credential': {
- # JSON serialized Credentials.
- }
- }
- ]
- }
-
-"""
-
-import errno
-import json
-import logging
-import os
-import threading
-
-from oauth2client import client
-from oauth2client import util
-from oauth2client.contrib import locked_file
-
-__author__ = 'jbeda@google.com (Joe Beda)'
-
-logger = logging.getLogger(__name__)
-
-logger.warning(
- 'The oauth2client.contrib.multistore_file module has been deprecated and '
- 'will be removed in the next release of oauth2client. Please migrate to '
- 'multiprocess_file_storage.')
-
-# A dict from 'filename'->_MultiStore instances
-_multistores = {}
-_multistores_lock = threading.Lock()
-
-
-class Error(Exception):
- """Base error for this module."""
-
-
-class NewerCredentialStoreError(Error):
- """The credential store is a newer version than supported."""
-
-
-def _dict_to_tuple_key(dictionary):
- """Converts a dictionary to a tuple that can be used as an immutable key.
-
- The resulting key is always sorted so that logically equivalent
- dictionaries always produce an identical tuple for a key.
-
- Args:
- dictionary: the dictionary to use as the key.
-
- Returns:
- A tuple representing the dictionary in it's naturally sorted ordering.
- """
- return tuple(sorted(dictionary.items()))
-
-
-@util.positional(4)
-def get_credential_storage(filename, client_id, user_agent, scope,
- warn_on_readonly=True):
- """Get a Storage instance for a credential.
-
- Args:
- filename: The JSON file storing a set of credentials
- client_id: The client_id for the credential
- user_agent: The user agent for the credential
- scope: string or iterable of strings, Scope(s) being requested
- warn_on_readonly: if True, log a warning if the store is readonly
-
- Returns:
- An object derived from client.Storage for getting/setting the
- credential.
- """
- # Recreate the legacy key with these specific parameters
- key = {'clientId': client_id, 'userAgent': user_agent,
- 'scope': util.scopes_to_string(scope)}
- return get_credential_storage_custom_key(
- filename, key, warn_on_readonly=warn_on_readonly)
-
-
-@util.positional(2)
-def get_credential_storage_custom_string_key(filename, key_string,
- warn_on_readonly=True):
- """Get a Storage instance for a credential using a single string as a key.
-
- Allows you to provide a string as a custom key that will be used for
- credential storage and retrieval.
-
- Args:
- filename: The JSON file storing a set of credentials
- key_string: A string to use as the key for storing this credential.
- warn_on_readonly: if True, log a warning if the store is readonly
-
- Returns:
- An object derived from client.Storage for getting/setting the
- credential.
- """
- # Create a key dictionary that can be used
- key_dict = {'key': key_string}
- return get_credential_storage_custom_key(
- filename, key_dict, warn_on_readonly=warn_on_readonly)
-
-
-@util.positional(2)
-def get_credential_storage_custom_key(filename, key_dict,
- warn_on_readonly=True):
- """Get a Storage instance for a credential using a dictionary as a key.
-
- Allows you to provide a dictionary as a custom key that will be used for
- credential storage and retrieval.
-
- Args:
- filename: The JSON file storing a set of credentials
- key_dict: A dictionary to use as the key for storing this credential.
- There is no ordering of the keys in the dictionary. Logically
- equivalent dictionaries will produce equivalent storage keys.
- warn_on_readonly: if True, log a warning if the store is readonly
-
- Returns:
- An object derived from client.Storage for getting/setting the
- credential.
- """
- multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
- key = _dict_to_tuple_key(key_dict)
- return multistore._get_storage(key)
-
-
-@util.positional(1)
-def get_all_credential_keys(filename, warn_on_readonly=True):
- """Gets all the registered credential keys in the given Multistore.
-
- Args:
- filename: The JSON file storing a set of credentials
- warn_on_readonly: if True, log a warning if the store is readonly
-
- Returns:
- A list of the credential keys present in the file. They are returned
- as dictionaries that can be passed into
- get_credential_storage_custom_key to get the actual credentials.
- """
- multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
- multistore._lock()
- try:
- return multistore._get_all_credential_keys()
- finally:
- multistore._unlock()
-
-
-@util.positional(1)
-def _get_multistore(filename, warn_on_readonly=True):
- """A helper method to initialize the multistore with proper locking.
-
- Args:
- filename: The JSON file storing a set of credentials
- warn_on_readonly: if True, log a warning if the store is readonly
-
- Returns:
- A multistore object
- """
- filename = os.path.expanduser(filename)
- _multistores_lock.acquire()
- try:
- multistore = _multistores.setdefault(
- filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
- finally:
- _multistores_lock.release()
- return multistore
-
-
-class _MultiStore(object):
- """A file backed store for multiple credentials."""
-
- @util.positional(2)
- def __init__(self, filename, warn_on_readonly=True):
- """Initialize the class.
-
- This will create the file if necessary.
- """
- self._file = locked_file.LockedFile(filename, 'r+', 'r')
- self._thread_lock = threading.Lock()
- self._read_only = False
- self._warn_on_readonly = warn_on_readonly
-
- self._create_file_if_needed()
-
- # Cache of deserialized store. This is only valid after the
- # _MultiStore is locked or _refresh_data_cache is called. This is
- # of the form of:
- #
- # ((key, value), (key, value)...) -> OAuth2Credential
- #
- # If this is None, then the store hasn't been read yet.
- self._data = None
-
- class _Storage(client.Storage):
- """A Storage object that can read/write a single credential."""
-
- def __init__(self, multistore, key):
- self._multistore = multistore
- self._key = key
-
- def acquire_lock(self):
- """Acquires any lock necessary to access this Storage.
-
- This lock is not reentrant.
- """
- self._multistore._lock()
-
- def release_lock(self):
- """Release the Storage lock.
-
- Trying to release a lock that isn't held will result in a
- RuntimeError.
- """
- self._multistore._unlock()
-
- def locked_get(self):
- """Retrieve credential.
-
- The Storage lock must be held when this is called.
-
- Returns:
- oauth2client.client.Credentials
- """
- credential = self._multistore._get_credential(self._key)
- if credential:
- credential.set_store(self)
- return credential
-
- def locked_put(self, credentials):
- """Write a credential.
-
- The Storage lock must be held when this is called.
-
- Args:
- credentials: Credentials, the credentials to store.
- """
- self._multistore._update_credential(self._key, credentials)
-
- def locked_delete(self):
- """Delete a credential.
-
- The Storage lock must be held when this is called.
-
- Args:
- credentials: Credentials, the credentials to store.
- """
- self._multistore._delete_credential(self._key)
-
- def _create_file_if_needed(self):
- """Create an empty file if necessary.
-
- This method will not initialize the file. Instead it implements a
- simple version of "touch" to ensure the file has been created.
- """
- if not os.path.exists(self._file.filename()):
- old_umask = os.umask(0o177)
- try:
- open(self._file.filename(), 'a+b').close()
- finally:
- os.umask(old_umask)
-
- def _lock(self):
- """Lock the entire multistore."""
- self._thread_lock.acquire()
- try:
- self._file.open_and_lock()
- except (IOError, OSError) as e:
- if e.errno == errno.ENOSYS:
- logger.warn('File system does not support locking the '
- 'credentials file.')
- elif e.errno == errno.ENOLCK:
- logger.warn('File system is out of resources for writing the '
- 'credentials file (is your disk full?).')
- elif e.errno == errno.EDEADLK:
- logger.warn('Lock contention on multistore file, opening '
- 'in read-only mode.')
- elif e.errno == errno.EACCES:
- logger.warn('Cannot access credentials file.')
- else:
- raise
- if not self._file.is_locked():
- self._read_only = True
- if self._warn_on_readonly:
- logger.warn('The credentials file (%s) is not writable. '
- 'Opening in read-only mode. Any refreshed '
- 'credentials will only be '
- 'valid for this run.', self._file.filename())
-
- if os.path.getsize(self._file.filename()) == 0:
- logger.debug('Initializing empty multistore file')
- # The multistore is empty so write out an empty file.
- self._data = {}
- self._write()
- elif not self._read_only or self._data is None:
- # Only refresh the data if we are read/write or we haven't
- # cached the data yet. If we are readonly, we assume is isn't
- # changing out from under us and that we only have to read it
- # once. This prevents us from whacking any new access keys that
- # we have cached in memory but were unable to write out.
- self._refresh_data_cache()
-
- def _unlock(self):
- """Release the lock on the multistore."""
- self._file.unlock_and_close()
- self._thread_lock.release()
-
- def _locked_json_read(self):
- """Get the raw content of the multistore file.
-
- The multistore must be locked when this is called.
-
- Returns:
- The contents of the multistore decoded as JSON.
- """
- assert self._thread_lock.locked()
- self._file.file_handle().seek(0)
- return json.load(self._file.file_handle())
-
- def _locked_json_write(self, data):
- """Write a JSON serializable data structure to the multistore.
-
- The multistore must be locked when this is called.
-
- Args:
- data: The data to be serialized and written.
- """
- assert self._thread_lock.locked()
- if self._read_only:
- return
- self._file.file_handle().seek(0)
- json.dump(data, self._file.file_handle(),
- sort_keys=True, indent=2, separators=(',', ': '))
- self._file.file_handle().truncate()
-
- def _refresh_data_cache(self):
- """Refresh the contents of the multistore.
-
- The multistore must be locked when this is called.
-
- Raises:
- NewerCredentialStoreError: Raised when a newer client has written
- the store.
- """
- self._data = {}
- try:
- raw_data = self._locked_json_read()
- except Exception:
- logger.warn('Credential data store could not be loaded. '
- 'Will ignore and overwrite.')
- return
-
- version = 0
- try:
- version = raw_data['file_version']
- except Exception:
- logger.warn('Missing version for credential data store. It may be '
- 'corrupt or an old version. Overwriting.')
- if version > 1:
- raise NewerCredentialStoreError(
- 'Credential file has file_version of {0}. '
- 'Only file_version of 1 is supported.'.format(version))
-
- credentials = []
- try:
- credentials = raw_data['data']
- except (TypeError, KeyError):
- pass
-
- for cred_entry in credentials:
- try:
- key, credential = self._decode_credential_from_json(cred_entry)
- self._data[key] = credential
- except:
- # If something goes wrong loading a credential, just ignore it
- logger.info('Error decoding credential, skipping',
- exc_info=True)
-
- def _decode_credential_from_json(self, cred_entry):
- """Load a credential from our JSON serialization.
-
- Args:
- cred_entry: A dict entry from the data member of our format
-
- Returns:
- (key, cred) where the key is the key tuple and the cred is the
- OAuth2Credential object.
- """
- raw_key = cred_entry['key']
- key = _dict_to_tuple_key(raw_key)
- credential = None
- credential = client.Credentials.new_from_json(
- json.dumps(cred_entry['credential']))
- return (key, credential)
-
- def _write(self):
- """Write the cached data back out.
-
- The multistore must be locked.
- """
- raw_data = {'file_version': 1}
- raw_creds = []
- raw_data['data'] = raw_creds
- for (cred_key, cred) in self._data.items():
- raw_key = dict(cred_key)
- raw_cred = json.loads(cred.to_json())
- raw_creds.append({'key': raw_key, 'credential': raw_cred})
- self._locked_json_write(raw_data)
-
- def _get_all_credential_keys(self):
- """Gets all the registered credential keys in the multistore.
-
- Returns:
- A list of dictionaries corresponding to all the keys currently
- registered
- """
- return [dict(key) for key in self._data.keys()]
-
- def _get_credential(self, key):
- """Get a credential from the multistore.
-
- The multistore must be locked.
-
- Args:
- key: The key used to retrieve the credential
-
- Returns:
- The credential specified or None if not present
- """
- return self._data.get(key, None)
-
- def _update_credential(self, key, cred):
- """Update a credential and write the multistore.
-
- This must be called when the multistore is locked.
-
- Args:
- key: The key used to retrieve the credential
- cred: The OAuth2Credential to update/set
- """
- self._data[key] = cred
- self._write()
-
- def _delete_credential(self, key):
- """Delete a credential and write the multistore.
-
- This must be called when the multistore is locked.
-
- Args:
- key: The key used to retrieve the credential
- """
- try:
- del self._data[key]
- except KeyError:
- pass
- self._write()
-
- def _get_storage(self, key):
- """Get a Storage object to get/set a credential.
-
- This Storage is a 'view' into the multistore.
-
- Args:
- key: The key used to retrieve the credential
-
- Returns:
- A Storage object that can be used to get/set this cred
- """
- return self._Storage(self, key)
diff --git a/oauth2client/contrib/xsrfutil.py b/oauth2client/contrib/xsrfutil.py
index c03e679..7c3ec03 100644
--- a/oauth2client/contrib/xsrfutil.py
+++ b/oauth2client/contrib/xsrfutil.py
@@ -20,12 +20,7 @@ import hmac
import time
from oauth2client import _helpers
-from oauth2client import util
-__authors__ = [
- '"Doug Coker" <dcoker@google.com>',
- '"Joe Gregorio" <jcgregorio@google.com>',
-]
# Delimiter character
DELIMITER = b':'
@@ -34,7 +29,7 @@ DELIMITER = b':'
DEFAULT_TIMEOUT_SECS = 60 * 60
-@util.positional(2)
+@_helpers.positional(2)
def generate_token(key, user_id, action_id='', when=None):
"""Generates a URL-safe token for the given user, action, time tuple.
@@ -62,7 +57,7 @@ def generate_token(key, user_id, action_id='', when=None):
return token
-@util.positional(3)
+@_helpers.positional(3)
def validate_token(key, token, user_id, action_id="", current_time=None):
"""Validates that the given token authorizes the user for the action.
diff --git a/oauth2client/file.py b/oauth2client/file.py
index feede11..3551c80 100644
--- a/oauth2client/file.py
+++ b/oauth2client/file.py
@@ -21,16 +21,10 @@ credentials.
import os
import threading
+from oauth2client import _helpers
from oauth2client import client
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
-
-class CredentialsFileSymbolicLinkError(Exception):
- """Credentials files must not be symbolic links."""
-
-
class Storage(client.Storage):
"""Store and retrieve a single credential to and from a file."""
@@ -38,11 +32,6 @@ class Storage(client.Storage):
super(Storage, self).__init__(lock=threading.Lock())
self._filename = filename
- def _validate_file(self):
- if os.path.islink(self._filename):
- raise CredentialsFileSymbolicLinkError(
- 'File: {0} is a symbolic link.'.format(self._filename))
-
def locked_get(self):
"""Retrieve Credential from file.
@@ -50,10 +39,10 @@ class Storage(client.Storage):
oauth2client.client.Credentials
Raises:
- CredentialsFileSymbolicLinkError if the file is a symbolic link.
+ IOError if the file is a symbolic link.
"""
credentials = None
- self._validate_file()
+ _helpers.validate_file(self._filename)
try:
f = open(self._filename, 'rb')
content = f.read()
@@ -89,10 +78,10 @@ class Storage(client.Storage):
credentials: Credentials, the credentials to store.
Raises:
- CredentialsFileSymbolicLinkError if the file is a symbolic link.
+ IOError if the file is a symbolic link.
"""
self._create_file_if_needed()
- self._validate_file()
+ _helpers.validate_file(self._filename)
f = open(self._filename, 'w')
f.write(credentials.to_json())
f.close()
diff --git a/oauth2client/service_account.py b/oauth2client/service_account.py
index bdcfd69..540bfaa 100644
--- a/oauth2client/service_account.py
+++ b/oauth2client/service_account.py
@@ -25,7 +25,6 @@ from oauth2client import _helpers
from oauth2client import client
from oauth2client import crypt
from oauth2client import transport
-from oauth2client import util
_PASSWORD_DEFAULT = 'notasecret'
@@ -110,7 +109,7 @@ class ServiceAccountCredentials(client.AssertionCredentials):
self._service_account_email = service_account_email
self._signer = signer
- self._scopes = util.scopes_to_string(scopes)
+ self._scopes = _helpers.scopes_to_string(scopes)
self._private_key_id = private_key_id
self.client_id = client_id
self._user_agent = user_agent
@@ -650,9 +649,22 @@ class _JWTAccessCredentials(ServiceAccountCredentials):
return result
def refresh(self, http):
+ """Refreshes the access_token.
+
+ The HTTP object is unused since no request needs to be made to
+ get a new token, it can just be generated locally.
+
+ Args:
+ http: unused HTTP object
+ """
self._refresh(None)
- def _refresh(self, http_request):
+ def _refresh(self, http):
+ """Refreshes the access_token.
+
+ Args:
+ http: unused HTTP object
+ """
self.access_token, self.token_expiry = self._create_token()
def _create_token(self, additional_claims=None):
diff --git a/oauth2client/tools.py b/oauth2client/tools.py
index 8947157..5166993 100644
--- a/oauth2client/tools.py
+++ b/oauth2client/tools.py
@@ -30,11 +30,10 @@ from six.moves import http_client
from six.moves import input
from six.moves import urllib
+from oauth2client import _helpers
from oauth2client import client
-from oauth2client import util
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
__all__ = ['argparser', 'run_flow', 'message_if_missing']
_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
@@ -93,6 +92,7 @@ def _CreateArgumentParser():
help='Set the logging level of detail.')
return parser
+
# argparser is an ArgumentParser that contains command-line options expected
# by tools.run(). Pass it in as part of the 'parents' argument to your own
# ArgumentParser.
@@ -123,22 +123,22 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
if an error occurred.
"""
self.send_response(http_client.OK)
- self.send_header("Content-type", "text/html")
+ self.send_header('Content-type', 'text/html')
self.end_headers()
- query = self.path.split('?', 1)[-1]
- query = dict(urllib.parse.parse_qsl(query))
+ parts = urllib.parse.urlparse(self.path)
+ query = _helpers.parse_unique_urlencoded(parts.query)
self.server.query_params = query
self.wfile.write(
- b"<html><head><title>Authentication Status</title></head>")
+ b'<html><head><title>Authentication Status</title></head>')
self.wfile.write(
- b"<body><p>The authentication flow has completed.</p>")
- self.wfile.write(b"</body></html>")
+ b'<body><p>The authentication flow has completed.</p>')
+ self.wfile.write(b'</body></html>')
def log_message(self, format, *args):
"""Do not log messages to stdout while running as cmd. line program."""
-@util.positional(3)
+@_helpers.positional(3)
def run_flow(flow, storage, flags=None, http=None):
"""Core code for a command-line application.
diff --git a/oauth2client/transport.py b/oauth2client/transport.py
index 8dbc60d..79a61f1 100644
--- a/oauth2client/transport.py
+++ b/oauth2client/transport.py
@@ -18,7 +18,7 @@ import httplib2
import six
from six.moves import http_client
-from oauth2client._helpers import _to_bytes
+from oauth2client import _helpers
_LOGGER = logging.getLogger(__name__)
@@ -58,13 +58,19 @@ def get_cached_http():
return _CACHED_HTTP
-def get_http_object():
+def get_http_object(*args, **kwargs):
"""Return a new HTTP object.
+ Args:
+ *args: tuple, The positional arguments to be passed when
+ contructing a new HTTP object.
+ **kwargs: dict, The keyword arguments to be passed when
+ contructing a new HTTP object.
+
Returns:
httplib2.Http, an HTTP object.
"""
- return httplib2.Http()
+ return httplib2.Http(*args, **kwargs)
def _initialize_headers(headers):
@@ -121,7 +127,7 @@ def clean_headers(headers):
k = str(k)
if not isinstance(v, six.binary_type):
v = str(v)
- clean[_to_bytes(k)] = _to_bytes(v)
+ clean[_helpers._to_bytes(k)] = _helpers._to_bytes(v)
except UnicodeEncodeError:
from oauth2client.client import NonAsciiHeaderError
raise NonAsciiHeaderError(k, ': ', v)
@@ -164,9 +170,9 @@ def wrap_http_for_auth(credentials, http):
_STREAM_PROPERTIES):
body_stream_position = body.tell()
- resp, content = orig_request_method(uri, method, body,
- clean_headers(headers),
- redirections, connection_type)
+ resp, content = request(orig_request_method, uri, method, body,
+ clean_headers(headers),
+ redirections, connection_type)
# A stored token may expire between the time it is retrieved and
# the time the request is made, so we may need to try twice.
@@ -182,9 +188,9 @@ def wrap_http_for_auth(credentials, http):
if body_stream_position is not None:
body.seek(body_stream_position)
- resp, content = orig_request_method(uri, method, body,
- clean_headers(headers),
- redirections, connection_type)
+ resp, content = request(orig_request_method, uri, method, body,
+ clean_headers(headers),
+ redirections, connection_type)
return resp, content
@@ -192,7 +198,7 @@ def wrap_http_for_auth(credentials, http):
http.request = new_request
# Set credentials as a property of the request method.
- setattr(http.request, 'credentials', credentials)
+ http.request.credentials = credentials
def wrap_http_for_jwt_access(credentials, http):
@@ -222,9 +228,9 @@ def wrap_http_for_jwt_access(credentials, http):
if (credentials.access_token is None or
credentials.access_token_expired):
credentials.refresh(None)
- return authenticated_request_method(uri, method, body,
- headers, redirections,
- connection_type)
+ return request(authenticated_request_method, uri,
+ method, body, headers, redirections,
+ connection_type)
else:
# If we don't have an 'aud' (audience) claim,
# create a 1-time token with the uri root as the audience
@@ -234,12 +240,46 @@ def wrap_http_for_jwt_access(credentials, http):
token, unused_expiry = credentials._create_token({'aud': uri_root})
headers['Authorization'] = 'Bearer ' + token
- return orig_request_method(uri, method, body,
- clean_headers(headers),
- redirections, connection_type)
+ return request(orig_request_method, uri, method, body,
+ clean_headers(headers),
+ redirections, connection_type)
# Replace the request method with our own closure.
http.request = new_request
+ # Set credentials as a property of the request method.
+ http.request.credentials = credentials
+
+
+def request(http, uri, method='GET', body=None, headers=None,
+ redirections=httplib2.DEFAULT_MAX_REDIRECTS,
+ connection_type=None):
+ """Make an HTTP request with an HTTP object and arguments.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make requests.
+ uri: string, The URI to be requested.
+ method: string, The HTTP method to use for the request. Defaults
+ to 'GET'.
+ body: string, The payload / body in HTTP request. By default
+ there is no payload.
+ headers: dict, Key-value pairs of request headers. By default
+ there are no headers.
+ redirections: int, The number of allowed 203 redirects for
+ the request. Defaults to 5.
+ connection_type: httplib.HTTPConnection, a subclass to be used for
+ establishing connection. If not set, the type
+ will be determined from the ``uri``.
+
+ Returns:
+ tuple, a pair of a httplib2.Response with the status code and other
+ headers and the bytes of the content returned.
+ """
+ # NOTE: Allowing http or http.request is temporary (See Issue 601).
+ http_callable = getattr(http, 'request', http)
+ return http_callable(uri, method=method, body=body, headers=headers,
+ redirections=redirections,
+ connection_type=connection_type)
+
_CACHED_HTTP = httplib2.Http(MemoryCache())
diff --git a/oauth2client/util.py b/oauth2client/util.py
deleted file mode 100644
index e3ba62b..0000000
--- a/oauth2client/util.py
+++ /dev/null
@@ -1,206 +0,0 @@
-# Copyright 2014 Google Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Common utility library."""
-
-import functools
-import inspect
-import logging
-
-import six
-from six.moves import urllib
-
-
-__author__ = [
- 'rafek@google.com (Rafe Kaplan)',
- 'guido@google.com (Guido van Rossum)',
-]
-
-__all__ = [
- 'positional',
- 'POSITIONAL_WARNING',
- 'POSITIONAL_EXCEPTION',
- 'POSITIONAL_IGNORE',
-]
-
-logger = logging.getLogger(__name__)
-
-POSITIONAL_WARNING = 'WARNING'
-POSITIONAL_EXCEPTION = 'EXCEPTION'
-POSITIONAL_IGNORE = 'IGNORE'
-POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
- POSITIONAL_IGNORE])
-
-positional_parameters_enforcement = POSITIONAL_WARNING
-
-
-def positional(max_positional_args):
- """A decorator to declare that only the first N arguments my be positional.
-
- This decorator makes it easy to support Python 3 style keyword-only
- parameters. For example, in Python 3 it is possible to write::
-
- def fn(pos1, *, kwonly1=None, kwonly1=None):
- ...
-
- All named parameters after ``*`` must be a keyword::
-
- fn(10, 'kw1', 'kw2') # Raises exception.
- fn(10, kwonly1='kw1') # Ok.
-
- Example
- ^^^^^^^
-
- To define a function like above, do::
-
- @positional(1)
- def fn(pos1, kwonly1=None, kwonly2=None):
- ...
-
- If no default value is provided to a keyword argument, it becomes a
- required keyword argument::
-
- @positional(0)
- def fn(required_kw):
- ...
-
- This must be called with the keyword parameter::
-
- fn() # Raises exception.
- fn(10) # Raises exception.
- fn(required_kw=10) # Ok.
-
- When defining instance or class methods always remember to account for
- ``self`` and ``cls``::
-
- class MyClass(object):
-
- @positional(2)
- def my_method(self, pos1, kwonly1=None):
- ...
-
- @classmethod
- @positional(2)
- def my_method(cls, pos1, kwonly1=None):
- ...
-
- The positional decorator behavior is controlled by
- ``util.positional_parameters_enforcement``, which may be set to
- ``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
- ``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
- nothing, respectively, if a declaration is violated.
-
- Args:
- max_positional_arguments: Maximum number of positional arguments. All
- parameters after the this index must be
- keyword only.
-
- Returns:
- A decorator that prevents using arguments after max_positional_args
- from being used as positional parameters.
-
- Raises:
- TypeError: if a key-word only argument is provided as a positional
- parameter, but only if
- util.positional_parameters_enforcement is set to
- POSITIONAL_EXCEPTION.
- """
-
- def positional_decorator(wrapped):
- @functools.wraps(wrapped)
- def positional_wrapper(*args, **kwargs):
- if len(args) > max_positional_args:
- plural_s = ''
- if max_positional_args != 1:
- plural_s = 's'
- message = ('{function}() takes at most {args_max} positional '
- 'argument{plural} ({args_given} given)'.format(
- function=wrapped.__name__,
- args_max=max_positional_args,
- args_given=len(args),
- plural=plural_s))
- if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
- raise TypeError(message)
- elif positional_parameters_enforcement == POSITIONAL_WARNING:
- logger.warning(message)
- return wrapped(*args, **kwargs)
- return positional_wrapper
-
- if isinstance(max_positional_args, six.integer_types):
- return positional_decorator
- else:
- args, _, _, defaults = inspect.getargspec(max_positional_args)
- return positional(len(args) - len(defaults))(max_positional_args)
-
-
-def scopes_to_string(scopes):
- """Converts scope value to a string.
-
- If scopes is a string then it is simply passed through. If scopes is an
- iterable then a string is returned that is all the individual scopes
- concatenated with spaces.
-
- Args:
- scopes: string or iterable of strings, the scopes.
-
- Returns:
- The scopes formatted as a single string.
- """
- if isinstance(scopes, six.string_types):
- return scopes
- else:
- return ' '.join(scopes)
-
-
-def string_to_scopes(scopes):
- """Converts stringifed scope value to a list.
-
- If scopes is a list then it is simply passed through. If scopes is an
- string then a list of each individual scope is returned.
-
- Args:
- scopes: a string or iterable of strings, the scopes.
-
- Returns:
- The scopes in a list.
- """
- if not scopes:
- return []
- if isinstance(scopes, six.string_types):
- return scopes.split(' ')
- else:
- return scopes
-
-
-def _add_query_parameter(url, name, value):
- """Adds a query parameter to a url.
-
- Replaces the current value if it already exists in the URL.
-
- Args:
- url: string, url to add the query parameter to.
- name: string, query parameter name.
- value: string, query parameter value.
-
- Returns:
- Updated query parameter. Does not update the url if value is None.
- """
- if value is None:
- return url
- else:
- parsed = list(urllib.parse.urlparse(url))
- q = dict(urllib.parse.parse_qsl(parsed[4]))
- q[name] = value
- parsed[4] = urllib.parse.urlencode(q)
- return urllib.parse.urlunparse(parsed)
diff --git a/samples/django/README.md b/samples/django/README.md
new file mode 100644
index 0000000..27c0fda
--- /dev/null
+++ b/samples/django/README.md
@@ -0,0 +1,21 @@
+# Django Samples
+
+These two sample Django apps provide a skeleton for the two main use cases of the
+`oauth2client.contrib.django_util` helpers.
+
+Please see the
+[core docs](https://oauth2client.readthedocs.io/en/latest/) for more information and usage examples.
+
+## google_user
+
+This is the simpler use case of the library. It assumes you are using Google OAuth as your primary
+authorization and authentication mechanism for your application. Users log in with their Google ID
+and their OAuth2 credentials are stored inside the session.
+
+## django_user
+
+This is the use case where the application is already using the Django authorization system and
+has a Django model with a `django.contrib.auth.models.User` field, and would like to attach
+a Google OAuth2 credentials object to that model. Users have to login, and then can login with
+their Google account to associate the Google account with the user in the Django system.
+Credentials will be stored in the Django ORM backend.
diff --git a/samples/django/django_user/manage.py b/samples/django/django_user/manage.py
new file mode 100755
index 0000000..1a30708
--- /dev/null
+++ b/samples/django/django_user/manage.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myoauth.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/samples/django/django_user/myoauth/__init__.py b/samples/django/django_user/myoauth/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samples/django/django_user/myoauth/__init__.py
diff --git a/samples/django/django_user/myoauth/settings.py b/samples/django/django_user/myoauth/settings.py
new file mode 100644
index 0000000..5ef2f99
--- /dev/null
+++ b/samples/django/django_user/myoauth/settings.py
@@ -0,0 +1,115 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'eiw+mvmua#98n@p2xq+c#liz@r2&#-s07nkgz)+$zcl^o4$-$o'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+# Application definition
+
+INSTALLED_APPS = (
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'polls',
+ 'oauth2client.contrib.django_util',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'django.middleware.security.SecurityMiddleware',
+)
+
+ROOT_URLCONF = 'myoauth.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'myoauth.wsgi.application'
+
+# Database
+# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ }
+}
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.8/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.8/howto/static-files/
+
+STATIC_URL = '/static/'
+
+GOOGLE_OAUTH2_CLIENT_ID = 'YOUR_CLIENT_ID'
+
+GOOGLE_OAUTH2_CLIENT_SECRET = 'YOUR_CLIENT_SECRET'
+
+GOOGLE_OAUTH2_SCOPES = (
+ 'email', 'profile')
+
+GOOGLE_OAUTH2_STORAGE_MODEL = {
+ 'model': 'polls.models.CredentialsModel',
+ 'user_property': 'user_id',
+ 'credentials_property': 'credential',
+}
+
+LOGIN_URL = '/login'
diff --git a/samples/django/django_user/myoauth/urls.py b/samples/django/django_user/myoauth/urls.py
new file mode 100644
index 0000000..6636b4e
--- /dev/null
+++ b/samples/django/django_user/myoauth/urls.py
@@ -0,0 +1,30 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.conf import urls
+from django.contrib import admin
+import django.contrib.auth.views
+from polls import views
+
+import oauth2client.contrib.django_util.site as django_util_site
+
+
+urlpatterns = [
+ urls.url(r'^$', views.index),
+ urls.url(r'^profile_required$', views.get_profile_required),
+ urls.url(r'^profile_enabled$', views.get_profile_optional),
+ urls.url(r'^admin/', urls.include(admin.site.urls)),
+ urls.url(r'^login', django.contrib.auth.views.login, name="login"),
+ urls.url(r'^oauth2/', urls.include(django_util_site.urls)),
+]
diff --git a/samples/django/django_user/myoauth/wsgi.py b/samples/django/django_user/myoauth/wsgi.py
new file mode 100644
index 0000000..39ee648
--- /dev/null
+++ b/samples/django/django_user/myoauth/wsgi.py
@@ -0,0 +1,21 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myoauth.settings")
+
+application = get_wsgi_application()
diff --git a/samples/django/django_user/polls/__init__.py b/samples/django/django_user/polls/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samples/django/django_user/polls/__init__.py
diff --git a/samples/django/django_user/polls/models.py b/samples/django/django_user/polls/models.py
new file mode 100644
index 0000000..563f66e
--- /dev/null
+++ b/samples/django/django_user/polls/models.py
@@ -0,0 +1,23 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.contrib.auth.models import User
+from django.db import models
+
+from oauth2client.contrib.django_util.models import CredentialsField
+
+
+class CredentialsModel(models.Model):
+ user_id = models.OneToOneField(User)
+ credential = CredentialsField()
diff --git a/samples/django/django_user/polls/templates/registration/login.html b/samples/django/django_user/polls/templates/registration/login.html
new file mode 100644
index 0000000..c43450e
--- /dev/null
+++ b/samples/django/django_user/polls/templates/registration/login.html
@@ -0,0 +1,45 @@
+<!--
+# Copyright 2016 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+-->
+
+{% if form.errors %}
+<p>Your username and password didn't match. Please try again.</p>
+{% endif %}
+
+{% if next %}
+{% if user.is_authenticated %}
+<p>Your account doesn't have access to this page. To proceed,
+ please login with an account that has access.</p>
+{% else %}
+<p>Please login to see this page.</p>
+{% endif %}
+{% endif %}
+
+<form method="post" action="{% url 'login' %}">
+ {% csrf_token %}
+ <table>
+ <tr>
+ <td>{{ form.username.label_tag }}</td>
+ <td>{{ form.username }}</td>
+ </tr>
+ <tr>
+ <td>{{ form.password.label_tag }}</td>
+ <td>{{ form.password }}</td>
+ </tr>
+ </table>
+
+ <input type="submit" value="login" />
+ <input type="hidden" name="next" value="{{ next }}" />
+</form>
diff --git a/samples/django/django_user/polls/views.py b/samples/django/django_user/polls/views.py
new file mode 100644
index 0000000..5888330
--- /dev/null
+++ b/samples/django/django_user/polls/views.py
@@ -0,0 +1,41 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.http import HttpResponse
+
+from oauth2client.contrib.django_util import decorators
+
+
+def index(request):
+ return HttpResponse("Hello world!")
+
+
+@decorators.oauth_required
+def get_profile_required(request):
+ resp, content = request.oauth.http.request(
+ 'https://www.googleapis.com/plus/v1/people/me')
+ return HttpResponse(content)
+
+
+@decorators.oauth_enabled
+def get_profile_optional(request):
+ if request.oauth.has_credentials():
+ # this could be passed into a view
+ # request.oauth.http is also initialized
+ return HttpResponse('User email: {}'.format(
+ request.oauth.credentials.id_token['email']))
+ else:
+ return HttpResponse(
+ 'Here is an OAuth Authorize link:<a href="{}">Authorize</a>'
+ .format(request.oauth.get_authorize_redirect()))
diff --git a/samples/django/django_user/requirements.txt b/samples/django/django_user/requirements.txt
new file mode 100644
index 0000000..b42af1f
--- /dev/null
+++ b/samples/django/django_user/requirements.txt
@@ -0,0 +1,3 @@
+Django==1.10.0
+oauth2client==3.0.0
+jsonpickle==0.9.3
diff --git a/samples/django/google_user/manage.py b/samples/django/google_user/manage.py
new file mode 100755
index 0000000..1a30708
--- /dev/null
+++ b/samples/django/google_user/manage.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myoauth.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/samples/django/google_user/myoauth/__init__.py b/samples/django/google_user/myoauth/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samples/django/google_user/myoauth/__init__.py
diff --git a/samples/django/google_user/myoauth/settings.py b/samples/django/google_user/myoauth/settings.py
new file mode 100644
index 0000000..e08661d
--- /dev/null
+++ b/samples/django/google_user/myoauth/settings.py
@@ -0,0 +1,107 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'eiw+mvmua#98n@p2xq+c#liz@r2&#-s07nkgz)+$zcl^o4$-$o'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+# Application definition
+
+INSTALLED_APPS = (
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'polls',
+ 'oauth2client.contrib.django_util',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'django.middleware.security.SecurityMiddleware',
+)
+
+ROOT_URLCONF = 'myoauth.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'myoauth.wsgi.application'
+
+# Database
+# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ }
+}
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.8/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.8/howto/static-files/
+
+STATIC_URL = '/static/'
+
+GOOGLE_OAUTH2_CLIENT_ID = 'YOUR_CLIENT_ID'
+
+GOOGLE_OAUTH2_CLIENT_SECRET = 'YOUR_CLIENT_SECRET'
+
+GOOGLE_OAUTH2_SCOPES = (
+ 'email', 'profile')
diff --git a/samples/django/google_user/myoauth/urls.py b/samples/django/google_user/myoauth/urls.py
new file mode 100644
index 0000000..4d3d0a1
--- /dev/null
+++ b/samples/django/google_user/myoauth/urls.py
@@ -0,0 +1,26 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django.conf import urls
+from polls import views
+
+import oauth2client.contrib.django_util.site as django_util_site
+
+
+urlpatterns = [
+ urls.url(r'^$', views.index),
+ urls.url(r'^profile_required$', views.get_profile_required),
+ urls.url(r'^profile_enabled$', views.get_profile_optional),
+ urls.url(r'^oauth2/', urls.include(django_util_site.urls))
+]
diff --git a/samples/django/google_user/myoauth/wsgi.py b/samples/django/google_user/myoauth/wsgi.py
new file mode 100644
index 0000000..39ee648
--- /dev/null
+++ b/samples/django/google_user/myoauth/wsgi.py
@@ -0,0 +1,21 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myoauth.settings")
+
+application = get_wsgi_application()
diff --git a/samples/django/google_user/polls/__init__.py b/samples/django/google_user/polls/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samples/django/google_user/polls/__init__.py
diff --git a/samples/django/google_user/polls/views.py b/samples/django/google_user/polls/views.py
new file mode 100644
index 0000000..e4b9119
--- /dev/null
+++ b/samples/django/google_user/polls/views.py
@@ -0,0 +1,41 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from django import http
+
+from oauth2client.contrib.django_util import decorators
+
+
+def index(request):
+ return http.HttpResponse("Hello world!")
+
+
+@decorators.oauth_required
+def get_profile_required(request):
+ resp, content = request.oauth.http.request(
+ 'https://www.googleapis.com/plus/v1/people/me')
+ return http.HttpResponse(content)
+
+
+@decorators.oauth_enabled
+def get_profile_optional(request):
+ if request.oauth.has_credentials():
+ # this could be passed into a view
+ # request.oauth.http is also initialized
+ return http.HttpResponse('User email: {}'.format(
+ request.oauth.credentials.id_token['email']))
+ else:
+ return http.HttpResponse(
+ 'Here is an OAuth Authorize link:<a href="{}">Authorize</a>'
+ .format(request.oauth.get_authorize_redirect()))
diff --git a/samples/django/google_user/requirements.txt b/samples/django/google_user/requirements.txt
new file mode 100644
index 0000000..b42af1f
--- /dev/null
+++ b/samples/django/google_user/requirements.txt
@@ -0,0 +1,3 @@
+Django==1.10.0
+oauth2client==3.0.0
+jsonpickle==0.9.3
diff --git a/scripts/fetch_gae_sdk.py b/scripts/fetch_gae_sdk.py
deleted file mode 100755
index 24a6db5..0000000
--- a/scripts/fetch_gae_sdk.py
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/env python
-"""Fetch the most recent GAE SDK and decompress it in the current directory.
-
-Usage:
- fetch_gae_sdk.py [<dest_dir>]
-
-Current releases are listed here:
- https://www.googleapis.com/storage/v1/b/appengine-sdks/o?prefix=featured
-"""
-from __future__ import print_function
-
-import json
-import os
-import StringIO
-import sys
-import urllib2
-import zipfile
-
-
-_SDK_URL = (
- 'https://www.googleapis.com/storage/v1/b/appengine-sdks/o?prefix=featured')
-
-
-def get_gae_versions():
- try:
- version_info_json = urllib2.urlopen(_SDK_URL).read()
- except:
- return {}
- try:
- version_info = json.loads(version_info_json)
- except:
- return {}
- return version_info.get('items', {})
-
-
-def _version_tuple(v):
- version_string = os.path.splitext(v['name'])[0].rpartition('_')[2]
- return tuple(int(x) for x in version_string.split('.'))
-
-
-def get_sdk_urls(sdk_versions):
- python_releases = [v for v in sdk_versions
- if v['name'].startswith('featured/google_appengine')]
- current_releases = sorted(python_releases, key=_version_tuple,
- reverse=True)
- return [release['mediaLink'] for release in current_releases]
-
-
-def main(argv):
- if len(argv) > 2:
- print('Usage: {0} [<destination_dir>]'.format(argv[0]))
- return 1
- dest_dir = argv[1] if len(argv) > 1 else '.'
- if not os.path.exists(dest_dir):
- os.makedirs(dest_dir)
-
- if os.path.exists(os.path.join(dest_dir, 'google_appengine')):
- print('GAE SDK already installed at {0}, exiting.'.format(dest_dir))
- return 0
-
- sdk_versions = get_gae_versions()
- if not sdk_versions:
- print('Error fetching GAE SDK version info')
- return 1
- sdk_urls = get_sdk_urls(sdk_versions)
- for sdk_url in sdk_urls:
- try:
- sdk_contents = StringIO.StringIO(urllib2.urlopen(sdk_url).read())
- break
- except:
- pass
- else:
- print('Could not read SDK from any of ', sdk_urls)
- return 1
- sdk_contents.seek(0)
- try:
- zip_contents = zipfile.ZipFile(sdk_contents)
- zip_contents.extractall(dest_dir)
- except:
- print('Error extracting SDK contents')
- return 1
-
-
-if __name__ == '__main__':
- sys.exit(main(sys.argv[:]))
diff --git a/scripts/install.sh b/scripts/install.sh
index 0ef7ad2..e1ed5c5 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -16,16 +16,18 @@
set -ev
-pip install tox
-if [[ "${TOX_ENV}" == "pypy" ]]; then
- git clone https://github.com/yyuu/pyenv.git ${HOME}/.pyenv
- PYENV_ROOT="${HOME}/.pyenv"
- PATH="${PYENV_ROOT}/bin:${PATH}"
- eval "$(pyenv init -)"
- pyenv install pypy-2.6.0
- pyenv global pypy-2.6.0
+pip install --upgrade pip setuptools tox coveralls
+
+# App Engine tests require the App Engine SDK.
+if [[ "${TOX_ENV}" == "gae" || "${TOX_ENV}" == "cover" ]]; then
+ pip install gcp-devrel-py-tools
+ gcp-devrel-py-tools download-appengine-sdk `dirname ${GAE_PYTHONPATH}`
fi
-if [[ "${TOX_ENV}" == "gae" && ! -d ${GAE_PYTHONPATH} ]]; then
- python scripts/fetch_gae_sdk.py `dirname ${GAE_PYTHONPATH}`
+# Travis ships with an old version of PyPy, so install at least version 2.6.
+if [[ "${TOX_ENV}" == "pypy" ]]; then
+ if [ ! -d "${HOME}/.pyenv/bin" ]; then
+ git clone https://github.com/yyuu/pyenv.git ${HOME}/.pyenv
+ fi
+ ${HOME}/.pyenv/bin/pyenv install --skip-existing pypy-2.6.0
fi
diff --git a/scripts/run.sh b/scripts/run.sh
index 0b537e2..c774f24 100755
--- a/scripts/run.sh
+++ b/scripts/run.sh
@@ -16,10 +16,11 @@
set -ev
+# If in the pypy environment, activate the never version of pypy provided by
+# pyenv.
if [[ "${TOX_ENV}" == "pypy" ]]; then
- PYENV_ROOT="${HOME}/.pyenv"
- PATH="${PYENV_ROOT}/bin:${PATH}"
- eval "$(pyenv init -)"
- pyenv global pypy-2.6.0
+ PATH="${HOME}/.pyenv/versions/pypy-2.6.0/bin:${PATH}"
+ export PATH
fi
+
tox -e ${TOX_ENV}
diff --git a/scripts/run_gce_system_tests.py b/scripts/run_gce_system_tests.py
index d446f9c..80794bd 100644
--- a/scripts/run_gce_system_tests.py
+++ b/scripts/run_gce_system_tests.py
@@ -13,26 +13,26 @@
# limitations under the License.
import json
+import unittest
-import httplib2
from six.moves import http_client
from six.moves import urllib
-import unittest2
-from oauth2client import GOOGLE_TOKEN_INFO_URI
-from oauth2client.client import GoogleCredentials
-from oauth2client.contrib.gce import AppAssertionCredentials
+import oauth2client
+from oauth2client import client
+from oauth2client import transport
+from oauth2client.contrib import gce
-class TestComputeEngine(unittest2.TestCase):
+class TestComputeEngine(unittest.TestCase):
def test_application_default(self):
- default_creds = GoogleCredentials.get_application_default()
- self.assertIsInstance(default_creds, AppAssertionCredentials)
+ default_creds = client.GoogleCredentials.get_application_default()
+ self.assertIsInstance(default_creds, gce.AppAssertionCredentials)
def test_token_info(self):
- credentials = AppAssertionCredentials([])
- http = httplib2.Http()
+ credentials = gce.AppAssertionCredentials([])
+ http = transport.get_http_object()
# First refresh to get the access token.
self.assertIsNone(credentials.access_token)
@@ -41,9 +41,9 @@ class TestComputeEngine(unittest2.TestCase):
# Then check the access token against the token info API.
query_params = {'access_token': credentials.access_token}
- token_uri = (GOOGLE_TOKEN_INFO_URI + '?' +
+ token_uri = (oauth2client.GOOGLE_TOKEN_INFO_URI + '?' +
urllib.parse.urlencode(query_params))
- response, content = http.request(token_uri)
+ response, content = transport.request(http, token_uri)
self.assertEqual(response.status, http_client.OK)
content = content.decode('utf-8')
@@ -53,4 +53,4 @@ class TestComputeEngine(unittest2.TestCase):
if __name__ == '__main__':
- unittest2.main()
+ unittest.main()
diff --git a/scripts/run_system_tests.py b/scripts/run_system_tests.py
index ce99e7c..4c9c80c 100644
--- a/scripts/run_system_tests.py
+++ b/scripts/run_system_tests.py
@@ -15,12 +15,12 @@
import json
import os
-import httplib2
from six.moves import http_client
import oauth2client
from oauth2client import client
-from oauth2client.service_account import ServiceAccountCredentials
+from oauth2client import service_account
+from oauth2client import transport
JSON_KEY_PATH = os.getenv('OAUTH2CLIENT_TEST_JSON_KEY_PATH')
@@ -56,8 +56,8 @@ def _require_environ():
def _check_user_info(credentials, expected_email):
- http = credentials.authorize(httplib2.Http())
- response, content = http.request(USER_INFO)
+ http = credentials.authorize(transport.get_http_object())
+ response, content = transport.request(http, USER_INFO)
if response.status != http_client.OK:
raise ValueError('Expected 200 OK response.')
@@ -68,14 +68,14 @@ def _check_user_info(credentials, expected_email):
def run_json():
- credentials = ServiceAccountCredentials.from_json_keyfile_name(
- JSON_KEY_PATH, scopes=SCOPE)
+ factory = service_account.ServiceAccountCredentials.from_json_keyfile_name
+ credentials = factory(JSON_KEY_PATH, scopes=SCOPE)
service_account_email = credentials._service_account_email
_check_user_info(credentials, service_account_email)
def run_p12():
- credentials = ServiceAccountCredentials.from_p12_keyfile(
+ credentials = service_account.ServiceAccountCredentials.from_p12_keyfile(
P12_KEY_EMAIL, P12_KEY_PATH, scopes=SCOPE)
_check_user_info(credentials, P12_KEY_EMAIL)
diff --git a/scripts/run_system_tests.sh b/scripts/run_system_tests.sh
index 7169eb7..2e10e5c 100755
--- a/scripts/run_system_tests.sh
+++ b/scripts/run_system_tests.sh
@@ -19,10 +19,9 @@ set -ev
# If we're on Travis, we need to set up the environment.
if [[ "${TRAVIS}" == "true" ]]; then
- # If merging to master and not a pull request, run system test.
- if [[ "${TRAVIS_BRANCH}" == "master" ]] && \
- [[ "${TRAVIS_PULL_REQUEST}" == "false" ]]; then
- echo "Running in Travis during merge, decrypting stored key file."
+ # If secure variables are available, run system test.
+ if [[ "${TRAVIS_SECURE_ENV_VARS}" ]]; then
+ echo "Running in Travis, decrypting stored key file."
# Convert encrypted JSON key file into decrypted file to be used.
openssl aes-256-cbc -K ${OAUTH2CLIENT_KEY} \
-iv ${OAUTH2CLIENT_IV} \
@@ -34,8 +33,8 @@ if [[ "${TRAVIS}" == "true" ]]; then
-in tests/data/key.p12.enc \
-out ${OAUTH2CLIENT_TEST_P12_KEY_PATH} -d
# Convert encrypted User JSON key file into decrypted file to be used.
- openssl aes-256-cbc -K ${OAUTH2CLIENT_KEY} \
- -iv ${OAUTH2CLIENT_IV} \
+ openssl aes-256-cbc -K ${encrypted_1ee98544e5ca_key} \
+ -iv ${encrypted_1ee98544e5ca_iv} \
-in tests/data/user-key.json.enc \
-out ${OAUTH2CLIENT_TEST_USER_KEY_PATH} -d
else
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2a9acf1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
index 686d1db..47d86b7 100644
--- a/setup.py
+++ b/setup.py
@@ -26,11 +26,11 @@ from setuptools import setup
import oauth2client
-if sys.version_info < (2, 6):
- print('oauth2client requires python2 version >= 2.6.', file=sys.stderr)
+if sys.version_info < (2, 7):
+ print('oauth2client requires python2 version >= 2.7.', file=sys.stderr)
sys.exit(1)
-if (3, 1) <= sys.version_info < (3, 3):
- print('oauth2client requires python3 version >= 3.3.', file=sys.stderr)
+if (3, 1) <= sys.version_info < (3, 4):
+ print('oauth2client requires python3 version >= 3.4.', file=sys.stderr)
sys.exit(1)
install_requires = [
@@ -41,29 +41,36 @@ install_requires = [
'six>=1.6.1',
]
-long_desc = """The oauth2client is a client library for OAuth 2.0."""
+long_desc = """
+oauth2client is a client library for OAuth 2.0.
+
+Note: oauth2client is now deprecated. No more features will be added to the
+ libraries and the core team is turning down support. We recommend you use
+ `google-auth <https://google-auth.readthedocs.io>`__ and
+ `oauthlib <http://oauthlib.readthedocs.io/>`__.
+"""
version = oauth2client.__version__
setup(
- name="oauth2client",
+ name='oauth2client',
version=version,
- description="OAuth 2.0 client library",
+ description='OAuth 2.0 client library',
long_description=long_desc,
- author="Google Inc.",
- url="http://github.com/google/oauth2client/",
+ author='Google Inc.',
+ author_email='jonwayne+oauth2client@google.com',
+ url='http://github.com/google/oauth2client/',
install_requires=install_requires,
- packages=find_packages(),
- license="Apache 2.0",
- keywords="google oauth 2.0 http client",
+ packages=find_packages(exclude=('tests*',)),
+ license='Apache 2.0',
+ keywords='google oauth 2.0 http client',
classifiers=[
'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
- 'Development Status :: 5 - Production/Stable',
+ 'Programming Language :: Python :: 3.5',
+ 'Development Status :: 7 - Inactive',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Operating System :: POSIX',
diff --git a/tests/__init__.py b/tests/__init__.py
index 5f6567c..e69de29 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,22 +0,0 @@
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Test package set-up."""
-
-from oauth2client import util
-
-__author__ = 'afshar@google.com (Ali Afshar)'
-
-
-def setup_package():
- """Run on testing package."""
- util.positional_parameters_enforcement = util.POSITIONAL_EXCEPTION
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..caadb80
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,31 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Py.test hooks."""
+
+from oauth2client import _helpers
+
+
+def pytest_addoption(parser):
+ """Adds the --gae-sdk option to py.test.
+
+ This is used to enable the GAE tests. This has to be in this conftest.py
+ due to the way py.test collects conftest files."""
+ parser.addoption('--gae-sdk')
+
+
+def pytest_configure(config):
+ """Py.test hook called before loading tests."""
+ # Default of POSITIONAL_WARNING is too verbose for testing
+ _helpers.positional_parameters_enforcement = _helpers.POSITIONAL_EXCEPTION
diff --git a/tests/contrib/appengine/__init__.py b/tests/contrib/appengine/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/contrib/appengine/__init__.py
diff --git a/tests/contrib/appengine/conftest.py b/tests/contrib/appengine/conftest.py
new file mode 100644
index 0000000..b56fbcd
--- /dev/null
+++ b/tests/contrib/appengine/conftest.py
@@ -0,0 +1,53 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""App Engine py.test configuration."""
+
+import sys
+
+from six.moves import reload_module
+
+
+def set_up_gae_environment(sdk_path):
+ """Set up appengine SDK third-party imports.
+
+ The App Engine SDK does terrible things to the global interpreter state.
+ Because of this, this stuff can't be neatly undone. As such, it can't be
+ a fixture.
+ """
+ if 'google' in sys.modules:
+ # Some packages, such as protobuf, clobber the google
+ # namespace package. This prevents that.
+ reload_module(sys.modules['google'])
+
+ # This sets up google-provided libraries.
+ sys.path.insert(0, sdk_path)
+ import dev_appserver
+ dev_appserver.fix_sys_path()
+
+ # Fixes timezone and other os-level items.
+ import google.appengine.tools.os_compat # noqa: unused import
+
+
+def pytest_configure(config):
+ """Configures the App Engine SDK imports on py.test startup."""
+ if config.getoption('gae_sdk') is not None:
+ set_up_gae_environment(config.getoption('gae_sdk'))
+
+
+def pytest_ignore_collect(path, config):
+ """Skip App Engine tests when --gae-sdk is not specified."""
+ return (
+ 'contrib/appengine' in str(path) and
+ config.getoption('gae_sdk') is None)
diff --git a/tests/contrib/test__appengine_ndb.py b/tests/contrib/appengine/test__appengine_ndb.py
index 41e3805..9af1dcc 100644
--- a/tests/contrib/test__appengine_ndb.py
+++ b/tests/contrib/appengine/test__appengine_ndb.py
@@ -14,17 +14,17 @@
import json
import os
+import unittest
from google.appengine.ext import ndb
from google.appengine.ext import testbed
import mock
-import unittest2
from oauth2client import client
from oauth2client.contrib import appengine
-DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
+DATA_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'data')
def datafile(filename):
@@ -36,7 +36,7 @@ class TestNDBModel(ndb.Model):
creds = appengine.CredentialsNDBProperty()
-class TestFlowNDBProperty(unittest2.TestCase):
+class TestFlowNDBProperty(unittest.TestCase):
def setUp(self):
self.testbed = testbed.Testbed()
@@ -85,7 +85,7 @@ class TestFlowNDBProperty(unittest2.TestCase):
type(flow_val))
-class TestCredentialsNDBProperty(unittest2.TestCase):
+class TestCredentialsNDBProperty(unittest.TestCase):
def setUp(self):
self.testbed = testbed.Testbed()
diff --git a/tests/contrib/test_appengine.py b/tests/contrib/appengine/test_appengine.py
index cdaf6c5..36d2713 100644
--- a/tests/contrib/test_appengine.py
+++ b/tests/contrib/appengine/test_appengine.py
@@ -17,10 +17,7 @@ import json
import os
import tempfile
import time
-
-import dev_appserver
-
-dev_appserver.fix_sys_path()
+import unittest
from google.appengine.api import apiproxy_stub
from google.appengine.api import apiproxy_stub_map
@@ -31,10 +28,9 @@ from google.appengine.api.memcache import memcache_stub
from google.appengine.ext import db
from google.appengine.ext import ndb
from google.appengine.ext import testbed
-import httplib2
import mock
from six.moves import urllib
-import unittest2
+from six.moves import urllib_parse
import webapp2
from webtest import TestApp
@@ -42,11 +38,20 @@ import oauth2client
from oauth2client import client
from oauth2client import clientsecrets
from oauth2client.contrib import appengine
-from ..http_mock import CacheMock
+from tests import http_mock
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
+DATA_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'data')
+DEFAULT_RESP = """\
+{
+ "access_token": "foo_access_token",
+ "expires_in": 3600,
+ "extra": "value",
+ "refresh_token": "foo_refresh_token"
+}
+"""
+BASIC_TOKEN = 'bar'
+BASIC_RESP = json.dumps({'access_token': BASIC_TOKEN})
def datafile(filename):
@@ -75,23 +80,7 @@ class UserNotLoggedInMock(object):
return None
-class Http2Mock(object):
- """Mock httplib2.Http"""
- status = 200
- content = {
- 'access_token': 'foo_access_token',
- 'refresh_token': 'foo_refresh_token',
- 'expires_in': 3600,
- 'extra': 'value',
- }
-
- def request(self, token_uri, method, body, headers, *args, **kwargs):
- self.body = body
- self.headers = headers
- return self, json.dumps(self.content)
-
-
-class TestAppAssertionCredentials(unittest2.TestCase):
+class TestAppAssertionCredentials(unittest.TestCase):
account_name = "service_account_name@appspot.com"
signature = "signature"
@@ -139,7 +128,7 @@ class TestAppAssertionCredentials(unittest2.TestCase):
scope = 'http://www.googleapis.com/scope'
credentials = appengine.AppAssertionCredentials(scope)
- http = httplib2.Http()
+ http = http_mock.HttpMock(data=DEFAULT_RESP)
with self.assertRaises(client.AccessTokenRefreshError):
credentials.refresh(http)
@@ -155,7 +144,7 @@ class TestAppAssertionCredentials(unittest2.TestCase):
"http://www.googleapis.com/scope",
"http://www.googleapis.com/scope2"]
credentials = appengine.AppAssertionCredentials(scope)
- http = httplib2.Http()
+ http = http_mock.HttpMock(data=DEFAULT_RESP)
credentials.refresh(http)
self.assertEqual('a_token_123', credentials.access_token)
@@ -168,7 +157,7 @@ class TestAppAssertionCredentials(unittest2.TestCase):
scope = ('http://www.googleapis.com/scope '
'http://www.googleapis.com/scope2')
credentials = appengine.AppAssertionCredentials(scope)
- http = httplib2.Http()
+ http = http_mock.HttpMock(data=DEFAULT_RESP)
credentials.refresh(http)
self.assertEqual('a_token_123', credentials.access_token)
self.assertEqual(
@@ -184,7 +173,7 @@ class TestAppAssertionCredentials(unittest2.TestCase):
autospec=True) as get_access_token:
credentials = appengine.AppAssertionCredentials(
scope, service_account_id=account_id)
- http = httplib2.Http()
+ http = http_mock.HttpMock(data=DEFAULT_RESP)
credentials.refresh(http)
self.assertEqual('a_token_456', credentials.access_token)
@@ -276,7 +265,7 @@ class TestFlowModel(db.Model):
flow = appengine.FlowProperty()
-class FlowPropertyTest(unittest2.TestCase):
+class FlowPropertyTest(unittest.TestCase):
def setUp(self):
self.testbed = testbed.Testbed()
@@ -315,7 +304,7 @@ class TestCredentialsModel(db.Model):
credentials = appengine.CredentialsProperty()
-class CredentialsPropertyTest(unittest2.TestCase):
+class CredentialsPropertyTest(unittest.TestCase):
def setUp(self):
self.testbed = testbed.Testbed()
@@ -369,14 +358,7 @@ class CredentialsPropertyTest(unittest2.TestCase):
appengine.CredentialsProperty().validate(42)
-def _http_request(*args, **kwargs):
- resp = httplib2.Response({'status': '200'})
- content = json.dumps({'access_token': 'bar'})
-
- return resp, content
-
-
-class StorageByKeyNameTest(unittest2.TestCase):
+class StorageByKeyNameTest(unittest.TestCase):
def setUp(self):
self.testbed = testbed.Testbed()
@@ -420,6 +402,23 @@ class StorageByKeyNameTest(unittest2.TestCase):
storage._model = appengine.CredentialsNDBModel
self.assertTrue(storage._is_ndb())
+ def _verify_basic_refresh(self, http):
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, oauth2client.GOOGLE_TOKEN_URI)
+ self.assertEqual(http.method, 'POST')
+ expected_body = {
+ 'grant_type': ['refresh_token'],
+ 'client_id': [self.credentials.client_id],
+ 'client_secret': [self.credentials.client_secret],
+ 'refresh_token': [self.credentials.refresh_token],
+ }
+ self.assertEqual(urllib_parse.parse_qs(http.body), expected_body)
+ expected_headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ 'user-agent': self.credentials.user_agent,
+ }
+ self.assertEqual(http.headers, expected_headers)
+
def test_get_and_put_simple(self):
storage = appengine.StorageByKeyName(
appengine.CredentialsModel, 'foo', 'credentials')
@@ -427,9 +426,12 @@ class StorageByKeyNameTest(unittest2.TestCase):
self.assertEqual(None, storage.get())
self.credentials.set_store(storage)
- self.credentials._refresh(_http_request)
+ http = http_mock.HttpMock(data=BASIC_RESP)
+ self.credentials._refresh(http)
credmodel = appengine.CredentialsModel.get_by_key_name('foo')
- self.assertEqual('bar', credmodel.credentials.access_token)
+ self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token)
+ # Verify mock.
+ self._verify_basic_refresh(http)
def test_get_and_put_cached(self):
storage = appengine.StorageByKeyName(
@@ -438,16 +440,17 @@ class StorageByKeyNameTest(unittest2.TestCase):
self.assertEqual(None, storage.get())
self.credentials.set_store(storage)
- self.credentials._refresh(_http_request)
+ http = http_mock.HttpMock(data=BASIC_RESP)
+ self.credentials._refresh(http)
credmodel = appengine.CredentialsModel.get_by_key_name('foo')
- self.assertEqual('bar', credmodel.credentials.access_token)
+ self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token)
# Now remove the item from the cache.
memcache.delete('foo')
# Check that getting refreshes the cache.
credentials = storage.get()
- self.assertEqual('bar', credentials.access_token)
+ self.assertEqual(BASIC_TOKEN, credentials.access_token)
self.assertNotEqual(None, memcache.get('foo'))
# Deleting should clear the cache.
@@ -456,6 +459,9 @@ class StorageByKeyNameTest(unittest2.TestCase):
self.assertEqual(None, credentials)
self.assertEqual(None, memcache.get('foo'))
+ # Verify mock.
+ self._verify_basic_refresh(http)
+
def test_get_and_put_set_store_on_cache_retrieval(self):
storage = appengine.StorageByKeyName(
appengine.CredentialsModel, 'foo', 'credentials', cache=memcache)
@@ -468,9 +474,13 @@ class StorageByKeyNameTest(unittest2.TestCase):
old_creds = storage.get()
self.assertEqual(old_creds.access_token, 'foo')
old_creds.invalid = True
- old_creds._refresh(_http_request)
+ http = http_mock.HttpMock(data=BASIC_RESP)
+ old_creds._refresh(http)
new_creds = storage.get()
- self.assertEqual(new_creds.access_token, 'bar')
+ self.assertEqual(new_creds.access_token, BASIC_TOKEN)
+
+ # Verify mock.
+ self._verify_basic_refresh(http)
def test_get_and_put_ndb(self):
# Start empty
@@ -480,12 +490,16 @@ class StorageByKeyNameTest(unittest2.TestCase):
# Refresh storage and retrieve without using storage
self.credentials.set_store(storage)
- self.credentials._refresh(_http_request)
+ http = http_mock.HttpMock(data=BASIC_RESP)
+ self.credentials._refresh(http)
credmodel = appengine.CredentialsNDBModel.get_by_id('foo')
- self.assertEqual('bar', credmodel.credentials.access_token)
+ self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token)
self.assertEqual(credmodel.credentials.to_json(),
self.credentials.to_json())
+ # Verify mock.
+ self._verify_basic_refresh(http)
+
def test_delete_ndb(self):
# Start empty
storage = appengine.StorageByKeyName(
@@ -511,14 +525,18 @@ class StorageByKeyNameTest(unittest2.TestCase):
# Set NDB store and refresh to add to storage
self.credentials.set_store(storage)
- self.credentials._refresh(_http_request)
+ http = http_mock.HttpMock(data=BASIC_RESP)
+ self.credentials._refresh(http)
# Retrieve same key from DB model to confirm mixing works
credmodel = appengine.CredentialsModel.get_by_key_name('foo')
- self.assertEqual('bar', credmodel.credentials.access_token)
+ self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token)
self.assertEqual(self.credentials.to_json(),
credmodel.credentials.to_json())
+ # Verify mock.
+ self._verify_basic_refresh(http)
+
def test_get_and_put_mixed_db_storage_ndb_get(self):
# Start empty
storage = appengine.StorageByKeyName(
@@ -527,14 +545,18 @@ class StorageByKeyNameTest(unittest2.TestCase):
# Set DB store and refresh to add to storage
self.credentials.set_store(storage)
- self.credentials._refresh(_http_request)
+ http = http_mock.HttpMock(data=BASIC_RESP)
+ self.credentials._refresh(http)
# Retrieve same key from NDB model to confirm mixing works
credmodel = appengine.CredentialsNDBModel.get_by_id('foo')
- self.assertEqual('bar', credmodel.credentials.access_token)
+ self.assertEqual(BASIC_TOKEN, credmodel.credentials.access_token)
self.assertEqual(self.credentials.to_json(),
credmodel.credentials.to_json())
+ # Verify mock.
+ self._verify_basic_refresh(http)
+
def test_delete_db_ndb_mixed(self):
# Start empty
storage_ndb = appengine.StorageByKeyName(
@@ -573,7 +595,7 @@ class MockRequestHandler(object):
request = MockRequest()
-class DecoratorTests(unittest2.TestCase):
+class DecoratorTests(unittest.TestCase):
def setUp(self):
self.testbed = testbed.Testbed()
@@ -630,12 +652,9 @@ class DecoratorTests(unittest2.TestCase):
})
self.current_user = user_mock()
users.get_current_user = self.current_user
- self.httplib2_orig = httplib2.Http
- httplib2.Http = Http2Mock
def tearDown(self):
self.testbed.deactivate()
- httplib2.Http = self.httplib2_orig
def test_in_error(self):
# NOTE: This branch is never reached. _in_error is not set by any code
@@ -655,7 +674,9 @@ class DecoratorTests(unittest2.TestCase):
app.router.match_routes[0].handler.__name__,
'OAuth2Handler')
- def test_required(self):
+ @mock.patch('oauth2client.transport.get_http_object')
+ def test_required(self, new_http):
+ new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP)
# An initial request to an oauth_required decorated path should be a
# redirect to start the OAuth dance.
self.assertEqual(self.decorator.flow, None)
@@ -688,7 +709,7 @@ class DecoratorTests(unittest2.TestCase):
response_query = urllib.parse.parse_qs(parts[1])
response = response_query[
self.decorator._token_response_param][0]
- self.assertEqual(Http2Mock.content,
+ self.assertEqual(json.loads(DEFAULT_RESP),
json.loads(urllib.parse.unquote(response)))
self.assertEqual(self.decorator.flow, self.decorator._tls.flow)
self.assertEqual(self.decorator.credentials,
@@ -736,7 +757,12 @@ class DecoratorTests(unittest2.TestCase):
self.assertEqual('http://localhost/oauth2callback',
query_params['redirect_uri'][0])
- def test_storage_delete(self):
+ # Check the mocks were called.
+ new_http.assert_called_once_with()
+
+ @mock.patch('oauth2client.transport.get_http_object')
+ def test_storage_delete(self, new_http):
+ new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP)
# An initial request to an oauth_required decorated path should be a
# redirect to start the OAuth dance.
response = self.app.get('/foo_path')
@@ -772,7 +798,12 @@ class DecoratorTests(unittest2.TestCase):
parse_state_value.assert_called_once_with(
'foo_path:xsrfkey123', self.current_user)
- def test_aware(self):
+ # Check the mocks were called.
+ new_http.assert_called_once_with()
+
+ @mock.patch('oauth2client.transport.get_http_object')
+ def test_aware(self, new_http):
+ new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP)
# An initial request to an oauth_aware decorated path should
# not redirect.
response = self.app.get('http://localhost/bar_path/2012/01')
@@ -825,6 +856,9 @@ class DecoratorTests(unittest2.TestCase):
self.should_raise = False
self.assertEqual(None, self.decorator.credentials)
+ # Check the mocks were called.
+ new_http.assert_called_once_with()
+
def test_error_in_step2(self):
# An initial request to an oauth_aware decorated path should
# not redirect.
@@ -855,10 +889,14 @@ class DecoratorTests(unittest2.TestCase):
self.assertEqual(decorator.flow, decorator._tls.flow)
def test_token_response_param(self):
+ # No need to set-up a mock since test_required() does.
self.decorator._token_response_param = 'foobar'
self.test_required()
- def test_decorator_from_client_secrets(self):
+ @mock.patch('oauth2client.transport.get_http_object')
+ def test_decorator_from_client_secrets(self, new_http):
+ new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP)
+ # Execute test after setting up mock.
decorator = appengine.OAuth2DecoratorFromClientSecrets(
datafile('client_secrets.json'),
scope=['foo_scope', 'bar_scope'])
@@ -873,10 +911,13 @@ class DecoratorTests(unittest2.TestCase):
# revoke_uri is not required
self.assertEqual(self.decorator._revoke_uri,
- 'https://accounts.google.com/o/oauth2/revoke')
+ 'https://oauth2.googleapis.com/revoke')
self.assertEqual(self.decorator._revoke_uri,
self.decorator.credentials.revoke_uri)
+ # Check the mocks were called.
+ new_http.assert_called_once_with()
+
def test_decorator_from_client_secrets_toplevel(self):
decorator_patch = mock.patch(
'oauth2client.contrib.appengine.OAuth2DecoratorFromClientSecrets')
@@ -915,7 +956,7 @@ class DecoratorTests(unittest2.TestCase):
self.assertIn('prompt', decorator._kwargs)
def test_decorator_from_cached_client_secrets(self):
- cache_mock = CacheMock()
+ cache_mock = http_mock.CacheMock()
load_and_cache('client_secrets.json', 'secret', cache_mock)
decorator = appengine.OAuth2DecoratorFromClientSecrets(
# filename, scope, message=None, cache=None
@@ -975,11 +1016,11 @@ class DecoratorTests(unittest2.TestCase):
'oauth2client.contrib.appengine.clientsecrets.loadfile')
with loadfile_patch as loadfile_mock:
loadfile_mock.return_value = (clientsecrets.TYPE_WEB, {
- "client_id": "foo_client_id",
- "client_secret": "foo_client_secret",
- "redirect_uris": [],
- "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
- "token_uri": "https://www.googleapis.com/oauth2/v4/token",
+ 'client_id': 'foo_client_id',
+ 'client_secret': 'foo_client_secret',
+ 'redirect_uris': [],
+ 'auth_uri': oauth2client.GOOGLE_AUTH_URI,
+ 'token_uri': oauth2client.GOOGLE_TOKEN_URI,
# No revoke URI
})
@@ -991,7 +1032,10 @@ class DecoratorTests(unittest2.TestCase):
# This is never set, but it's consistent with other tests.
self.assertFalse(decorator._in_error)
- def test_invalid_state(self):
+ @mock.patch('oauth2client.transport.get_http_object')
+ def test_invalid_state(self, new_http):
+ new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP)
+ # Execute test after setting up mock.
with mock.patch.object(appengine, '_parse_state_value',
return_value=None, autospec=True):
# Now simulate the callback to /oauth2callback.
@@ -1002,8 +1046,11 @@ class DecoratorTests(unittest2.TestCase):
self.assertEqual('200 OK', response.status)
self.assertEqual('The authorization request failed', response.body)
+ # Check the mocks were called.
+ new_http.assert_called_once_with()
+
-class DecoratorXsrfSecretTests(unittest2.TestCase):
+class DecoratorXsrfSecretTests(unittest.TestCase):
"""Test xsrf_secret_key."""
def setUp(self):
@@ -1052,7 +1099,7 @@ class DecoratorXsrfSecretTests(unittest2.TestCase):
self.assertEqual(site_key.secret, secret)
-class DecoratorXsrfProtectionTests(unittest2.TestCase):
+class DecoratorXsrfProtectionTests(unittest.TestCase):
"""Test _build_state_value and _parse_state_value."""
def setUp(self):
diff --git a/tests/contrib/django_util/test_decorators.py b/tests/contrib/django_util/test_decorators.py
index 846c6dd..f237f88 100644
--- a/tests/contrib/django_util/test_decorators.py
+++ b/tests/contrib/django_util/test_decorators.py
@@ -18,18 +18,18 @@ import copy
from django import http
import django.conf
-from django.contrib.auth.models import AnonymousUser, User
+from django.contrib.auth import models as django_models
import mock
from six.moves import http_client
from six.moves import reload_module
from six.moves.urllib import parse
-from tests.contrib.django_util import TestWithDjangoEnvironment
import oauth2client.contrib.django_util
from oauth2client.contrib.django_util import decorators
+from tests.contrib import django_util as tests_django_util
-class OAuth2EnabledDecoratorTest(TestWithDjangoEnvironment):
+class OAuth2EnabledDecoratorTest(tests_django_util.TestWithDjangoEnvironment):
def setUp(self):
super(OAuth2EnabledDecoratorTest, self).setUp()
@@ -39,7 +39,7 @@ class OAuth2EnabledDecoratorTest(TestWithDjangoEnvironment):
# at import time, so in order for us to reload the settings
# we need to reload the module
reload_module(oauth2client.contrib.django_util)
- self.user = User.objects.create_user(
+ self.user = django_models.User.objects.create_user(
username='bill', email='bill@example.com', password='hunter2')
def tearDown(self):
@@ -63,7 +63,7 @@ class OAuth2EnabledDecoratorTest(TestWithDjangoEnvironment):
@mock.patch('oauth2client.client.OAuth2Credentials')
def test_has_credentials_in_storage(self, OAuth2Credentials):
request = self.factory.get('/test')
- request.session = mock.MagicMock()
+ request.session = mock.Mock()
credentials_mock = mock.Mock(
scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES))
@@ -88,11 +88,11 @@ class OAuth2EnabledDecoratorTest(TestWithDjangoEnvironment):
@mock.patch('oauth2client.contrib.dictionary_storage.DictionaryStorage')
def test_specified_scopes(self, dictionary_storage_mock):
request = self.factory.get('/test')
- request.session = mock.MagicMock()
+ request.session = mock.Mock()
credentials_mock = mock.Mock(
scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES))
- credentials_mock.has_scopes = True
+ credentials_mock.has_scopes = mock.Mock(return_value=True)
credentials_mock.is_valid = True
dictionary_storage_mock.get.return_value = credentials_mock
@@ -106,14 +106,14 @@ class OAuth2EnabledDecoratorTest(TestWithDjangoEnvironment):
self.assertFalse(request.oauth.has_credentials())
-class OAuth2RequiredDecoratorTest(TestWithDjangoEnvironment):
+class OAuth2RequiredDecoratorTest(tests_django_util.TestWithDjangoEnvironment):
def setUp(self):
super(OAuth2RequiredDecoratorTest, self).setUp()
self.save_settings = copy.deepcopy(django.conf.settings)
reload_module(oauth2client.contrib.django_util)
- self.user = User.objects.create_user(
+ self.user = django_models.User.objects.create_user(
username='bill', email='bill@example.com', password='hunter2')
def tearDown(self):
@@ -141,13 +141,13 @@ class OAuth2RequiredDecoratorTest(TestWithDjangoEnvironment):
@mock.patch('oauth2client.contrib.django_util.UserOAuth2', autospec=True)
def test_has_credentials_in_storage(self, UserOAuth2):
request = self.factory.get('/test')
- request.session = mock.MagicMock()
+ request.session = mock.Mock()
@decorators.oauth_required
def test_view(request):
return http.HttpResponse("test")
- my_user_oauth = mock.MagicMock()
+ my_user_oauth = mock.Mock()
UserOAuth2.return_value = my_user_oauth
my_user_oauth.has_credentials.return_value = True
@@ -161,7 +161,7 @@ class OAuth2RequiredDecoratorTest(TestWithDjangoEnvironment):
self, OAuth2Credentials):
request = self.factory.get('/test')
- request.session = mock.MagicMock()
+ request.session = mock.Mock()
credentials_mock = mock.Mock(
scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES))
credentials_mock.has_scopes.return_value = False
@@ -179,11 +179,11 @@ class OAuth2RequiredDecoratorTest(TestWithDjangoEnvironment):
@mock.patch('oauth2client.client.OAuth2Credentials')
def test_specified_scopes(self, OAuth2Credentials):
request = self.factory.get('/test')
- request.session = mock.MagicMock()
+ request.session = mock.Mock()
credentials_mock = mock.Mock(
scopes=set(django.conf.settings.GOOGLE_OAUTH2_SCOPES))
- credentials_mock.has_scopes = False
+ credentials_mock.has_scopes = mock.Mock(return_value=False)
OAuth2Credentials.from_json.return_value = credentials_mock
@decorators.oauth_required(scopes=['additional-scope'])
@@ -195,7 +195,8 @@ class OAuth2RequiredDecoratorTest(TestWithDjangoEnvironment):
response.status_code, django.http.HttpResponseRedirect.status_code)
-class OAuth2RequiredDecoratorStorageModelTest(TestWithDjangoEnvironment):
+class OAuth2RequiredDecoratorStorageModelTest(
+ tests_django_util.TestWithDjangoEnvironment):
def setUp(self):
super(OAuth2RequiredDecoratorStorageModelTest, self).setUp()
@@ -209,7 +210,7 @@ class OAuth2RequiredDecoratorStorageModelTest(TestWithDjangoEnvironment):
django.conf.settings.GOOGLE_OAUTH2_STORAGE_MODEL = STORAGE_MODEL
reload_module(oauth2client.contrib.django_util)
- self.user = User.objects.create_user(
+ self.user = django_models.User.objects.create_user(
username='bill', email='bill@example.com', password='hunter2')
def tearDown(self):
@@ -219,7 +220,7 @@ class OAuth2RequiredDecoratorStorageModelTest(TestWithDjangoEnvironment):
def test_redirects_anonymous_to_login(self):
request = self.factory.get('/test')
request.session = self.session
- request.user = AnonymousUser()
+ request.user = django_models.AnonymousUser()
@decorators.oauth_required
def test_view(request):
@@ -233,7 +234,7 @@ class OAuth2RequiredDecoratorStorageModelTest(TestWithDjangoEnvironment):
def test_redirects_user_to_oauth_authorize(self):
request = self.factory.get('/test')
request.session = self.session
- request.user = User.objects.create_user(
+ request.user = django_models.User.objects.create_user(
username='bill3', email='bill@example.com', password='hunter2')
@decorators.oauth_required
diff --git a/tests/contrib/django_util/test_django_models.py b/tests/contrib/django_util/test_django_models.py
index aeaed15..da54965 100644
--- a/tests/contrib/django_util/test_django_models.py
+++ b/tests/contrib/django_util/test_django_models.py
@@ -19,36 +19,42 @@ Unit tests for models and fields defined by the django_util helper.
import base64
import pickle
+import unittest
-from tests.contrib.django_util.models import CredentialsModel
+import jsonpickle
-import unittest2
+from oauth2client import _helpers
+from oauth2client import client
+from oauth2client.contrib.django_util import models
+from tests.contrib.django_util import models as tests_models
-from oauth2client._helpers import _from_bytes
-from oauth2client.client import Credentials
-from oauth2client.contrib.django_util.models import CredentialsField
-
-class TestCredentialsField(unittest2.TestCase):
+class TestCredentialsField(unittest.TestCase):
def setUp(self):
- self.fake_model = CredentialsModel()
+ self.fake_model = tests_models.CredentialsModel()
self.fake_model_field = self.fake_model._meta.get_field('credentials')
- self.field = CredentialsField(null=True)
- self.credentials = Credentials()
- self.pickle_str = _from_bytes(
+ self.field = models.CredentialsField(null=True)
+ self.credentials = client.Credentials()
+ self.pickle_str = _helpers._from_bytes(
base64.b64encode(pickle.dumps(self.credentials)))
+ self.jsonpickle_str = _helpers._from_bytes(
+ base64.b64encode(jsonpickle.encode(self.credentials).encode()))
def test_field_is_text(self):
self.assertEqual(self.field.get_internal_type(), 'BinaryField')
def test_field_unpickled(self):
self.assertIsInstance(
- self.field.to_python(self.pickle_str), Credentials)
+ self.field.to_python(self.pickle_str), client.Credentials)
+
+ def test_field_jsonunpickled(self):
+ self.assertIsInstance(
+ self.field.to_python(self.jsonpickle_str), client.Credentials)
def test_field_already_unpickled(self):
self.assertIsInstance(
- self.field.to_python(self.credentials), Credentials)
+ self.field.to_python(self.credentials), client.Credentials)
def test_none_field_unpickled(self):
self.assertIsNone(self.field.to_python(None))
@@ -56,7 +62,7 @@ class TestCredentialsField(unittest2.TestCase):
def test_from_db_value(self):
value = self.field.from_db_value(
self.pickle_str, None, None, None)
- self.assertIsInstance(value, Credentials)
+ self.assertIsInstance(value, client.Credentials)
def test_field_unpickled_none(self):
self.assertEqual(self.field.to_python(None), None)
@@ -64,12 +70,12 @@ class TestCredentialsField(unittest2.TestCase):
def test_field_pickled(self):
prep_value = self.field.get_db_prep_value(self.credentials,
connection=None)
- self.assertEqual(prep_value, self.pickle_str)
+ self.assertEqual(prep_value, self.jsonpickle_str)
def test_field_value_to_string(self):
self.fake_model.credentials = self.credentials
value_str = self.fake_model_field.value_to_string(self.fake_model)
- self.assertEqual(value_str, self.pickle_str)
+ self.assertEqual(value_str, self.jsonpickle_str)
def test_field_value_to_string_none(self):
self.fake_model.credentials = None
@@ -77,11 +83,11 @@ class TestCredentialsField(unittest2.TestCase):
self.assertIsNone(value_str)
def test_credentials_without_null(self):
- credentials = CredentialsField()
+ credentials = models.CredentialsField()
self.assertTrue(credentials.null)
-class CredentialWithSetStore(CredentialsField):
+class CredentialWithSetStore(models.CredentialsField):
def __init__(self):
self.model = CredentialWithSetStore
@@ -96,4 +102,4 @@ class FakeCredentialsModelMock(object):
class FakeCredentialsModelMockNoSet(object):
- credentials = CredentialsField()
+ credentials = models.CredentialsField()
diff --git a/tests/contrib/django_util/test_django_storage.py b/tests/contrib/django_util/test_django_storage.py
index 8f76b18..a608c94 100644
--- a/tests/contrib/django_util/test_django_storage.py
+++ b/tests/contrib/django_util/test_django_storage.py
@@ -16,10 +16,10 @@
# Mock a Django environment
import datetime
+import unittest
from django.db import models
import mock
-import unittest2
from oauth2client import GOOGLE_TOKEN_URI
from oauth2client.client import OAuth2Credentials
@@ -28,7 +28,7 @@ from oauth2client.contrib.django_util.storage import (
DjangoORMStorage as Storage)
-class TestStorage(unittest2.TestCase):
+class TestStorage(unittest.TestCase):
def setUp(self):
access_token = 'foo'
client_id = 'some_client_id'
diff --git a/tests/contrib/django_util/test_django_util.py b/tests/contrib/django_util/test_django_util.py
index 84457cb..82d7be7 100644
--- a/tests/contrib/django_util/test_django_util.py
+++ b/tests/contrib/django_util/test_django_util.py
@@ -15,20 +15,19 @@
"""Tests the initialization logic of django_util."""
import copy
+import unittest
import django.conf
from django.conf.urls import include, url
-from django.contrib.auth.models import AnonymousUser
+from django.contrib.auth import models as django_models
from django.core import exceptions
import mock
from six.moves import reload_module
-from tests.contrib.django_util import TestWithDjangoEnvironment
-import unittest2
from oauth2client.contrib import django_util
import oauth2client.contrib.django_util
-from oauth2client.contrib.django_util import (
- _CREDENTIALS_KEY, get_storage, site, UserOAuth2)
+from oauth2client.contrib.django_util import site
+from tests.contrib import django_util as tests_django_util
urlpatterns = [
@@ -36,7 +35,7 @@ urlpatterns = [
]
-class OAuth2SetupTest(unittest2.TestCase):
+class OAuth2SetupTest(unittest.TestCase):
def setUp(self):
self.save_settings = copy.deepcopy(django.conf.settings)
@@ -101,6 +100,20 @@ class OAuth2SetupTest(unittest2.TestCase):
object.__new__(django_util.OAuth2Settings),
django.conf.settings)
+ def test_no_middleware(self):
+ django.conf.settings.MIDDLEWARE_CLASSES = None
+ with self.assertRaises(exceptions.ImproperlyConfigured):
+ django_util.OAuth2Settings.__init__(
+ object.__new__(django_util.OAuth2Settings),
+ django.conf.settings)
+
+ def test_middleware_no_classes(self):
+ django.conf.settings.MIDDLEWARE = (
+ django.conf.settings.MIDDLEWARE_CLASSES)
+ django.conf.settings.MIDDLEWARE_CLASSES = None
+ # primarily testing this doesn't raise an exception
+ django_util.OAuth2Settings(django.conf.settings)
+
def test_storage_model(self):
STORAGE_MODEL = {
'model': 'tests.contrib.django_util.models.CredentialsModel',
@@ -121,7 +134,7 @@ class MockObjectWithSession(object):
self.session = session
-class SessionStorageTest(TestWithDjangoEnvironment):
+class SessionStorageTest(tests_django_util.TestWithDjangoEnvironment):
def setUp(self):
super(SessionStorageTest, self).setUp()
@@ -133,19 +146,19 @@ class SessionStorageTest(TestWithDjangoEnvironment):
django.conf.settings = copy.deepcopy(self.save_settings)
def test_session_delete(self):
- self.session[_CREDENTIALS_KEY] = "test_val"
+ self.session[django_util._CREDENTIALS_KEY] = "test_val"
request = MockObjectWithSession(self.session)
- django_storage = get_storage(request)
+ django_storage = django_util.get_storage(request)
django_storage.delete()
- self.assertIsNone(self.session.get(_CREDENTIALS_KEY))
+ self.assertIsNone(self.session.get(django_util._CREDENTIALS_KEY))
def test_session_delete_nothing(self):
request = MockObjectWithSession(self.session)
- django_storage = get_storage(request)
+ django_storage = django_util.get_storage(request)
django_storage.delete()
-class TestUserOAuth2Object(TestWithDjangoEnvironment):
+class TestUserOAuth2Object(tests_django_util.TestWithDjangoEnvironment):
def setUp(self):
super(TestUserOAuth2Object, self).setUp()
@@ -167,6 +180,6 @@ class TestUserOAuth2Object(TestWithDjangoEnvironment):
request = self.factory.get('oauth2/oauth2authorize',
data={'return_url': '/return_endpoint'})
request.session = self.session
- request.user = AnonymousUser()
- oauth2 = UserOAuth2(request)
+ request.user = django_models.AnonymousUser()
+ oauth2 = django_util.UserOAuth2(request)
self.assertIsNone(oauth2.credentials)
diff --git a/tests/contrib/django_util/test_views.py b/tests/contrib/django_util/test_views.py
index df0d11c..0b3fe30 100644
--- a/tests/contrib/django_util/test_views.py
+++ b/tests/contrib/django_util/test_views.py
@@ -20,27 +20,25 @@ import json
import django
from django import http
import django.conf
-from django.contrib.auth.models import AnonymousUser, User
+from django.contrib.auth import models as django_models
import mock
from six.moves import reload_module
-from tests.contrib.django_util import TestWithDjangoEnvironment
-from tests.contrib.django_util.models import CredentialsModel
-
-from oauth2client.client import FlowExchangeError, OAuth2WebServerFlow
+from oauth2client import client
import oauth2client.contrib.django_util
from oauth2client.contrib.django_util import views
-from oauth2client.contrib.django_util.models import CredentialsField
+from tests.contrib import django_util as tests_django_util
+from tests.contrib.django_util import models as tests_models
-class OAuth2AuthorizeTest(TestWithDjangoEnvironment):
+class OAuth2AuthorizeTest(tests_django_util.TestWithDjangoEnvironment):
def setUp(self):
super(OAuth2AuthorizeTest, self).setUp()
self.save_settings = copy.deepcopy(django.conf.settings)
reload_module(oauth2client.contrib.django_util)
- self.user = User.objects.create_user(
- username='bill', email='bill@example.com', password='hunter2')
+ self.user = django_models.User.objects.create_user(
+ username='bill', email='bill@example.com', password='hunter2')
def tearDown(self):
django.conf.settings = copy.deepcopy(self.save_settings)
@@ -55,7 +53,7 @@ class OAuth2AuthorizeTest(TestWithDjangoEnvironment):
def test_authorize_anonymous_user(self):
request = self.factory.get('oauth2/oauth2authorize')
request.session = self.session
- request.user = AnonymousUser()
+ request.user = django_models.AnonymousUser()
response = views.oauth2_authorize(request)
self.assertIsInstance(response, http.HttpResponseRedirect)
@@ -68,7 +66,8 @@ class OAuth2AuthorizeTest(TestWithDjangoEnvironment):
self.assertIsInstance(response, http.HttpResponseRedirect)
-class Oauth2AuthorizeStorageModelTest(TestWithDjangoEnvironment):
+class Oauth2AuthorizeStorageModelTest(
+ tests_django_util.TestWithDjangoEnvironment):
def setUp(self):
super(Oauth2AuthorizeStorageModelTest, self).setUp()
@@ -85,7 +84,7 @@ class Oauth2AuthorizeStorageModelTest(TestWithDjangoEnvironment):
# at import time, so in order for us to reload the settings
# we need to reload the module
reload_module(oauth2client.contrib.django_util)
- self.user = User.objects.create_user(
+ self.user = django_models.User.objects.create_user(
username='bill', email='bill@example.com', password='hunter2')
def tearDown(self):
@@ -103,7 +102,7 @@ class Oauth2AuthorizeStorageModelTest(TestWithDjangoEnvironment):
def test_authorize_anonymous_user_redirects_login(self):
request = self.factory.get('oauth2/oauth2authorize')
request.session = self.session
- request.user = AnonymousUser()
+ request.user = django_models.AnonymousUser()
response = views.oauth2_authorize(request)
self.assertIsInstance(response, http.HttpResponseRedirect)
# redirects to Django login
@@ -117,25 +116,53 @@ class Oauth2AuthorizeStorageModelTest(TestWithDjangoEnvironment):
response = views.oauth2_authorize(request)
self.assertIsInstance(response, http.HttpResponseRedirect)
- def test_authorized_user_not_logged_in_redirects(self):
+ def test_authorized_user_no_credentials_redirects(self):
+ request = self.factory.get('oauth2/oauth2authorize',
+ data={'return_url': '/return_endpoint'})
+ request.session = self.session
+
+ authorized_user = django_models.User.objects.create_user(
+ username='bill2', email='bill@example.com', password='hunter2')
+
+ tests_models.CredentialsModel.objects.create(
+ user_id=authorized_user,
+ credentials=None)
+
+ request.user = authorized_user
+ response = views.oauth2_authorize(request)
+ self.assertIsInstance(response, http.HttpResponseRedirect)
+
+ def test_already_authorized(self):
request = self.factory.get('oauth2/oauth2authorize',
data={'return_url': '/return_endpoint'})
request.session = self.session
- authorized_user = User.objects.create_user(
+ authorized_user = django_models.User.objects.create_user(
username='bill2', email='bill@example.com', password='hunter2')
- credentials = CredentialsField()
- CredentialsModel.objects.create(
+ credentials = _Credentials()
+ tests_models.CredentialsModel.objects.create(
user_id=authorized_user,
credentials=credentials)
request.user = authorized_user
response = views.oauth2_authorize(request)
self.assertIsInstance(response, http.HttpResponseRedirect)
+ self.assertEqual(response.url, '/return_endpoint')
+
+
+class _Credentials(object):
+ # Can't use mock when testing Django models
+ # https://code.djangoproject.com/ticket/25493
+ def __init__(self):
+ self.invalid = False
+ self.scopes = set()
+
+ def has_scopes(self, _):
+ return True
-class Oauth2CallbackTest(TestWithDjangoEnvironment):
+class Oauth2CallbackTest(tests_django_util.TestWithDjangoEnvironment):
def setUp(self):
super(Oauth2CallbackTest, self).setUp()
@@ -149,11 +176,11 @@ class Oauth2CallbackTest(TestWithDjangoEnvironment):
'return_url': self.RETURN_URL,
'scopes': django.conf.settings.GOOGLE_OAUTH2_SCOPES
}
- self.user = User.objects.create_user(
+ self.user = django_models.User.objects.create_user(
username='bill', email='bill@example.com', password='hunter2')
- @mock.patch('oauth2client.contrib.django_util.views.pickle')
- def test_callback_works(self, pickle):
+ @mock.patch('oauth2client.contrib.django_util.views.jsonpickle')
+ def test_callback_works(self, jsonpickle_mock):
request = self.factory.get('oauth2/oauth2callback', data={
'state': json.dumps(self.fake_state),
'code': 123
@@ -161,7 +188,7 @@ class Oauth2CallbackTest(TestWithDjangoEnvironment):
self.session['google_oauth2_csrf_token'] = self.CSRF_TOKEN
- flow = OAuth2WebServerFlow(
+ flow = client.OAuth2WebServerFlow(
client_id='clientid',
client_secret='clientsecret',
scope=['email'],
@@ -169,9 +196,10 @@ class Oauth2CallbackTest(TestWithDjangoEnvironment):
redirect_uri=request.build_absolute_uri("oauth2/oauth2callback"))
name = 'google_oauth2_flow_{0}'.format(self.CSRF_TOKEN)
- self.session[name] = pickle.dumps(flow)
+ pickled_flow = object()
+ self.session[name] = pickled_flow
flow.step2_exchange = mock.Mock()
- pickle.loads.return_value = flow
+ jsonpickle_mock.decode.return_value = flow
request.session = self.session
request.user = self.user
@@ -180,9 +208,10 @@ class Oauth2CallbackTest(TestWithDjangoEnvironment):
self.assertEqual(
response.status_code, django.http.HttpResponseRedirect.status_code)
self.assertEqual(response['Location'], self.RETURN_URL)
+ jsonpickle_mock.decode.assert_called_once_with(pickled_flow)
- @mock.patch('oauth2client.contrib.django_util.views.pickle')
- def test_callback_handles_bad_flow_exchange(self, pickle):
+ @mock.patch('oauth2client.contrib.django_util.views.jsonpickle')
+ def test_callback_handles_bad_flow_exchange(self, jsonpickle_mock):
request = self.factory.get('oauth2/oauth2callback', data={
"state": json.dumps(self.fake_state),
"code": 123
@@ -190,25 +219,27 @@ class Oauth2CallbackTest(TestWithDjangoEnvironment):
self.session['google_oauth2_csrf_token'] = self.CSRF_TOKEN
- flow = OAuth2WebServerFlow(
+ flow = client.OAuth2WebServerFlow(
client_id='clientid',
client_secret='clientsecret',
scope=['email'],
state=json.dumps(self.fake_state),
redirect_uri=request.build_absolute_uri('oauth2/oauth2callback'))
- self.session['google_oauth2_flow_{0}'.format(self.CSRF_TOKEN)] \
- = pickle.dumps(flow)
+ session_key = 'google_oauth2_flow_{0}'.format(self.CSRF_TOKEN)
+ pickled_flow = object()
+ self.session[session_key] = pickled_flow
def local_throws(code):
- raise FlowExchangeError('test')
+ raise client.FlowExchangeError('test')
flow.step2_exchange = local_throws
- pickle.loads.return_value = flow
+ jsonpickle_mock.decode.return_value = flow
request.session = self.session
response = views.oauth2_callback(request)
self.assertIsInstance(response, http.HttpResponseBadRequest)
+ jsonpickle_mock.decode.assert_called_once_with(pickled_flow)
def test_error_returns_bad_request(self):
request = self.factory.get('oauth2/oauth2callback', data={
@@ -218,6 +249,15 @@ class Oauth2CallbackTest(TestWithDjangoEnvironment):
self.assertIsInstance(response, http.HttpResponseBadRequest)
self.assertIn(b'Authorization failed', response.content)
+ def test_error_escapes_html(self):
+ request = self.factory.get('oauth2/oauth2callback', data={
+ 'error': '<script>bad</script>',
+ })
+ response = views.oauth2_callback(request)
+ self.assertIsInstance(response, http.HttpResponseBadRequest)
+ self.assertNotIn(b'<script>', response.content)
+ self.assertIn(b'&lt;script&gt;', response.content)
+
def test_no_session(self):
request = self.factory.get('oauth2/oauth2callback', data={
'code': 123,
diff --git a/tests/contrib/test_devshell.py b/tests/contrib/test_devshell.py
index 659a53b..1346080 100644
--- a/tests/contrib/test_devshell.py
+++ b/tests/contrib/test_devshell.py
@@ -19,9 +19,9 @@ import json
import os
import socket
import threading
+import unittest
import mock
-import unittest2
from oauth2client import _helpers
from oauth2client import client
@@ -38,7 +38,7 @@ DEFAULT_CREDENTIAL_JSON = json.dumps([
])
-class TestCredentialInfoResponse(unittest2.TestCase):
+class TestCredentialInfoResponse(unittest.TestCase):
def test_constructor_with_non_list(self):
json_non_list = '{}'
@@ -71,29 +71,29 @@ class TestCredentialInfoResponse(unittest2.TestCase):
self.assertEqual(info_response.expires_in, expires_in)
-class Test_SendRecv(unittest2.TestCase):
+class Test_SendRecv(unittest.TestCase):
def test_port_zero(self):
with mock.patch('oauth2client.contrib.devshell.os') as os_mod:
- os_mod.getenv = mock.MagicMock(name='getenv', return_value=0)
+ os_mod.getenv = mock.Mock(name='getenv', return_value=0)
with self.assertRaises(devshell.NoDevshellServer):
devshell._SendRecv()
os_mod.getenv.assert_called_once_with(devshell.DEVSHELL_ENV, 0)
def test_no_newline_in_received_header(self):
non_zero_port = 1
- sock = mock.MagicMock()
+ sock = mock.Mock()
header_without_newline = ''
- sock.recv(6).decode = mock.MagicMock(
+ sock.recv(6).decode = mock.Mock(
name='decode', return_value=header_without_newline)
with mock.patch('oauth2client.contrib.devshell.os') as os_mod:
- os_mod.getenv = mock.MagicMock(name='getenv',
- return_value=non_zero_port)
+ os_mod.getenv = mock.Mock(name='getenv',
+ return_value=non_zero_port)
with mock.patch('oauth2client.contrib.devshell.socket') as socket:
- socket.socket = mock.MagicMock(name='socket',
- return_value=sock)
+ socket.socket = mock.Mock(name='socket',
+ return_value=sock)
with self.assertRaises(devshell.CommunicationError):
devshell._SendRecv()
os_mod.getenv.assert_called_once_with(devshell.DEVSHELL_ENV, 0)
@@ -160,15 +160,15 @@ class _AuthReferenceServer(threading.Thread):
s.recv(to_read, socket.MSG_WAITALL))
if resp_buffer != devshell.CREDENTIAL_INFO_REQUEST_JSON:
self.bad_request = True
- l = len(self.response)
- s.sendall('{0}\n{1}'.format(l, self.response).encode())
+ response_len = len(self.response)
+ s.sendall('{0}\n{1}'.format(response_len, self.response).encode())
finally:
# Will fail if s is None, but these tests never encounter
# that scenario.
s.close()
-class DevshellCredentialsTests(unittest2.TestCase):
+class DevshellCredentialsTests(unittest.TestCase):
def test_signals_no_server(self):
with self.assertRaises(devshell.NoDevshellServer):
diff --git a/tests/contrib/test_dictionary_storage.py b/tests/contrib/test_dictionary_storage.py
index 888c938..b9f833b 100644
--- a/tests/contrib/test_dictionary_storage.py
+++ b/tests/contrib/test_dictionary_storage.py
@@ -14,7 +14,7 @@
"""Unit tests for oauth2client.contrib.dictionary_storage"""
-import unittest2
+import unittest
import oauth2client
from oauth2client import client
@@ -37,7 +37,7 @@ def _generate_credentials(scopes=None):
scopes=scopes)
-class DictionaryStorageTests(unittest2.TestCase):
+class DictionaryStorageTests(unittest.TestCase):
def test_constructor_defaults(self):
dictionary = {}
diff --git a/tests/contrib/test_flask_util.py b/tests/contrib/test_flask_util.py
index 74cb218..112bff0 100644
--- a/tests/contrib/test_flask_util.py
+++ b/tests/contrib/test_flask_util.py
@@ -17,54 +17,31 @@
import datetime
import json
import logging
+import unittest
import flask
-import httplib2
import mock
import six.moves.http_client as httplib
import six.moves.urllib.parse as urlparse
-import unittest2
import oauth2client
from oauth2client import client
from oauth2client import clientsecrets
from oauth2client.contrib import flask_util
+from tests import http_mock
-__author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
+DEFAULT_RESP = """\
+{
+ "access_token": "foo_access_token",
+ "expires_in": 3600,
+ "extra": "value",
+ "refresh_token": "foo_refresh_token"
+}
+"""
-class Http2Mock(object):
- """Mock httplib2.Http for code exchange / refresh"""
-
- def __init__(self, status=httplib.OK, **kwargs):
- self.status = status
- self.content = {
- 'access_token': 'foo_access_token',
- 'refresh_token': 'foo_refresh_token',
- 'expires_in': 3600,
- 'extra': 'value',
- }
- self.content.update(kwargs)
-
- def request(self, token_uri, method, body, headers, *args, **kwargs):
- self.body = body
- self.headers = headers
- return (self, json.dumps(self.content).encode('utf-8'))
-
- def __enter__(self):
- self.httplib2_orig = httplib2.Http
- httplib2.Http = self
- return self
-
- def __exit__(self, exc_type, exc_value, traceback):
- httplib2.Http = self.httplib2_orig
-
- def __call__(self, *args, **kwargs):
- return self
-
-
-class FlaskOAuth2Tests(unittest2.TestCase):
+class FlaskOAuth2Tests(unittest.TestCase):
def setUp(self):
self.app = flask.Flask(__name__)
@@ -246,7 +223,12 @@ class FlaskOAuth2Tests(unittest2.TestCase):
def test_callback_view(self):
self.oauth2.storage = mock.Mock()
with self.app.test_client() as client:
- with Http2Mock() as http:
+ with mock.patch(
+ 'oauth2client.transport.get_http_object') as new_http:
+ # Set-up mock.
+ http = http_mock.HttpMock(data=DEFAULT_RESP)
+ new_http.return_value = http
+ # Run tests.
state = self._setup_callback_state(client)
response = client.get(
@@ -258,6 +240,9 @@ class FlaskOAuth2Tests(unittest2.TestCase):
self.assertIn('codez', http.body)
self.assertTrue(self.oauth2.storage.put.called)
+ # Check the mocks were called.
+ new_http.assert_called_once_with()
+
def test_authorize_callback(self):
self.oauth2.authorize_callback = mock.Mock()
self.test_callback_view()
@@ -273,6 +258,18 @@ class FlaskOAuth2Tests(unittest2.TestCase):
self.assertEqual(response.status_code, httplib.BAD_REQUEST)
self.assertIn('something', response.data.decode('utf-8'))
+ # Error supplied to callback with html
+ with self.app.test_client() as client:
+ with client.session_transaction() as session:
+ session['google_oauth2_csrf_token'] = 'tokenz'
+
+ response = client.get(
+ '/oauth2callback?state={}&error=<script>something<script>')
+ self.assertEqual(response.status_code, httplib.BAD_REQUEST)
+ self.assertIn(
+ '&lt;script&gt;something&lt;script&gt;',
+ response.data.decode('utf-8'))
+
# CSRF mismatch
with self.app.test_client() as client:
with client.session_transaction() as session:
@@ -296,11 +293,20 @@ class FlaskOAuth2Tests(unittest2.TestCase):
with self.app.test_client() as client:
state = self._setup_callback_state(client)
- with Http2Mock(status=httplib.INTERNAL_SERVER_ERROR):
+ with mock.patch(
+ 'oauth2client.transport.get_http_object') as new_http:
+ # Set-up mock.
+ new_http.return_value = http_mock.HttpMock(
+ headers={'status': httplib.INTERNAL_SERVER_ERROR},
+ data=DEFAULT_RESP)
+ # Run tests.
response = client.get(
'/oauth2callback?state={0}&code=codez'.format(state))
self.assertEqual(response.status_code, httplib.BAD_REQUEST)
+ # Check the mocks were called.
+ new_http.assert_called_once_with()
+
# Invalid state json
with self.app.test_client() as client:
with client.session_transaction() as session:
@@ -495,7 +501,10 @@ class FlaskOAuth2Tests(unittest2.TestCase):
def test_incremental_auth_exchange(self):
self._create_incremental_auth_app()
- with Http2Mock():
+ with mock.patch('oauth2client.transport.get_http_object') as new_http:
+ # Set-up mock.
+ new_http.return_value = http_mock.HttpMock(data=DEFAULT_RESP)
+ # Run tests.
with self.app.test_client() as client:
state = self._setup_callback_state(
client,
@@ -511,16 +520,21 @@ class FlaskOAuth2Tests(unittest2.TestCase):
self.assertTrue(
credentials.has_scopes(['email', 'one', 'two']))
+ # Check the mocks were called.
+ new_http.assert_called_once_with()
+
def test_refresh(self):
+ token_val = 'new_token'
+ json_resp = '{"access_token": "%s"}' % (token_val,)
+ http = http_mock.HttpMock(data=json_resp)
with self.app.test_request_context():
with mock.patch('flask.session'):
self.oauth2.storage.put(self._generate_credentials())
- self.oauth2.credentials.refresh(
- Http2Mock(access_token='new_token'))
+ self.oauth2.credentials.refresh(http)
self.assertEqual(
- self.oauth2.storage.get().access_token, 'new_token')
+ self.oauth2.storage.get().access_token, token_val)
def test_delete(self):
with self.app.test_request_context():
diff --git a/tests/contrib/test_gce.py b/tests/contrib/test_gce.py
index e71bd44..5f34995 100644
--- a/tests/contrib/test_gce.py
+++ b/tests/contrib/test_gce.py
@@ -16,26 +16,28 @@
import datetime
import json
+import os
+import unittest
-import httplib2
import mock
from six.moves import http_client
-from tests.contrib.test_metadata import request_mock
-import unittest2
+from six.moves import reload_module
from oauth2client import client
+from oauth2client.contrib import _metadata
from oauth2client.contrib import gce
+from tests import http_mock
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
SERVICE_ACCOUNT_INFO = {
'scopes': ['a', 'b'],
'email': 'a@example.com',
'aliases': ['default']
}
+METADATA_PATH = 'instance/service-accounts/a@example.com/token'
-class AppAssertionCredentialsTests(unittest2.TestCase):
+class AppAssertionCredentialsTests(unittest.TestCase):
def test_constructor(self):
credentials = gce.AppAssertionCredentials()
@@ -68,8 +70,7 @@ class AppAssertionCredentialsTests(unittest2.TestCase):
@mock.patch('oauth2client.contrib._metadata.get_service_account_info',
return_value=SERVICE_ACCOUNT_INFO)
def test_refresh_token(self, get_info, get_token):
- http_request = mock.MagicMock()
- http_mock = mock.MagicMock(request=http_request)
+ http_mock = object()
credentials = gce.AppAssertionCredentials()
credentials.invalid = False
credentials.service_account_email = 'a@example.com'
@@ -77,26 +78,34 @@ class AppAssertionCredentialsTests(unittest2.TestCase):
credentials.get_access_token(http=http_mock)
self.assertEqual(credentials.access_token, 'A')
self.assertTrue(credentials.access_token_expired)
- get_token.assert_called_with(http_request,
+ get_token.assert_called_with(http_mock,
service_account='a@example.com')
credentials.get_access_token(http=http_mock)
self.assertEqual(credentials.access_token, 'B')
self.assertFalse(credentials.access_token_expired)
- get_token.assert_called_with(http_request,
+ get_token.assert_called_with(http_mock,
service_account='a@example.com')
get_info.assert_not_called()
def test_refresh_token_failed_fetch(self):
- http_request = request_mock(
- http_client.NOT_FOUND,
- 'application/json',
- json.dumps({'access_token': 'a', 'expires_in': 100})
- )
+ headers = {
+ 'status': http_client.NOT_FOUND,
+ 'content-type': 'application/json',
+ }
+ response = json.dumps({'access_token': 'a', 'expires_in': 100})
+ http = http_mock.HttpMock(headers=headers, data=response)
credentials = gce.AppAssertionCredentials()
credentials.invalid = False
credentials.service_account_email = 'a@example.com'
with self.assertRaises(client.HttpAccessTokenRefreshError):
- credentials._refresh(http_request)
+ credentials._refresh(http)
+ # Verify mock.
+ self.assertEqual(http.requests, 1)
+ expected_uri = _metadata.METADATA_ROOT + METADATA_PATH
+ self.assertEqual(http.uri, expected_uri)
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertEqual(http.headers, _metadata.METADATA_HEADERS)
def test_serialization_data(self):
credentials = gce.AppAssertionCredentials()
@@ -115,8 +124,7 @@ class AppAssertionCredentialsTests(unittest2.TestCase):
@mock.patch('oauth2client.contrib._metadata.get_service_account_info',
return_value=SERVICE_ACCOUNT_INFO)
def test_retrieve_scopes(self, metadata):
- http_request = mock.MagicMock()
- http_mock = mock.MagicMock(request=http_request)
+ http_mock = object()
credentials = gce.AppAssertionCredentials()
self.assertTrue(credentials.invalid)
self.assertIsNone(credentials.scopes)
@@ -125,19 +133,18 @@ class AppAssertionCredentialsTests(unittest2.TestCase):
self.assertFalse(credentials.invalid)
credentials.retrieve_scopes(http_mock)
# Assert scopes weren't refetched
- metadata.assert_called_once_with(http_request,
+ metadata.assert_called_once_with(http_mock,
service_account='default')
@mock.patch('oauth2client.contrib._metadata.get_service_account_info',
- side_effect=httplib2.HttpLib2Error('No Such Email'))
+ side_effect=http_client.HTTPException('No Such Email'))
def test_retrieve_scopes_bad_email(self, metadata):
- http_request = mock.MagicMock()
- http_mock = mock.MagicMock(request=http_request)
+ http_mock = object()
credentials = gce.AppAssertionCredentials(email='b@example.com')
- with self.assertRaises(httplib2.HttpLib2Error):
+ with self.assertRaises(http_client.HTTPException):
credentials.retrieve_scopes(http_mock)
- metadata.assert_called_once_with(http_request,
+ metadata.assert_called_once_with(http_mock,
service_account='b@example.com')
def test_save_to_well_known_file(self):
@@ -150,3 +157,19 @@ class AppAssertionCredentialsTests(unittest2.TestCase):
client.save_to_well_known_file(credentials)
finally:
os.path.isdir = ORIGINAL_ISDIR
+
+ def test_custom_metadata_root_from_env(self):
+ headers = {'content-type': 'application/json'}
+ http = http_mock.HttpMock(headers=headers, data='{}')
+ fake_metadata_root = 'another.metadata.service'
+ os.environ['GCE_METADATA_ROOT'] = fake_metadata_root
+ reload_module(_metadata)
+ try:
+ _metadata.get(http, '')
+ finally:
+ del os.environ['GCE_METADATA_ROOT']
+ reload_module(_metadata)
+ # Verify mock.
+ self.assertEqual(http.requests, 1)
+ expected_uri = 'http://{}/computeMetadata/v1/'.format(fake_metadata_root)
+ self.assertEqual(http.uri, expected_uri)
diff --git a/tests/contrib/test_keyring_storage.py b/tests/contrib/test_keyring_storage.py
index 5d274c0..0f8090d 100644
--- a/tests/contrib/test_keyring_storage.py
+++ b/tests/contrib/test_keyring_storage.py
@@ -16,20 +16,17 @@
import datetime
import threading
+import unittest
import keyring
import mock
-import unittest2
import oauth2client
from oauth2client import client
from oauth2client.contrib import keyring_storage
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
-
-class KeyringStorageTests(unittest2.TestCase):
+class KeyringStorageTests(unittest.TestCase):
def test_constructor(self):
service_name = 'my_unit_test'
@@ -58,15 +55,15 @@ class KeyringStorageTests(unittest2.TestCase):
service_name = 'my_unit_test'
user_name = 'me'
mock_content = (object(), 'mock_content')
- mock_return_creds = mock.MagicMock()
- mock_return_creds.set_store = set_store = mock.MagicMock(
+ mock_return_creds = mock.Mock()
+ mock_return_creds.set_store = set_store = mock.Mock(
name='set_store')
with mock.patch.object(keyring, 'get_password',
return_value=mock_content,
autospec=True) as get_password:
class_name = 'oauth2client.client.Credentials'
with mock.patch(class_name) as MockCreds:
- MockCreds.new_from_json = new_from_json = mock.MagicMock(
+ MockCreds.new_from_json = new_from_json = mock.Mock(
name='new_from_json', return_value=mock_return_creds)
store = keyring_storage.Storage(service_name, user_name)
credentials = store.locked_get()
@@ -82,9 +79,9 @@ class KeyringStorageTests(unittest2.TestCase):
with mock.patch.object(keyring, 'set_password',
return_value=None,
autospec=True) as set_password:
- credentials = mock.MagicMock()
+ credentials = mock.Mock()
to_json_ret = object()
- credentials.to_json = to_json = mock.MagicMock(
+ credentials.to_json = to_json = mock.Mock(
name='to_json', return_value=to_json_ret)
store.locked_put(credentials)
to_json.assert_called_once_with()
diff --git a/tests/contrib/test_locked_file.py b/tests/contrib/test_locked_file.py
deleted file mode 100644
index 384bef3..0000000
--- a/tests/contrib/test_locked_file.py
+++ /dev/null
@@ -1,244 +0,0 @@
-# Copyright 2016 Google Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import errno
-import os
-import sys
-import tempfile
-
-import mock
-import unittest2
-
-from oauth2client.contrib import locked_file
-
-
-class TestOpener(unittest2.TestCase):
- def _make_one(self):
- _filehandle, filename = tempfile.mkstemp()
- os.close(_filehandle)
- return locked_file._Opener(filename, 'r+', 'r'), filename
-
- def test_ctor(self):
- instance, filename = self._make_one()
- self.assertFalse(instance._locked)
- self.assertEqual(instance._filename, filename)
- self.assertEqual(instance._mode, 'r+')
- self.assertEqual(instance._fallback_mode, 'r')
- self.assertIsNone(instance._fh)
- self.assertIsNone(instance._lock_fd)
-
- def test_is_locked(self):
- instance, _ = self._make_one()
- self.assertFalse(instance.is_locked())
- instance._locked = True
- self.assertTrue(instance.is_locked())
-
- def test_file_handle(self):
- instance, _ = self._make_one()
- self.assertIsNone(instance.file_handle())
- fh = mock.Mock()
- instance._fh = fh
- self.assertEqual(instance.file_handle(), fh)
-
- def test_filename(self):
- instance, filename = self._make_one()
- self.assertEqual(instance.filename(), filename)
-
- def test_open_and_lock(self):
- instance, _ = self._make_one()
- instance.open_and_lock(1, 1)
-
- def test_unlock_and_close(self):
- instance, _ = self._make_one()
- instance.unlock_and_close()
-
-
-class TestPosixOpener(TestOpener):
- def _make_one(self):
- _filehandle, filename = tempfile.mkstemp()
- os.close(_filehandle)
- return locked_file._PosixOpener(filename, 'r+', 'r'), filename
-
- def test_relock_fail(self):
- instance, _ = self._make_one()
- instance.open_and_lock(1, 1)
-
- self.assertTrue(instance.is_locked())
- self.assertIsNotNone(instance.file_handle())
- with self.assertRaises(locked_file.AlreadyLockedException):
- instance.open_and_lock(1, 1)
-
- @mock.patch('oauth2client.contrib.locked_file.open', create=True)
- def test_lock_access_error_fallback_mode(self, mock_open):
- # NOTE: This is a bad case. The behavior here should be that the
- # error gets re-raised, but the module lets the if statement fall
- # through.
- instance, _ = self._make_one()
- mock_open.side_effect = [IOError(errno.ENOENT, '')]
- instance.open_and_lock(1, 1)
-
- self.assertIsNone(instance.file_handle())
- self.assertTrue(instance.is_locked())
-
- @mock.patch('oauth2client.contrib.locked_file.open', create=True)
- def test_lock_non_access_error(self, mock_open):
- instance, _ = self._make_one()
- fh_mock = mock.Mock()
- mock_open.side_effect = [IOError(errno.EACCES, ''), fh_mock]
- instance.open_and_lock(1, 1)
-
- self.assertEqual(instance.file_handle(), fh_mock)
- self.assertFalse(instance.is_locked())
-
- @mock.patch('oauth2client.contrib.locked_file.open', create=True)
- def test_lock_unexpected_error(self, mock_open):
- instance, _ = self._make_one()
-
- with mock.patch('os.open') as mock_os_open:
- mock_os_open.side_effect = [OSError(errno.EPERM, '')]
- with self.assertRaises(OSError):
- instance.open_and_lock(1, 1)
-
- @mock.patch('oauth2client.contrib.locked_file.open', create=True)
- @mock.patch('oauth2client.contrib.locked_file.logger')
- @mock.patch('time.time')
- def test_lock_timeout_error(self, mock_time, mock_logger, mock_open):
- instance, _ = self._make_one()
- # Make it seem like 10 seconds have passed between calls.
- mock_time.side_effect = [0, 10]
-
- with mock.patch('os.open') as mock_os_open:
- # Raising EEXIST should cause it to try to retry locking.
- mock_os_open.side_effect = [OSError(errno.EEXIST, '')]
- instance.open_and_lock(1, 1)
- self.assertFalse(instance.is_locked())
- self.assertTrue(mock_logger.warn.called)
-
- @mock.patch('oauth2client.contrib.locked_file.open', create=True)
- @mock.patch('oauth2client.contrib.locked_file.logger')
- @mock.patch('time.time')
- def test_lock_timeout_error_no_fh(self, mock_time, mock_logger, mock_open):
- instance, _ = self._make_one()
- # Make it seem like 10 seconds have passed between calls.
- mock_time.side_effect = [0, 10]
- # This will cause the retry loop to enter without a file handle.
- fh_mock = mock.Mock()
- mock_open.side_effect = [IOError(errno.ENOENT, ''), fh_mock]
-
- with mock.patch('os.open') as mock_os_open:
- # Raising EEXIST should cause it to try to retry locking.
- mock_os_open.side_effect = [OSError(errno.EEXIST, '')]
- instance.open_and_lock(1, 1)
- self.assertFalse(instance.is_locked())
- self.assertTrue(mock_logger.warn.called)
- self.assertEqual(instance.file_handle(), fh_mock)
-
- @mock.patch('oauth2client.contrib.locked_file.open', create=True)
- @mock.patch('time.time')
- @mock.patch('time.sleep')
- def test_lock_retry_success(self, mock_sleep, mock_time, mock_open):
- instance, _ = self._make_one()
- # Make it seem like 1 second has passed between calls. Extra values
- # are needed by the logging module.
- mock_time.side_effect = [0, 1]
-
- with mock.patch('os.open') as mock_os_open:
- # Raising EEXIST should cause it to try to retry locking.
- mock_os_open.side_effect = [
- OSError(errno.EEXIST, ''), mock.Mock()]
- instance.open_and_lock(10, 1)
- print(mock_os_open.call_args_list)
- self.assertTrue(instance.is_locked())
- mock_sleep.assert_called_with(1)
-
- @mock.patch('oauth2client.contrib.locked_file.os')
- def test_unlock(self, os_mock):
- instance, _ = self._make_one()
- instance._locked = True
- lock_fd_mock = instance._lock_fd = mock.Mock()
- instance._fh = mock.Mock()
-
- instance.unlock_and_close()
-
- self.assertFalse(instance.is_locked())
- os_mock.close.assert_called_once_with(lock_fd_mock)
- self.assertTrue(os_mock.unlink.called)
- self.assertTrue(instance._fh.close.called)
-
-
-class TestLockedFile(unittest2.TestCase):
-
- @mock.patch('oauth2client.contrib.locked_file._PosixOpener')
- def _make_one(self, opener_ctor_mock):
- opener_mock = mock.Mock()
- opener_ctor_mock.return_value = opener_mock
- return locked_file.LockedFile(
- 'a_file', 'r+', 'r', use_native_locking=False), opener_mock
-
- @mock.patch('oauth2client.contrib.locked_file._PosixOpener')
- def test_ctor_minimal(self, opener_mock):
- locked_file.LockedFile(
- 'a_file', 'r+', 'r', use_native_locking=False)
- opener_mock.assert_called_with('a_file', 'r+', 'r')
-
- @mock.patch.dict('sys.modules', {
- 'oauth2client.contrib._win32_opener': mock.Mock()})
- def test_ctor_native_win32(self):
- _win32_opener_mock = sys.modules['oauth2client.contrib._win32_opener']
- locked_file.LockedFile(
- 'a_file', 'r+', 'r', use_native_locking=True)
- _win32_opener_mock._Win32Opener.assert_called_with('a_file', 'r+', 'r')
-
- @mock.patch.dict('sys.modules', {
- 'oauth2client.contrib._win32_opener': None,
- 'oauth2client.contrib._fcntl_opener': mock.Mock()})
- def test_ctor_native_fcntl(self):
- _fnctl_opener_mock = sys.modules['oauth2client.contrib._fcntl_opener']
- locked_file.LockedFile(
- 'a_file', 'r+', 'r', use_native_locking=True)
- _fnctl_opener_mock._FcntlOpener.assert_called_with('a_file', 'r+', 'r')
-
- @mock.patch('oauth2client.contrib.locked_file._PosixOpener')
- @mock.patch.dict('sys.modules', {
- 'oauth2client.contrib._win32_opener': None,
- 'oauth2client.contrib._fcntl_opener': None})
- def test_ctor_native_posix_fallback(self, opener_mock):
- locked_file.LockedFile(
- 'a_file', 'r+', 'r', use_native_locking=True)
- opener_mock.assert_called_with('a_file', 'r+', 'r')
-
- def test_filename(self):
- instance, opener = self._make_one()
- opener._filename = 'some file'
- self.assertEqual(instance.filename(), 'some file')
-
- def test_file_handle(self):
- instance, opener = self._make_one()
- self.assertEqual(instance.file_handle(), opener.file_handle())
- self.assertTrue(opener.file_handle.called)
-
- def test_is_locked(self):
- instance, opener = self._make_one()
- self.assertEqual(instance.is_locked(), opener.is_locked())
- self.assertTrue(opener.is_locked.called)
-
- def test_open_and_lock(self):
- instance, opener = self._make_one()
- instance.open_and_lock()
- opener.open_and_lock.assert_called_with(0, 0.05)
-
- def test_unlock_and_close(self):
- instance, opener = self._make_one()
- instance.unlock_and_close()
- opener.unlock_and_close.assert_called_with()
diff --git a/tests/contrib/test_metadata.py b/tests/contrib/test_metadata.py
index 7f11d04..cd48f0a 100644
--- a/tests/contrib/test_metadata.py
+++ b/tests/contrib/test_metadata.py
@@ -14,84 +14,103 @@
import datetime
import json
+import unittest
-import httplib2
import mock
from six.moves import http_client
-import unittest2
from oauth2client.contrib import _metadata
+from tests import http_mock
+
PATH = 'instance/service-accounts/default'
DATA = {'foo': 'bar'}
EXPECTED_URL = (
'http://metadata.google.internal/computeMetadata/v1/instance'
'/service-accounts/default')
-EXPECTED_KWARGS = dict(headers=_metadata.METADATA_HEADERS)
def request_mock(status, content_type, content):
- return mock.MagicMock(return_value=(
- httplib2.Response(
- {'status': status, 'content-type': content_type}
- ),
- content.encode('utf-8')
- ))
+ headers = {'status': status, 'content-type': content_type}
+ http = http_mock.HttpMock(headers=headers,
+ data=content.encode('utf-8'))
+ return http
-class TestMetadata(unittest2.TestCase):
+class TestMetadata(unittest.TestCase):
def test_get_success_json(self):
- http_request = request_mock(
+ http = request_mock(
http_client.OK, 'application/json', json.dumps(DATA))
self.assertEqual(
- _metadata.get(http_request, PATH),
+ _metadata.get(http, PATH),
DATA
)
- http_request.assert_called_once_with(EXPECTED_URL, **EXPECTED_KWARGS)
+
+ # Verify mocks.
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, EXPECTED_URL)
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertEqual(http.headers, _metadata.METADATA_HEADERS)
def test_get_success_string(self):
- http_request = request_mock(
+ http = request_mock(
http_client.OK, 'text/html', '<p>Hello World!</p>')
self.assertEqual(
- _metadata.get(http_request, PATH),
+ _metadata.get(http, PATH),
'<p>Hello World!</p>'
)
- http_request.assert_called_once_with(EXPECTED_URL, **EXPECTED_KWARGS)
+
+ # Verify mocks.
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, EXPECTED_URL)
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertEqual(http.headers, _metadata.METADATA_HEADERS)
def test_get_failure(self):
- http_request = request_mock(
+ http = request_mock(
http_client.NOT_FOUND, 'text/html', '<p>Error</p>')
- with self.assertRaises(httplib2.HttpLib2Error):
- _metadata.get(http_request, PATH)
+ with self.assertRaises(http_client.HTTPException):
+ _metadata.get(http, PATH)
- http_request.assert_called_once_with(EXPECTED_URL, **EXPECTED_KWARGS)
+ # Verify mocks.
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, EXPECTED_URL)
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertEqual(http.headers, _metadata.METADATA_HEADERS)
@mock.patch(
'oauth2client.client._UTCNOW',
return_value=datetime.datetime.min)
def test_get_token_success(self, now):
- http_request = request_mock(
+ http = request_mock(
http_client.OK,
'application/json',
json.dumps({'access_token': 'a', 'expires_in': 100})
)
- token, expiry = _metadata.get_token(http_request=http_request)
+ token, expiry = _metadata.get_token(http=http)
self.assertEqual(token, 'a')
self.assertEqual(
expiry, datetime.datetime.min + datetime.timedelta(seconds=100))
- http_request.assert_called_once_with(
- EXPECTED_URL + '/token',
- **EXPECTED_KWARGS
- )
+ # Verify mocks.
now.assert_called_once_with()
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, EXPECTED_URL + '/token')
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertEqual(http.headers, _metadata.METADATA_HEADERS)
def test_service_account_info(self):
- http_request = request_mock(
+ http = request_mock(
http_client.OK, 'application/json', json.dumps(DATA))
- info = _metadata.get_service_account_info(http_request)
+ info = _metadata.get_service_account_info(http)
self.assertEqual(info, DATA)
- http_request.assert_called_once_with(
- EXPECTED_URL + '/?recursive=True',
- **EXPECTED_KWARGS
- )
+ # Verify mock.
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, EXPECTED_URL + '/?recursive=True')
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertEqual(http.headers, _metadata.METADATA_HEADERS)
diff --git a/tests/contrib/test_multiprocess_file_storage.py b/tests/contrib/test_multiprocess_file_storage.py
index bf30c14..d8b91a9 100644
--- a/tests/contrib/test_multiprocess_file_storage.py
+++ b/tests/contrib/test_multiprocess_file_storage.py
@@ -20,16 +20,16 @@ import json
import multiprocessing
import os
import tempfile
+import unittest
import fasteners
import mock
-from six import StringIO
-import unittest2
+import six
+from six.moves import urllib_parse
from oauth2client import client
from oauth2client.contrib import multiprocess_file_storage
-
-from ..http_mock import HttpMockSequence
+from tests import http_mock
@contextlib.contextmanager
@@ -68,14 +68,10 @@ def _generate_token_response_http(new_token='new_token'):
'access_token': new_token,
'expires_in': '3600',
})
- http = HttpMockSequence([
- ({'status': '200'}, token_response),
- ])
-
- return http
+ return http_mock.HttpMock(data=token_response)
-class MultiprocessStorageBehaviorTests(unittest2.TestCase):
+class MultiprocessStorageBehaviorTests(unittest.TestCase):
def setUp(self):
filehandle, self.filename = tempfile.mkstemp(
@@ -115,6 +111,23 @@ class MultiprocessStorageBehaviorTests(unittest2.TestCase):
self.assertIsNone(credentials)
+ def _verify_refresh_payload(self, http, credentials):
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, credentials.token_uri)
+ self.assertEqual(http.method, 'POST')
+ expected_body = {
+ 'grant_type': ['refresh_token'],
+ 'client_id': [credentials.client_id],
+ 'client_secret': [credentials.client_secret],
+ 'refresh_token': [credentials.refresh_token],
+ }
+ self.assertEqual(urllib_parse.parse_qs(http.body), expected_body)
+ expected_headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ 'user-agent': credentials.user_agent,
+ }
+ self.assertEqual(http.headers, expected_headers)
+
def test_single_process_refresh(self):
store = multiprocess_file_storage.MultiprocessFileStorage(
self.filename, 'single-process')
@@ -128,6 +141,9 @@ class MultiprocessStorageBehaviorTests(unittest2.TestCase):
retrieved = store.get()
self.assertEqual(retrieved.access_token, 'new_token')
+ # Verify mocks.
+ self._verify_refresh_payload(http, credentials)
+
def test_multi_process_refresh(self):
# This will test that two processes attempting to refresh credentials
# will only refresh once.
@@ -136,6 +152,7 @@ class MultiprocessStorageBehaviorTests(unittest2.TestCase):
credentials = _create_test_credentials()
credentials.set_store(store)
store.put(credentials)
+ actual_token = 'b'
def child_process_func(
die_event, ready_event, check_event): # pragma: NO COVER
@@ -156,10 +173,12 @@ class MultiprocessStorageBehaviorTests(unittest2.TestCase):
credentials.store.acquire_lock = replacement_acquire_lock
- http = _generate_token_response_http('b')
+ http = _generate_token_response_http(actual_token)
credentials.refresh(http)
+ self.assertEqual(credentials.access_token, actual_token)
- self.assertEqual(credentials.access_token, 'b')
+ # Verify mock http.
+ self._verify_refresh_payload(http, credentials)
check_event = multiprocessing.Event()
with scoped_child_process(child_process_func, check_event=check_event):
@@ -168,15 +187,17 @@ class MultiprocessStorageBehaviorTests(unittest2.TestCase):
store._backend._process_lock.acquire(blocking=False))
check_event.set()
- # The child process will refresh first, so we should end up
- # with 'b' as the token.
- http = mock.Mock()
+ http = _generate_token_response_http('not ' + actual_token)
credentials.refresh(http=http)
- self.assertEqual(credentials.access_token, 'b')
- self.assertFalse(http.request.called)
+ # The child process will refresh first, so we should end up
+ # with `actual_token`' as the token.
+ self.assertEqual(credentials.access_token, actual_token)
+
+ # Make sure the refresh did not make a request.
+ self.assertEqual(http.requests, 0)
retrieved = store.get()
- self.assertEqual(retrieved.access_token, 'b')
+ self.assertEqual(retrieved.access_token, actual_token)
def test_read_only_file_fail_lock(self):
credentials = _create_test_credentials()
@@ -200,7 +221,7 @@ class MultiprocessStorageBehaviorTests(unittest2.TestCase):
self.assertIsNotNone(store.get())
-class MultiprocessStorageUnitTests(unittest2.TestCase):
+class MultiprocessStorageUnitTests(unittest.TestCase):
def setUp(self):
filehandle, self.filename = tempfile.mkstemp(
@@ -233,7 +254,7 @@ class MultiprocessStorageUnitTests(unittest2.TestCase):
def test__read_write_credentials_file(self):
credentials = _create_test_credentials()
- contents = StringIO()
+ contents = six.StringIO()
multiprocess_file_storage._write_credentials_file(
contents, {'key': credentials})
@@ -253,23 +274,23 @@ class MultiprocessStorageUnitTests(unittest2.TestCase):
# the invalid one but still load the valid one.
data['credentials']['invalid'] = '123'
results = multiprocess_file_storage._load_credentials_file(
- StringIO(json.dumps(data)))
+ six.StringIO(json.dumps(data)))
self.assertNotIn('invalid', results)
self.assertEqual(
results['key'].access_token, credentials.access_token)
def test__load_credentials_file_invalid_json(self):
- contents = StringIO('{[')
+ contents = six.StringIO('{[')
self.assertEqual(
multiprocess_file_storage._load_credentials_file(contents), {})
def test__load_credentials_file_no_file_version(self):
- contents = StringIO('{}')
+ contents = six.StringIO('{}')
self.assertEqual(
multiprocess_file_storage._load_credentials_file(contents), {})
def test__load_credentials_file_bad_file_version(self):
- contents = StringIO(json.dumps({'file_version': 1}))
+ contents = six.StringIO(json.dumps({'file_version': 1}))
self.assertEqual(
multiprocess_file_storage._load_credentials_file(contents), {})
@@ -310,4 +331,4 @@ class MultiprocessStorageUnitTests(unittest2.TestCase):
if __name__ == '__main__': # pragma: NO COVER
- unittest2.main()
+ unittest.main()
diff --git a/tests/contrib/test_multistore_file.py b/tests/contrib/test_multistore_file.py
deleted file mode 100644
index b5cb598..0000000
--- a/tests/contrib/test_multistore_file.py
+++ /dev/null
@@ -1,383 +0,0 @@
-# Copyright 2015 Google Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""Unit tests for oauth2client.multistore_file."""
-
-import datetime
-import errno
-import os
-import stat
-import tempfile
-
-import mock
-import unittest2
-
-from oauth2client import client
-from oauth2client import util
-from oauth2client.contrib import locked_file
-from oauth2client.contrib import multistore_file
-
-_filehandle, FILENAME = tempfile.mkstemp('oauth2client_test.data')
-os.close(_filehandle)
-
-
-class _MockLockedFile(object):
-
- def __init__(self, filename_str, error_class, error_code):
- self.filename_str = filename_str
- self.error_class = error_class
- self.error_code = error_code
- self.open_and_lock_called = False
-
- def open_and_lock(self):
- self.open_and_lock_called = True
- raise self.error_class(self.error_code, '')
-
- def is_locked(self):
- return False
-
- def filename(self):
- return self.filename_str
-
-
-class Test__dict_to_tuple_key(unittest2.TestCase):
-
- def test_key_conversions(self):
- key1, val1 = 'somekey', 'some value'
- key2, val2 = 'another', 'something else'
- key3, val3 = 'onemore', 'foo'
- test_dict = {
- key1: val1,
- key2: val2,
- key3: val3,
- }
- tuple_key = multistore_file._dict_to_tuple_key(test_dict)
-
- # the resulting key should be naturally sorted
- expected_output = (
- (key2, val2),
- (key3, val3),
- (key1, val1),
- )
- self.assertTupleEqual(expected_output, tuple_key)
- # check we get the original dictionary back
- self.assertDictEqual(test_dict, dict(tuple_key))
-
-
-class MultistoreFileTests(unittest2.TestCase):
-
- def tearDown(self):
- try:
- os.unlink(FILENAME)
- except OSError:
- pass
-
- def setUp(self):
- try:
- os.unlink(FILENAME)
- except OSError:
- pass
-
- def _create_test_credentials(self, client_id='some_client_id',
- expiration=None):
- access_token = 'foo'
- client_secret = 'cOuDdkfjxxnv+'
- refresh_token = '1/0/a.df219fjls0'
- token_expiry = expiration or datetime.datetime.utcnow()
- token_uri = 'https://www.google.com/accounts/o8/oauth2/token'
- user_agent = 'refresh_checker/1.0'
-
- credentials = client.OAuth2Credentials(
- access_token, client_id, client_secret,
- refresh_token, token_expiry, token_uri,
- user_agent)
- return credentials
-
- def test_lock_file_raises_ioerror(self):
- filehandle, filename = tempfile.mkstemp()
- os.close(filehandle)
-
- try:
- for error_code in (errno.EDEADLK, errno.ENOSYS, errno.ENOLCK,
- errno.EACCES):
- for error_class in (IOError, OSError):
- multistore = multistore_file._MultiStore(filename)
- multistore._file = _MockLockedFile(
- filename, error_class, error_code)
- # Should not raise though the underlying file class did.
- multistore._lock()
- self.assertTrue(multistore._file.open_and_lock_called)
- finally:
- os.unlink(filename)
-
- def test_lock_file_raise_unexpected_error(self):
- filehandle, filename = tempfile.mkstemp()
- os.close(filehandle)
-
- try:
- multistore = multistore_file._MultiStore(filename)
- multistore._file = _MockLockedFile(filename, IOError, errno.EBUSY)
- with self.assertRaises(IOError):
- multistore._lock()
- self.assertTrue(multistore._file.open_and_lock_called)
- finally:
- os.unlink(filename)
-
- def test_read_only_file_fail_lock(self):
- credentials = self._create_test_credentials()
-
- open(FILENAME, 'a+b').close()
- os.chmod(FILENAME, 0o400)
-
- store = multistore_file.get_credential_storage(
- FILENAME,
- credentials.client_id,
- credentials.user_agent,
- ['some-scope', 'some-other-scope'])
-
- store.put(credentials)
- if os.name == 'posix': # pragma: NO COVER
- self.assertTrue(store._multistore._read_only)
- os.chmod(FILENAME, 0o600)
-
- def test_read_only_file_fail_lock_no_warning(self):
- open(FILENAME, 'a+b').close()
- os.chmod(FILENAME, 0o400)
-
- multistore = multistore_file._MultiStore(FILENAME)
-
- with mock.patch.object(multistore_file.logger, 'warn') as mock_warn:
- multistore._warn_on_readonly = False
- multistore._lock()
- self.assertFalse(mock_warn.called)
-
- def test_lock_skip_refresh(self):
- with open(FILENAME, 'w') as f:
- f.write('123')
- os.chmod(FILENAME, 0o400)
-
- multistore = multistore_file._MultiStore(FILENAME)
-
- refresh_patch = mock.patch.object(
- multistore, '_refresh_data_cache')
-
- with refresh_patch as refresh_mock:
- multistore._data = {}
- multistore._lock()
- self.assertFalse(refresh_mock.called)
-
- @unittest2.skipIf(not hasattr(os, 'symlink'), 'No symlink available')
- def test_multistore_no_symbolic_link_files(self):
- SYMFILENAME = FILENAME + 'sym'
- os.symlink(FILENAME, SYMFILENAME)
- store = multistore_file.get_credential_storage(
- SYMFILENAME,
- 'some_client_id',
- 'user-agent/1.0',
- ['some-scope', 'some-other-scope'])
- try:
- with self.assertRaises(
- locked_file.CredentialsFileSymbolicLinkError):
- store.get()
- finally:
- os.unlink(SYMFILENAME)
-
- def test_multistore_non_existent_file(self):
- store = multistore_file.get_credential_storage(
- FILENAME,
- 'some_client_id',
- 'user-agent/1.0',
- ['some-scope', 'some-other-scope'])
-
- credentials = store.get()
- self.assertEquals(None, credentials)
-
- def test_multistore_file(self):
- credentials = self._create_test_credentials()
-
- store = multistore_file.get_credential_storage(
- FILENAME,
- credentials.client_id,
- credentials.user_agent,
- ['some-scope', 'some-other-scope'])
-
- # Save credentials
- store.put(credentials)
- credentials = store.get()
-
- self.assertNotEquals(None, credentials)
- self.assertEquals('foo', credentials.access_token)
-
- # Delete credentials
- store.delete()
- credentials = store.get()
-
- self.assertEquals(None, credentials)
-
- if os.name == 'posix': # pragma: NO COVER
- self.assertEquals(
- 0o600, stat.S_IMODE(os.stat(FILENAME).st_mode))
-
- def test_multistore_file_custom_key(self):
- credentials = self._create_test_credentials()
-
- custom_key = {'myapp': 'testing', 'clientid': 'some client'}
- store = multistore_file.get_credential_storage_custom_key(
- FILENAME, custom_key)
-
- store.put(credentials)
- stored_credentials = store.get()
-
- self.assertNotEquals(None, stored_credentials)
- self.assertEqual(credentials.access_token,
- stored_credentials.access_token)
-
- store.delete()
- stored_credentials = store.get()
-
- self.assertEquals(None, stored_credentials)
-
- def test_multistore_file_custom_string_key(self):
- credentials = self._create_test_credentials()
-
- # store with string key
- store = multistore_file.get_credential_storage_custom_string_key(
- FILENAME, 'mykey')
-
- store.put(credentials)
- stored_credentials = store.get()
-
- self.assertNotEquals(None, stored_credentials)
- self.assertEqual(credentials.access_token,
- stored_credentials.access_token)
-
- # try retrieving with a dictionary
- multistore_file.get_credential_storage_custom_string_key(
- FILENAME, {'key': 'mykey'})
- stored_credentials = store.get()
- self.assertNotEquals(None, stored_credentials)
- self.assertEqual(credentials.access_token,
- stored_credentials.access_token)
-
- store.delete()
- stored_credentials = store.get()
-
- self.assertEquals(None, stored_credentials)
-
- def test_multistore_file_backwards_compatibility(self):
- credentials = self._create_test_credentials()
- scopes = ['scope1', 'scope2']
-
- # store the credentials using the legacy key method
- store = multistore_file.get_credential_storage(
- FILENAME, 'client_id', 'user_agent', scopes)
- store.put(credentials)
-
- # retrieve the credentials using a custom key that matches the
- # legacy key
- key = {'clientId': 'client_id', 'userAgent': 'user_agent',
- 'scope': util.scopes_to_string(scopes)}
- store = multistore_file.get_credential_storage_custom_key(
- FILENAME, key)
- stored_credentials = store.get()
-
- self.assertEqual(credentials.access_token,
- stored_credentials.access_token)
-
- def test_multistore_file_get_all_keys(self):
- # start with no keys
- keys = multistore_file.get_all_credential_keys(FILENAME)
- self.assertEquals([], keys)
-
- # store credentials
- credentials = self._create_test_credentials(client_id='client1')
- custom_key = {'myapp': 'testing', 'clientid': 'client1'}
- store1 = multistore_file.get_credential_storage_custom_key(
- FILENAME, custom_key)
- store1.put(credentials)
-
- keys = multistore_file.get_all_credential_keys(FILENAME)
- self.assertEquals([custom_key], keys)
-
- # store more credentials
- credentials = self._create_test_credentials(client_id='client2')
- string_key = 'string_key'
- store2 = multistore_file.get_credential_storage_custom_string_key(
- FILENAME, string_key)
- store2.put(credentials)
-
- keys = multistore_file.get_all_credential_keys(FILENAME)
- self.assertEquals(2, len(keys))
- self.assertTrue(custom_key in keys)
- self.assertTrue({'key': string_key} in keys)
-
- # back to no keys
- store1.delete()
- store2.delete()
- keys = multistore_file.get_all_credential_keys(FILENAME)
- self.assertEquals([], keys)
-
- def _refresh_data_cache_helper(self):
- multistore = multistore_file._MultiStore(FILENAME)
- json_patch = mock.patch.object(multistore, '_locked_json_read')
-
- return multistore, json_patch
-
- def test__refresh_data_cache_bad_json(self):
- multistore, json_patch = self._refresh_data_cache_helper()
-
- with json_patch as json_mock:
- json_mock.side_effect = ValueError('')
- multistore._refresh_data_cache()
- self.assertTrue(json_mock.called)
- self.assertEqual(multistore._data, {})
-
- def test__refresh_data_cache_bad_version(self):
- multistore, json_patch = self._refresh_data_cache_helper()
-
- with json_patch as json_mock:
- json_mock.return_value = {}
- multistore._refresh_data_cache()
- self.assertTrue(json_mock.called)
- self.assertEqual(multistore._data, {})
-
- def test__refresh_data_cache_newer_version(self):
- multistore, json_patch = self._refresh_data_cache_helper()
-
- with json_patch as json_mock:
- json_mock.return_value = {'file_version': 5}
- with self.assertRaises(multistore_file.NewerCredentialStoreError):
- multistore._refresh_data_cache()
- self.assertTrue(json_mock.called)
-
- def test__refresh_data_cache_bad_credentials(self):
- multistore, json_patch = self._refresh_data_cache_helper()
-
- with json_patch as json_mock:
- json_mock.return_value = {
- 'file_version': 1,
- 'data': [
- {'lol': 'this is a bad credential object.'}
- ]}
- multistore._refresh_data_cache()
- self.assertTrue(json_mock.called)
- self.assertEqual(multistore._data, {})
-
- def test__delete_credential_nonexistent(self):
- multistore = multistore_file._MultiStore(FILENAME)
-
- with mock.patch.object(multistore, '_write') as write_mock:
- multistore._data = {}
- multistore._delete_credential('nonexistent_key')
- self.assertTrue(write_mock.called)
diff --git a/tests/contrib/test_sqlalchemy.py b/tests/contrib/test_sqlalchemy.py
index 421f516..068aa92 100644
--- a/tests/contrib/test_sqlalchemy.py
+++ b/tests/contrib/test_sqlalchemy.py
@@ -13,11 +13,12 @@
# limitations under the License.
import datetime
+import unittest
+import mock
import sqlalchemy
import sqlalchemy.ext.declarative
import sqlalchemy.orm
-import unittest2
import oauth2client
import oauth2client.client
@@ -36,7 +37,7 @@ class DummyModel(Base):
oauth2client.contrib.sqlalchemy.CredentialsType)
-class TestSQLAlchemyStorage(unittest2.TestCase):
+class TestSQLAlchemyStorage(unittest.TestCase):
def setUp(self):
engine = sqlalchemy.create_engine('sqlite://')
Base.metadata.create_all(engine)
@@ -66,7 +67,8 @@ class TestSQLAlchemyStorage(unittest2.TestCase):
self.assertEqual(result.token_uri, self.credentials.token_uri)
self.assertEqual(result.user_agent, self.credentials.user_agent)
- def test_get(self):
+ @mock.patch('oauth2client.client.OAuth2Credentials.set_store')
+ def test_get(self, set_store):
session = self.session()
credentials_storage = oauth2client.contrib.sqlalchemy.Storage(
session=session,
@@ -75,7 +77,21 @@ class TestSQLAlchemyStorage(unittest2.TestCase):
key_value=1,
property_name='credentials',
)
+ # No credentials stored
self.assertIsNone(credentials_storage.get())
+
+ # Invalid credentials stored
+ session.add(DummyModel(
+ key=1,
+ credentials=oauth2client.client.Credentials(),
+ ))
+ session.commit()
+ bad_credentials = credentials_storage.get()
+ self.assertIsInstance(bad_credentials, oauth2client.client.Credentials)
+ set_store.assert_not_called()
+
+ # Valid credentials stored
+ session.query(DummyModel).filter_by(key=1).delete()
session.add(DummyModel(
key=1,
credentials=self.credentials,
@@ -83,16 +99,20 @@ class TestSQLAlchemyStorage(unittest2.TestCase):
session.commit()
self.compare_credentials(credentials_storage.get())
+ set_store.assert_called_with(credentials_storage)
def test_put(self):
session = self.session()
- oauth2client.contrib.sqlalchemy.Storage(
+ storage = oauth2client.contrib.sqlalchemy.Storage(
session=session,
model_class=DummyModel,
key_name='key',
key_value=1,
property_name='credentials',
- ).put(self.credentials)
+ )
+ # Store invalid credentials first to verify overwriting
+ storage.put(oauth2client.client.Credentials())
+ storage.put(self.credentials)
session.commit()
entity = session.query(DummyModel).filter_by(key=1).first()
diff --git a/tests/contrib/test_xsrfutil.py b/tests/contrib/test_xsrfutil.py
index 64b842f..3115827 100644
--- a/tests/contrib/test_xsrfutil.py
+++ b/tests/contrib/test_xsrfutil.py
@@ -15,9 +15,9 @@
"""Tests for oauth2client.contrib.xsrfutil."""
import base64
+import unittest
import mock
-import unittest2
from oauth2client import _helpers
from oauth2client.contrib import xsrfutil
@@ -34,10 +34,7 @@ TEST_EXTRA_INFO_1 = b'extra_info_1'
TEST_EXTRA_INFO_2 = b'more_extra_info'
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
-
-class Test_generate_token(unittest2.TestCase):
+class Test_generate_token(unittest.TestCase):
def test_bad_positional(self):
# Need 2 positional arguments.
@@ -49,10 +46,10 @@ class Test_generate_token(unittest2.TestCase):
def test_it(self):
digest = b'foobar'
- digester = mock.MagicMock()
- digester.digest = mock.MagicMock(name='digest', return_value=digest)
+ digester = mock.Mock()
+ digester.digest = mock.Mock(name='digest', return_value=digest)
with mock.patch('oauth2client.contrib.xsrfutil.hmac') as hmac:
- hmac.new = mock.MagicMock(name='new', return_value=digester)
+ hmac.new = mock.Mock(name='new', return_value=digester)
token = xsrfutil.generate_token(TEST_KEY,
TEST_USER_ID_1,
action_id=TEST_ACTION_ID_1,
@@ -78,13 +75,13 @@ class Test_generate_token(unittest2.TestCase):
def test_with_system_time(self):
digest = b'foobar'
curr_time = 1440449755.74
- digester = mock.MagicMock()
- digester.digest = mock.MagicMock(name='digest', return_value=digest)
+ digester = mock.Mock()
+ digester.digest = mock.Mock(name='digest', return_value=digest)
with mock.patch('oauth2client.contrib.xsrfutil.hmac') as hmac:
- hmac.new = mock.MagicMock(name='new', return_value=digester)
+ hmac.new = mock.Mock(name='new', return_value=digester)
with mock.patch('oauth2client.contrib.xsrfutil.time') as time:
- time.time = mock.MagicMock(name='time', return_value=curr_time)
+ time.time = mock.Mock(name='time', return_value=curr_time)
# when= is omitted
token = xsrfutil.generate_token(TEST_KEY,
TEST_USER_ID_1,
@@ -111,7 +108,7 @@ class Test_generate_token(unittest2.TestCase):
self.assertEqual(token, expected_token)
-class Test_validate_token(unittest2.TestCase):
+class Test_validate_token(unittest.TestCase):
def test_bad_positional(self):
# Need 3 positional arguments.
@@ -142,7 +139,7 @@ class Test_validate_token(unittest2.TestCase):
key = user_id = None
token = base64.b64encode(_helpers._to_bytes(str(token_time)))
with mock.patch('oauth2client.contrib.xsrfutil.time') as time:
- time.time = mock.MagicMock(name='time', return_value=curr_time)
+ time.time = mock.Mock(name='time', return_value=curr_time)
self.assertFalse(xsrfutil.validate_token(key, token, user_id))
time.time.assert_called_once_with()
@@ -218,7 +215,7 @@ class Test_validate_token(unittest2.TestCase):
when=token_time)
-class XsrfUtilTests(unittest2.TestCase):
+class XsrfUtilTests(unittest.TestCase):
"""Test xsrfutil functions."""
def testGenerateAndValidateToken(self):
diff --git a/tests/data/client_secrets.json b/tests/data/client_secrets.json
index 5356103..81079e6 100644
--- a/tests/data/client_secrets.json
+++ b/tests/data/client_secrets.json
@@ -4,7 +4,7 @@
"client_secret": "foo_client_secret",
"redirect_uris": [],
"auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
- "token_uri": "https://www.googleapis.com/oauth2/v4/token",
- "revoke_uri": "https://accounts.google.com/o/oauth2/revoke"
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "revoke_uri": "https://oauth2.googleapis.com/revoke"
}
}
diff --git a/tests/data/unfilled_client_secrets.json b/tests/data/unfilled_client_secrets.json
index a85ca01..8b5d55e 100644
--- a/tests/data/unfilled_client_secrets.json
+++ b/tests/data/unfilled_client_secrets.json
@@ -4,6 +4,6 @@
"client_secret": "[[INSERT CLIENT SECRET HERE]]",
"redirect_uris": [],
"auth_uri": "https://accounts.google.com/o/oauth2/v2/auth",
- "token_uri": "https://www.googleapis.com/oauth2/v4/token"
+ "token_uri": "https://oauth2.googleapis.com/token"
}
}
diff --git a/tests/data/user-key.json.enc b/tests/data/user-key.json.enc
index 03e1bc6..5f53207 100644
--- a/tests/data/user-key.json.enc
+++ b/tests/data/user-key.json.enc
Binary files differ
diff --git a/tests/http_mock.py b/tests/http_mock.py
index 6053299..a29024f 100644
--- a/tests/http_mock.py
+++ b/tests/http_mock.py
@@ -12,31 +12,41 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Copy of googleapiclient.http's mock functionality."""
+"""HTTP helpers mock functionality."""
-import httplib2
-# TODO(craigcitro): Find a cleaner way to share this code with googleapiclient.
+from six.moves import http_client
+
+
+class ResponseMock(dict):
+ """Mock HTTP response"""
+
+ def __init__(self, vals=None):
+ if vals is None:
+ vals = {}
+ self.update(vals)
+ self.status = int(self.get('status', http_client.OK))
class HttpMock(object):
- """Mock of httplib2.Http"""
+ """Mock of HTTP object."""
- def __init__(self, headers=None):
+ def __init__(self, headers=None, data=None):
"""HttpMock constructor.
Args:
headers: dict, header to return with response
"""
if headers is None:
- headers = {'status': '200'}
- self.data = None
+ headers = {'status': http_client.OK}
+ self.data = data
self.response_headers = headers
self.headers = None
self.uri = None
self.method = None
self.body = None
self.headers = None
+ self.requests = 0
def request(self, uri,
method='GET',
@@ -48,22 +58,24 @@ class HttpMock(object):
self.method = method
self.body = body
self.headers = headers
- return httplib2.Response(self.response_headers), self.data
+ self.redirections = redirections
+ self.requests += 1
+ return ResponseMock(self.response_headers), self.data
class HttpMockSequence(object):
- """Mock of httplib2.Http
+ """Mock of HTTP object with multiple return values.
Mocks a sequence of calls to request returning different responses for each
call. Create an instance initialized with the desired response headers
- and content and then use as if an httplib2.Http instance::
+ and content and then use as if an HttpMock instance::
http = HttpMockSequence([
({'status': '401'}, b''),
({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'),
({'status': '200'}, 'echo_request_headers'),
])
- resp, content = http.request("http://examples.com")
+ resp, content = http.request('http://examples.com')
There are special values you can pass in for content to trigger
behavours that are helpful in testing.
@@ -80,7 +92,6 @@ class HttpMockSequence(object):
iterable: iterable, a sequence of pairs of (headers, body)
"""
self._iterable = iterable
- self.follow_redirects = True
self.requests = []
def request(self, uri,
@@ -90,7 +101,12 @@ class HttpMockSequence(object):
redirections=1,
connection_type=None):
resp, content = self._iterable.pop(0)
- self.requests.append({'uri': uri, 'body': body, 'headers': headers})
+ self.requests.append({
+ 'method': method,
+ 'uri': uri,
+ 'body': body,
+ 'headers': headers,
+ })
# Read any underlying stream before sending the request.
body_stream_content = (body.read()
if getattr(body, 'read', None) else None)
@@ -99,7 +115,7 @@ class HttpMockSequence(object):
elif content == 'echo_request_body':
content = (body
if body_stream_content is None else body_stream_content)
- return httplib2.Response(resp), content
+ return ResponseMock(resp), content
class CacheMock(object):
diff --git a/tests/test__helpers.py b/tests/test__helpers.py
index cd54186..00cd38a 100644
--- a/tests/test__helpers.py
+++ b/tests/test__helpers.py
@@ -11,14 +11,133 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+
"""Unit tests for oauth2client._helpers."""
-import unittest2
+import unittest
+
+import mock
from oauth2client import _helpers
+from tests import test_client
+
+
+class PositionalTests(unittest.TestCase):
+
+ def test_usage(self):
+ _helpers.positional_parameters_enforcement = (
+ _helpers.POSITIONAL_EXCEPTION)
+
+ # 1 positional arg, 1 keyword-only arg.
+ @_helpers.positional(1)
+ def function(pos, kwonly=None):
+ return True
+
+ self.assertTrue(function(1))
+ self.assertTrue(function(1, kwonly=2))
+ with self.assertRaises(TypeError):
+ function(1, 2)
+
+ # No positional, but a required keyword arg.
+ @_helpers.positional(0)
+ def function2(required_kw):
+ return True
+
+ self.assertTrue(function2(required_kw=1))
+ with self.assertRaises(TypeError):
+ function2(1)
+
+ # Unspecified positional, should automatically figure out 1 positional
+ # 1 keyword-only (same as first case above).
+ @_helpers.positional
+ def function3(pos, kwonly=None):
+ return True
+
+ self.assertTrue(function3(1))
+ self.assertTrue(function3(1, kwonly=2))
+ with self.assertRaises(TypeError):
+ function3(1, 2)
+
+ @mock.patch('oauth2client._helpers.logger')
+ def test_enforcement_warning(self, mock_logger):
+ _helpers.positional_parameters_enforcement = (
+ _helpers.POSITIONAL_WARNING)
+
+ @_helpers.positional(1)
+ def function(pos, kwonly=None):
+ return True
+
+ self.assertTrue(function(1, 2))
+ self.assertTrue(mock_logger.warning.called)
+
+ @mock.patch('oauth2client._helpers.logger')
+ def test_enforcement_ignore(self, mock_logger):
+ _helpers.positional_parameters_enforcement = _helpers.POSITIONAL_IGNORE
+
+ @_helpers.positional(1)
+ def function(pos, kwonly=None):
+ return True
+
+ self.assertTrue(function(1, 2))
+ self.assertFalse(mock_logger.warning.called)
+
+
+class ScopeToStringTests(unittest.TestCase):
+
+ def test_iterables(self):
+ cases = [
+ ('', ''),
+ ('', ()),
+ ('', []),
+ ('', ('',)),
+ ('', ['', ]),
+ ('a', ('a',)),
+ ('b', ['b', ]),
+ ('a b', ['a', 'b']),
+ ('a b', ('a', 'b')),
+ ('a b', 'a b'),
+ ('a b', (s for s in ['a', 'b'])),
+ ]
+ for expected, case in cases:
+ self.assertEqual(expected, _helpers.scopes_to_string(case))
+
+
+class StringToScopeTests(unittest.TestCase):
+
+ def test_conversion(self):
+ cases = [
+ (['a', 'b'], ['a', 'b']),
+ ('', []),
+ ('a', ['a']),
+ ('a b c d e f', ['a', 'b', 'c', 'd', 'e', 'f']),
+ ]
+ for case, expected in cases:
+ self.assertEqual(expected, _helpers.string_to_scopes(case))
-class Test__parse_pem_key(unittest2.TestCase):
+
+class AddQueryParameterTests(unittest.TestCase):
+
+ def test__add_query_parameter(self):
+ self.assertEqual(
+ _helpers._add_query_parameter('/action', 'a', None),
+ '/action')
+ self.assertEqual(
+ _helpers._add_query_parameter('/action', 'a', 'b'),
+ '/action?a=b')
+ self.assertEqual(
+ _helpers._add_query_parameter('/action?a=b', 'a', 'c'),
+ '/action?a=c')
+ # Order is non-deterministic.
+ self.assertIn(
+ _helpers._add_query_parameter('/action?a=b', 'c', 'd'),
+ ['/action?a=b&c=d', '/action?c=d&a=b'])
+ self.assertEqual(
+ _helpers._add_query_parameter('/action', 'a', ' ='),
+ '/action?a=+%3D')
+
+
+class Test__parse_pem_key(unittest.TestCase):
def test_valid_input(self):
test_string = b'1234-----BEGIN FOO BAR BAZ'
@@ -31,7 +150,7 @@ class Test__parse_pem_key(unittest2.TestCase):
self.assertEqual(result, None)
-class Test__json_encode(unittest2.TestCase):
+class Test__json_encode(unittest.TestCase):
def test_dictionary_input(self):
# Use only a single key since dictionary hash order
@@ -46,7 +165,7 @@ class Test__json_encode(unittest2.TestCase):
self.assertEqual(result, '[42,1337]')
-class Test__to_bytes(unittest2.TestCase):
+class Test__to_bytes(unittest.TestCase):
def test_with_bytes(self):
value = b'bytes-val'
@@ -63,7 +182,7 @@ class Test__to_bytes(unittest2.TestCase):
_helpers._to_bytes(value)
-class Test__from_bytes(unittest2.TestCase):
+class Test__from_bytes(unittest.TestCase):
def test_with_unicode(self):
value = u'bytes-val'
@@ -80,10 +199,15 @@ class Test__from_bytes(unittest2.TestCase):
_helpers._from_bytes(value)
-class Test__urlsafe_b64encode(unittest2.TestCase):
+class Test__urlsafe_b64encode(unittest.TestCase):
DEADBEEF_ENCODED = b'ZGVhZGJlZWY'
+ def test_valid_input_str(self):
+ test_string = 'deadbeef'
+ result = _helpers._urlsafe_b64encode(test_string)
+ self.assertEqual(result, self.DEADBEEF_ENCODED)
+
def test_valid_input_bytes(self):
test_string = b'deadbeef'
result = _helpers._urlsafe_b64encode(test_string)
@@ -95,20 +219,66 @@ class Test__urlsafe_b64encode(unittest2.TestCase):
self.assertEqual(result, self.DEADBEEF_ENCODED)
-class Test__urlsafe_b64decode(unittest2.TestCase):
+class Test__urlsafe_b64decode(unittest.TestCase):
+
+ DEADBEEF_DECODED = b'deadbeef'
+
+ def test_valid_input_str(self):
+ test_string = 'ZGVhZGJlZWY'
+ result = _helpers._urlsafe_b64decode(test_string)
+ self.assertEqual(result, self.DEADBEEF_DECODED)
def test_valid_input_bytes(self):
test_string = b'ZGVhZGJlZWY'
result = _helpers._urlsafe_b64decode(test_string)
- self.assertEqual(result, b'deadbeef')
+ self.assertEqual(result, self.DEADBEEF_DECODED)
def test_valid_input_unicode(self):
- test_string = b'ZGVhZGJlZWY'
+ test_string = u'ZGVhZGJlZWY'
result = _helpers._urlsafe_b64decode(test_string)
- self.assertEqual(result, b'deadbeef')
+ self.assertEqual(result, self.DEADBEEF_DECODED)
def test_bad_input(self):
import binascii
bad_string = b'+'
with self.assertRaises((TypeError, binascii.Error)):
_helpers._urlsafe_b64decode(bad_string)
+
+
+class Test_update_query_params(unittest.TestCase):
+
+ def test_update_query_params_no_params(self):
+ uri = 'http://www.google.com'
+ updated = _helpers.update_query_params(uri, {'a': 'b'})
+ self.assertEqual(updated, uri + '?a=b')
+
+ def test_update_query_params_existing_params(self):
+ uri = 'http://www.google.com?x=y'
+ updated = _helpers.update_query_params(uri, {'a': 'b', 'c': 'd&'})
+ hardcoded_update = uri + '&a=b&c=d%26'
+ test_client.assertUrisEqual(self, updated, hardcoded_update)
+
+ def test_update_query_params_replace_param(self):
+ base_uri = 'http://www.google.com'
+ uri = base_uri + '?x=a'
+ updated = _helpers.update_query_params(uri, {'x': 'b', 'y': 'c'})
+ hardcoded_update = base_uri + '?x=b&y=c'
+ test_client.assertUrisEqual(self, updated, hardcoded_update)
+
+ def test_update_query_params_repeated_params(self):
+ uri = 'http://www.google.com?x=a&x=b'
+ with self.assertRaises(ValueError):
+ _helpers.update_query_params(uri, {'a': 'c'})
+
+
+class Test_parse_unique_urlencoded(unittest.TestCase):
+
+ def test_without_repeats(self):
+ content = 'a=b&c=d'
+ result = _helpers.parse_unique_urlencoded(content)
+ self.assertEqual(result, {'a': 'b', 'c': 'd'})
+
+ def test_with_repeats(self):
+ content = 'a=b&a=d'
+ with self.assertRaises(ValueError):
+ _helpers.parse_unique_urlencoded(content)
diff --git a/tests/test__pkce.py b/tests/test__pkce.py
new file mode 100644
index 0000000..9f66560
--- /dev/null
+++ b/tests/test__pkce.py
@@ -0,0 +1,54 @@
+# Copyright 2016 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+
+import mock
+
+from oauth2client import _pkce
+
+
+class PKCETests(unittest.TestCase):
+
+ @mock.patch('oauth2client._pkce.os.urandom')
+ def test_verifier(self, fake_urandom):
+ canned_randomness = (
+ b'\x98\x10D7\xf3\xb7\xaa\xfc\xdd\xd3M\xe2'
+ b'\xa3,\x06\xa0\xb0\xa9\xb4\x8f\xcb\xd0'
+ b'\xf5\x86N2p\x8c]!W\x9a\xed54\x99\x9d'
+ b'\x8dv\\\xa7/\x81\xf3J\x98\xc3\x90\xee'
+ b'\xb0\x8c\xb7Zc#\x05M0O\x08\xda\t\x1f\x07'
+ )
+ fake_urandom.return_value = canned_randomness
+ expected = (
+ b'mBBEN_O3qvzd003ioywGoLCptI_L0PWGTjJwjF0hV5rt'
+ b'NTSZnY12XKcvgfNKmMOQ7rCMt1pjIwVNME8I2gkfBw'
+ )
+ result = _pkce.code_verifier()
+ self.assertEqual(result, expected)
+
+ def test_verifier_too_long(self):
+ with self.assertRaises(ValueError) as caught:
+ _pkce.code_verifier(97)
+ self.assertIn("too long", str(caught.exception))
+
+ def test_verifier_too_short(self):
+ with self.assertRaises(ValueError) as caught:
+ _pkce.code_verifier(30)
+ self.assertIn("too short", str(caught.exception))
+
+ def test_challenge(self):
+ result = _pkce.code_challenge(b'SOME_VERIFIER')
+ expected = b'6xJCQsjTtS3zjUwd8_ZqH0SyviGHnp5PsHXWKOCqDuI'
+ self.assertEqual(result, expected)
diff --git a/tests/test__pure_python_crypt.py b/tests/test__pure_python_crypt.py
index 3c2962a..e9844b9 100644
--- a/tests/test__pure_python_crypt.py
+++ b/tests/test__pure_python_crypt.py
@@ -15,19 +15,19 @@
"""Unit tests for oauth2client._pure_python_crypt."""
import os
+import unittest
import mock
from pyasn1_modules import pem
import rsa
import six
-import unittest2
from oauth2client import _helpers
from oauth2client import _pure_python_crypt
from oauth2client import crypt
-class TestRsaVerifier(unittest2.TestCase):
+class TestRsaVerifier(unittest.TestCase):
PUBLIC_KEY_FILENAME = os.path.join(os.path.dirname(__file__),
'data', 'privatekey.pub')
@@ -112,7 +112,7 @@ class TestRsaVerifier(unittest2.TestCase):
load_pem.assert_called_once_with(cert_bytes, 'CERTIFICATE')
-class TestRsaSigner(unittest2.TestCase):
+class TestRsaSigner(unittest.TestCase):
PKCS1_KEY_FILENAME = os.path.join(os.path.dirname(__file__),
'data', 'privatekey.pem')
diff --git a/tests/test__pycrypto_crypt.py b/tests/test__pycrypto_crypt.py
index 2f45291..2ca18ec 100644
--- a/tests/test__pycrypto_crypt.py
+++ b/tests/test__pycrypto_crypt.py
@@ -14,13 +14,12 @@
"""Unit tests for oauth2client._pycrypto_crypt."""
import os
-
-import unittest2
+import unittest
from oauth2client import crypt
-class TestPyCryptoVerifier(unittest2.TestCase):
+class TestPyCryptoVerifier(unittest.TestCase):
PUBLIC_CERT_FILENAME = os.path.join(os.path.dirname(__file__),
'data', 'public_cert.pem')
@@ -65,7 +64,7 @@ class TestPyCryptoVerifier(unittest2.TestCase):
self.assertIsInstance(verifier, crypt.PyCryptoVerifier)
-class TestPyCryptoSigner(unittest2.TestCase):
+class TestPyCryptoSigner(unittest.TestCase):
def test_from_string_bad_key(self):
key_bytes = 'definitely-not-pem-format'
diff --git a/tests/test_client.py b/tests/test_client.py
index db75603..18b7df4 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -12,10 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Oauth2client tests
-
-Unit tests for oauth2client.
-"""
+"""Unit tests for oauth2client.client."""
import base64
import contextlib
@@ -26,31 +23,25 @@ import os
import socket
import sys
import tempfile
+import unittest
-import httplib2
import mock
import six
from six.moves import http_client
from six.moves import urllib
-import unittest2
import oauth2client
from oauth2client import _helpers
from oauth2client import client
from oauth2client import clientsecrets
from oauth2client import service_account
-from oauth2client import util
-from .http_mock import CacheMock
-from .http_mock import HttpMock
-from .http_mock import HttpMockSequence
+from oauth2client import transport
+from tests import http_mock
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
-# TODO(craigcitro): This is duplicated from
-# googleapiclient.test_discovery; consolidate these definitions.
def assertUrisEqual(testcase, expected, actual):
"""Test that URIs are the same, up to reordering of query parameters."""
expected = urllib.parse.urlparse(expected)
@@ -77,7 +68,7 @@ def load_and_cache(existing_file, fakename, cache_mock):
cache_mock.cache[fakename] = {client_type: client_info}
-class CredentialsTests(unittest2.TestCase):
+class CredentialsTests(unittest.TestCase):
def test_to_from_json(self):
credentials = client.Credentials()
@@ -221,7 +212,7 @@ class CredentialsTests(unittest2.TestCase):
self.assertEqual(credentials.__dict__, {})
-class TestStorage(unittest2.TestCase):
+class TestStorage(unittest.TestCase):
def test_locked_get_abstract(self):
storage = client.Storage()
@@ -256,7 +247,7 @@ def mock_module_import(module):
del sys.modules[entry]
-class GoogleCredentialsTests(unittest2.TestCase):
+class GoogleCredentialsTests(unittest.TestCase):
def setUp(self):
self.os_name = os.name
@@ -364,67 +355,41 @@ class GoogleCredentialsTests(unittest2.TestCase):
# is cached.
self.assertTrue(client._in_gae_environment())
- def _environment_check_gce_helper(self, status_ok=True, socket_error=False,
+ def _environment_check_gce_helper(self, status_ok=True,
server_software=''):
- response = mock.MagicMock()
if status_ok:
- response.status = http_client.OK
- response.getheader = mock.MagicMock(
- name='getheader',
- return_value=client._DESIRED_METADATA_FLAVOR)
+ headers = {'status': http_client.OK}
+ headers.update(client._GCE_HEADERS)
else:
- response.status = http_client.NOT_FOUND
-
- connection = mock.MagicMock()
- connection.getresponse = mock.MagicMock(name='getresponse',
- return_value=response)
- if socket_error:
- connection.getresponse.side_effect = socket.error()
+ headers = {'status': http_client.NOT_FOUND}
+ http = http_mock.HttpMock(headers=headers)
with mock.patch('oauth2client.client.os') as os_module:
os_module.environ = {client._SERVER_SOFTWARE: server_software}
- with mock.patch('oauth2client.client.six') as six_module:
- http_client_module = six_module.moves.http_client
- http_client_module.HTTPConnection = mock.MagicMock(
- name='HTTPConnection', return_value=connection)
-
+ with mock.patch('oauth2client.transport.get_http_object',
+ return_value=http) as new_http:
if server_software == '':
self.assertFalse(client._in_gae_environment())
else:
self.assertTrue(client._in_gae_environment())
- if status_ok and not socket_error and server_software == '':
+ if status_ok and server_software == '':
self.assertTrue(client._in_gce_environment())
else:
self.assertFalse(client._in_gce_environment())
+ # Verify mocks.
if server_software == '':
- http_client_module.HTTPConnection.assert_called_once_with(
- client._GCE_METADATA_HOST,
+ new_http.assert_called_once_with(
timeout=client.GCE_METADATA_TIMEOUT)
- connection.getresponse.assert_called_once_with()
- # Remaining calls are not "getresponse"
- headers = {
- client._METADATA_FLAVOR_HEADER: (
- client._DESIRED_METADATA_FLAVOR),
- }
- self.assertEqual(connection.method_calls, [
- mock.call.request('GET', '/',
- headers=headers),
- mock.call.close(),
- ])
- self.assertEqual(response.method_calls, [])
- if status_ok and not socket_error:
- response.getheader.assert_called_once_with(
- client._METADATA_FLAVOR_HEADER)
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, client._GCE_METADATA_URI)
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertEqual(http.headers, client._GCE_HEADERS)
else:
- self.assertEqual(
- http_client_module.HTTPConnection.mock_calls, [])
- self.assertEqual(connection.getresponse.mock_calls, [])
- # Remaining calls are not "getresponse"
- self.assertEqual(connection.method_calls, [])
- self.assertEqual(response.method_calls, [])
- self.assertEqual(response.getheader.mock_calls, [])
+ new_http.assert_not_called()
+ self.assertEqual(http.requests, 0)
def test_environment_check_gce_production(self):
self._environment_check_gce_helper(status_ok=True)
@@ -433,8 +398,21 @@ class GoogleCredentialsTests(unittest2.TestCase):
with mock_module_import('google.appengine'):
self._environment_check_gce_helper(status_ok=True)
- def test_environment_check_gce_timeout(self):
- self._environment_check_gce_helper(socket_error=True)
+ @mock.patch('oauth2client.client.os.environ',
+ new={client._SERVER_SOFTWARE: ''})
+ @mock.patch('oauth2client.transport.get_http_object',
+ return_value=object())
+ @mock.patch('oauth2client.transport.request',
+ side_effect=socket.timeout())
+ def test_environment_check_gce_timeout(self, mock_request, new_http):
+ self.assertFalse(client._in_gae_environment())
+ self.assertFalse(client._in_gce_environment())
+
+ # Verify mocks.
+ new_http.assert_called_once_with(timeout=client.GCE_METADATA_TIMEOUT)
+ mock_request.assert_called_once_with(
+ new_http.return_value, client._GCE_METADATA_URI,
+ headers=client._GCE_HEADERS)
def test_environ_check_gae_module_unknown(self):
with mock_module_import('google.appengine'):
@@ -709,8 +687,9 @@ class GoogleCredentialsTests(unittest2.TestCase):
# Make sure the well-known file actually doesn't exist.
self.assertTrue(os.path.exists(get_well_known.return_value))
- method_name = \
- 'oauth2client.client._get_application_default_credential_from_file'
+ method_name = (
+ 'oauth2client.client.'
+ '_get_application_default_credential_from_file')
result_creds = object()
with mock.patch(method_name,
return_value=result_creds) as get_from_file:
@@ -840,8 +819,8 @@ class DummyDeleteStorage(client.Storage):
self.delete_called = True
-def _token_revoke_test_helper(testcase, status, revoke_raise,
- valid_bool_value, token_attr):
+def _token_revoke_test_helper(testcase, revoke_raise, valid_bool_value,
+ token_attr, http_mock):
current_store = getattr(testcase.credentials, 'store', None)
dummy_store = DummyDeleteStorage()
@@ -850,17 +829,16 @@ def _token_revoke_test_helper(testcase, status, revoke_raise,
actual_do_revoke = testcase.credentials._do_revoke
testcase.token_from_revoke = None
- def do_revoke_stub(http_request, token):
+ def do_revoke_stub(http, token):
testcase.token_from_revoke = token
- return actual_do_revoke(http_request, token)
+ return actual_do_revoke(http, token)
testcase.credentials._do_revoke = do_revoke_stub
- http = HttpMock(headers={'status': status})
if revoke_raise:
testcase.assertRaises(client.TokenRevokeError,
- testcase.credentials.revoke, http)
+ testcase.credentials.revoke, http_mock)
else:
- testcase.credentials.revoke(http)
+ testcase.credentials.revoke(http_mock)
testcase.assertEqual(getattr(testcase.credentials, token_attr),
testcase.token_from_revoke)
@@ -870,7 +848,7 @@ def _token_revoke_test_helper(testcase, status, revoke_raise,
testcase.credentials.set_store(current_store)
-class BasicCredentialsTests(unittest2.TestCase):
+class BasicCredentialsTests(unittest.TestCase):
def setUp(self):
access_token = 'foo'
@@ -885,54 +863,50 @@ class BasicCredentialsTests(unittest2.TestCase):
user_agent, revoke_uri=oauth2client.GOOGLE_REVOKE_URI,
scopes='foo', token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI)
- # Provoke a failure if @util.positional is not respected.
+ # Provoke a failure if @_helpers.positional is not respected.
self.old_positional_enforcement = (
- util.positional_parameters_enforcement)
- util.positional_parameters_enforcement = (
- util.POSITIONAL_EXCEPTION)
+ _helpers.positional_parameters_enforcement)
+ _helpers.positional_parameters_enforcement = (
+ _helpers.POSITIONAL_EXCEPTION)
def tearDown(self):
- util.positional_parameters_enforcement = (
+ _helpers.positional_parameters_enforcement = (
self.old_positional_enforcement)
def test_token_refresh_success(self):
for status_code in client.REFRESH_STATUS_CODES:
token_response = {'access_token': '1/3w', 'expires_in': 3600}
- http = HttpMockSequence([
+ json_resp = json.dumps(token_response).encode('utf-8')
+ http = http_mock.HttpMockSequence([
({'status': status_code}, b''),
- ({'status': '200'}, json.dumps(token_response).encode(
- 'utf-8')),
- ({'status': '200'}, 'echo_request_headers'),
+ ({'status': http_client.OK}, json_resp),
+ ({'status': http_client.OK}, 'echo_request_headers'),
])
http = self.credentials.authorize(http)
- resp, content = http.request('http://example.com')
+ resp, content = transport.request(http, 'http://example.com')
self.assertEqual(b'Bearer 1/3w', content[b'Authorization'])
self.assertFalse(self.credentials.access_token_expired)
self.assertEqual(token_response, self.credentials.token_response)
def test_recursive_authorize(self):
- """Tests that OAuth2Credentials doesn't intro. new method constraints.
-
- Formerly, OAuth2Credentials.authorize monkeypatched the request method
- of its httplib2.Http argument with a wrapper annotated with
- @util.positional(1). Since the original method has no such annotation,
- that meant that the wrapper was violating the contract of the original
- method by adding a new requirement to it. And in fact the wrapper
- itself doesn't even respect that requirement. So before the removal of
- the annotation, this test would fail.
- """
+ # Tests that OAuth2Credentials doesn't introduce new method
+ # constraints. Formerly, OAuth2Credentials.authorize monkeypatched the
+ # request method of the passed in HTTP object with a wrapper annotated
+ # with @_helpers.positional(1). Since the original method has no such
+ # annotation, that meant that the wrapper was violating the contract of
+ # the original method by adding a new requirement to it. And in fact
+ # the wrapper itself doesn't even respect that requirement. So before
+ # the removal of the annotation, this test would fail.
token_response = {'access_token': '1/3w', 'expires_in': 3600}
encoded_response = json.dumps(token_response).encode('utf-8')
- http = HttpMockSequence([
- ({'status': '200'}, encoded_response),
- ])
+ http = http_mock.HttpMock(data=encoded_response)
http = self.credentials.authorize(http)
http = self.credentials.authorize(http)
- http.request('http://example.com')
+ transport.request(http, 'http://example.com')
def test_token_refresh_failure(self):
for status_code in client.REFRESH_STATUS_CODES:
- http = HttpMockSequence([
+ http = http_mock.HttpMockSequence([
({'status': status_code}, b''),
({'status': http_client.BAD_REQUEST},
b'{"error":"access_denied"}'),
@@ -940,36 +914,51 @@ class BasicCredentialsTests(unittest2.TestCase):
http = self.credentials.authorize(http)
with self.assertRaises(
client.HttpAccessTokenRefreshError) as exc_manager:
- http.request('http://example.com')
+ transport.request(http, 'http://example.com')
self.assertEqual(http_client.BAD_REQUEST,
exc_manager.exception.status)
self.assertTrue(self.credentials.access_token_expired)
self.assertEqual(None, self.credentials.token_response)
def test_token_revoke_success(self):
+ http = http_mock.HttpMock(headers={'status': http_client.OK})
_token_revoke_test_helper(
- self, '200', revoke_raise=False,
- valid_bool_value=True, token_attr='refresh_token')
+ self, revoke_raise=False, valid_bool_value=True,
+ token_attr='refresh_token', http_mock=http)
def test_token_revoke_failure(self):
+ http = http_mock.HttpMock(headers={'status': http_client.BAD_REQUEST})
_token_revoke_test_helper(
- self, '400', revoke_raise=True,
- valid_bool_value=False, token_attr='refresh_token')
+ self, revoke_raise=True, valid_bool_value=False,
+ token_attr='refresh_token', http_mock=http)
def test_token_revoke_fallback(self):
original_credentials = self.credentials.to_json()
self.credentials.refresh_token = None
+
+ http = http_mock.HttpMock(headers={'status': http_client.OK})
_token_revoke_test_helper(
- self, '200', revoke_raise=False,
- valid_bool_value=True, token_attr='access_token')
+ self, revoke_raise=False, valid_bool_value=True,
+ token_attr='access_token', http_mock=http)
self.credentials = self.credentials.from_json(original_credentials)
- def test_non_401_error_response(self):
- http = HttpMockSequence([
- ({'status': '400'}, b''),
+ def test_token_revoke_405(self):
+ original_credentials = self.credentials.to_json()
+ self.credentials.refresh_token = None
+
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.METHOD_NOT_ALLOWED}, b''),
+ ({'status': http_client.OK}, b''),
])
+ _token_revoke_test_helper(
+ self, revoke_raise=False, valid_bool_value=True,
+ token_attr='access_token', http_mock=http)
+ self.credentials = self.credentials.from_json(original_credentials)
+
+ def test_non_401_error_response(self):
+ http = http_mock.HttpMock(headers={'status': http_client.BAD_REQUEST})
http = self.credentials.authorize(http)
- resp, content = http.request('http://example.com')
+ resp, content = transport.request(http, 'http://example.com')
self.assertEqual(http_client.BAD_REQUEST, resp.status)
self.assertEqual(None, self.credentials.token_response)
@@ -1010,10 +999,11 @@ class BasicCredentialsTests(unittest2.TestCase):
# First, test that we correctly encode basic objects, making sure
# to include a bytes object. Note that oauth2client will normalize
# everything to bytes, no matter what python version we're in.
- http = credentials.authorize(HttpMock())
+ http = credentials.authorize(http_mock.HttpMock())
headers = {u'foo': 3, b'bar': True, 'baz': b'abc'}
cleaned_headers = {b'foo': b'3', b'bar': b'True', b'baz': b'abc'}
- http.request(u'http://example.com', method=u'GET', headers=headers)
+ transport.request(
+ http, u'http://example.com', method=u'GET', headers=headers)
for k, v in cleaned_headers.items():
self.assertTrue(k in http.headers)
self.assertEqual(v, http.headers[k])
@@ -1021,8 +1011,9 @@ class BasicCredentialsTests(unittest2.TestCase):
# Next, test that we do fail on unicode.
unicode_str = six.unichr(40960) + 'abcd'
with self.assertRaises(client.NonAsciiHeaderError):
- http.request(u'http://example.com', method=u'GET',
- headers={u'foo': unicode_str})
+ transport.request(
+ http, u'http://example.com', method=u'GET',
+ headers={u'foo': unicode_str})
def test_no_unicode_in_request_params(self):
access_token = u'foo'
@@ -1037,10 +1028,11 @@ class BasicCredentialsTests(unittest2.TestCase):
access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, revoke_uri=revoke_uri)
- http = HttpMock()
+ http = http_mock.HttpMock()
http = credentials.authorize(http)
- http.request(u'http://example.com', method=u'GET',
- headers={u'foo': u'bar'})
+ transport.request(
+ http, u'http://example.com', method=u'GET',
+ headers={u'foo': u'bar'})
for k, v in six.iteritems(http.headers):
self.assertIsInstance(k, six.binary_type)
self.assertIsInstance(v, six.binary_type)
@@ -1048,8 +1040,8 @@ class BasicCredentialsTests(unittest2.TestCase):
# Test again with unicode strings that can't simply be converted
# to ASCII.
with self.assertRaises(client.NonAsciiHeaderError):
- http.request(
- u'http://example.com', method=u'GET',
+ transport.request(
+ http, u'http://example.com', method=u'GET',
headers={u'foo': u'\N{COMET}'})
self.credentials.token_response = 'foobar'
@@ -1107,7 +1099,7 @@ class BasicCredentialsTests(unittest2.TestCase):
'access_token': token2,
'expires_in': lifetime,
}
- http = HttpMockSequence([
+ http = http_mock.HttpMockSequence([
({'status': '200'}, json.dumps(token_response_first).encode(
'utf-8')),
({'status': '200'}, json.dumps(token_response_second).encode(
@@ -1181,11 +1173,12 @@ class BasicCredentialsTests(unittest2.TestCase):
# Specify a token so we can use it in the response.
credentials.access_token = 'ya29-s3kr3t'
- with mock.patch('httplib2.Http',
- return_value=object) as http_kls:
+ with mock.patch('oauth2client.transport.get_http_object',
+ return_value=object()) as new_http:
token_info = credentials.get_access_token()
expires_in.assert_called_once_with()
- refresh_mock.assert_called_once_with(http_kls.return_value)
+ refresh_mock.assert_called_once_with(new_http.return_value)
+ new_http.assert_called_once_with()
self.assertIsInstance(token_info, client.AccessTokenInfo)
self.assertEqual(token_info.access_token,
@@ -1225,21 +1218,25 @@ class BasicCredentialsTests(unittest2.TestCase):
def _do_refresh_request_test_helper(self, response, content,
error_msg, logger, gen_body,
gen_headers, store=None):
+ token_uri = 'http://token_uri'
credentials = client.OAuth2Credentials(None, None, None, None,
- None, None, None)
+ None, token_uri, None)
credentials.store = store
- http_request = mock.Mock()
- http_request.return_value = response, content
+ http = http_mock.HttpMock(headers=response, data=content)
with self.assertRaises(
client.HttpAccessTokenRefreshError) as exc_manager:
- credentials._do_refresh_request(http_request)
+ credentials._do_refresh_request(http)
self.assertEqual(exc_manager.exception.args, (error_msg,))
self.assertEqual(exc_manager.exception.status, response.status)
- http_request.assert_called_once_with(None, body=gen_body.return_value,
- headers=gen_headers.return_value,
- method='POST')
+
+ # Verify mocks.
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, token_uri)
+ self.assertEqual(http.method, 'POST')
+ self.assertEqual(http.body, gen_body.return_value)
+ self.assertEqual(http.headers, gen_headers.return_value)
call1 = mock.call('Refreshing access_token')
failure_template = 'Failed to retrieve access token: %s'
@@ -1249,43 +1246,35 @@ class BasicCredentialsTests(unittest2.TestCase):
store.locked_put.assert_called_once_with(credentials)
def test__do_refresh_request_non_json_failure(self):
- response = httplib2.Response({
- 'status': int(http_client.BAD_REQUEST),
- })
+ response = http_mock.ResponseMock({'status': http_client.BAD_REQUEST})
content = u'Bad request'
error_msg = 'Invalid response {0}.'.format(int(response.status))
self._do_refresh_request_test_helper(response, content, error_msg)
def test__do_refresh_request_basic_failure(self):
- response = httplib2.Response({
- 'status': int(http_client.INTERNAL_SERVER_ERROR),
- })
+ response = http_mock.ResponseMock(
+ {'status': http_client.INTERNAL_SERVER_ERROR})
content = u'{}'
error_msg = 'Invalid response {0}.'.format(int(response.status))
self._do_refresh_request_test_helper(response, content, error_msg)
def test__do_refresh_request_failure_w_json_error(self):
- response = httplib2.Response({
- 'status': http_client.BAD_GATEWAY,
- })
+ response = http_mock.ResponseMock({'status': http_client.BAD_GATEWAY})
error_msg = 'Hi I am an error not a bearer'
content = json.dumps({'error': error_msg})
self._do_refresh_request_test_helper(response, content, error_msg)
def test__do_refresh_request_failure_w_json_error_and_store(self):
- response = httplib2.Response({
- 'status': http_client.BAD_GATEWAY,
- })
+ response = http_mock.ResponseMock({'status': http_client.BAD_GATEWAY})
error_msg = 'Where are we going wearer?'
content = json.dumps({'error': error_msg})
- store = mock.MagicMock()
+ store = mock.Mock()
self._do_refresh_request_test_helper(response, content, error_msg,
store=store)
def test__do_refresh_request_failure_w_json_error_and_desc(self):
- response = httplib2.Response({
- 'status': http_client.SERVICE_UNAVAILABLE,
- })
+ response = http_mock.ResponseMock(
+ {'status': http_client.SERVICE_UNAVAILABLE})
base_error = 'Ruckus'
error_desc = 'Can you describe the ruckus'
content = json.dumps({
@@ -1302,20 +1291,20 @@ class BasicCredentialsTests(unittest2.TestCase):
None, None, None, None, None, None, None,
revoke_uri=oauth2client.GOOGLE_REVOKE_URI)
credentials.store = store
- http_request = mock.Mock()
- http_request.return_value = response, content
+
+ http = http_mock.HttpMock(headers=response, data=content)
token = u's3kr3tz'
if response.status == http_client.OK:
self.assertFalse(credentials.invalid)
- self.assertIsNone(credentials._do_revoke(http_request, token))
+ self.assertIsNone(credentials._do_revoke(http, token))
self.assertTrue(credentials.invalid)
if store is not None:
store.delete.assert_called_once_with()
else:
self.assertFalse(credentials.invalid)
with self.assertRaises(client.TokenRevokeError) as exc_manager:
- credentials._do_revoke(http_request, token)
+ credentials._do_revoke(http, token)
# Make sure invalid was not flipped on.
self.assertFalse(credentials.invalid)
self.assertEqual(exc_manager.exception.args, (error_msg,))
@@ -1323,54 +1312,49 @@ class BasicCredentialsTests(unittest2.TestCase):
store.delete.assert_not_called()
revoke_uri = oauth2client.GOOGLE_REVOKE_URI + '?token=' + token
- http_request.assert_called_once_with(revoke_uri)
+
+ # Verify mocks.
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, revoke_uri)
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertIsNone(http.headers)
logger.info.assert_called_once_with('Revoking token')
def test__do_revoke_success(self):
- response = httplib2.Response({
- 'status': http_client.OK,
- })
+ response = http_mock.ResponseMock()
self._do_revoke_test_helper(response, b'', None)
def test__do_revoke_success_with_store(self):
- response = httplib2.Response({
- 'status': http_client.OK,
- })
- store = mock.MagicMock()
+ response = http_mock.ResponseMock()
+ store = mock.Mock()
self._do_revoke_test_helper(response, b'', None, store=store)
def test__do_revoke_non_json_failure(self):
- response = httplib2.Response({
- 'status': http_client.BAD_REQUEST,
- })
+ response = http_mock.ResponseMock({'status': http_client.BAD_REQUEST})
content = u'Bad request'
error_msg = 'Invalid response {0}.'.format(response.status)
self._do_revoke_test_helper(response, content, error_msg)
def test__do_revoke_basic_failure(self):
- response = httplib2.Response({
- 'status': http_client.INTERNAL_SERVER_ERROR,
- })
+ response = http_mock.ResponseMock(
+ {'status': http_client.INTERNAL_SERVER_ERROR})
content = u'{}'
error_msg = 'Invalid response {0}.'.format(response.status)
self._do_revoke_test_helper(response, content, error_msg)
def test__do_revoke_failure_w_json_error(self):
- response = httplib2.Response({
- 'status': http_client.BAD_GATEWAY,
- })
+ response = http_mock.ResponseMock({'status': http_client.BAD_GATEWAY})
error_msg = 'Hi I am an error not a bearer'
content = json.dumps({'error': error_msg})
self._do_revoke_test_helper(response, content, error_msg)
def test__do_revoke_failure_w_json_error_and_store(self):
- response = httplib2.Response({
- 'status': http_client.BAD_GATEWAY,
- })
+ response = http_mock.ResponseMock({'status': http_client.BAD_GATEWAY})
error_msg = 'Where are we going wearer?'
content = json.dumps({'error': error_msg})
- store = mock.MagicMock()
+ store = mock.Mock()
self._do_revoke_test_helper(response, content, error_msg,
store=store)
@@ -1380,70 +1364,61 @@ class BasicCredentialsTests(unittest2.TestCase):
credentials = client.OAuth2Credentials(
None, None, None, None, None, None, None,
token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI)
- http_request = mock.Mock()
- http_request.return_value = response, content
+ http = http_mock.HttpMock(headers=response, data=content)
token = u's3kr3tz'
if response.status == http_client.OK:
self.assertEqual(credentials.scopes, set())
self.assertIsNone(
- credentials._do_retrieve_scopes(http_request, token))
+ credentials._do_retrieve_scopes(http, token))
self.assertEqual(credentials.scopes, scopes)
else:
self.assertEqual(credentials.scopes, set())
with self.assertRaises(client.Error) as exc_manager:
- credentials._do_retrieve_scopes(http_request, token)
+ credentials._do_retrieve_scopes(http, token)
# Make sure scopes were not changed.
self.assertEqual(credentials.scopes, set())
self.assertEqual(exc_manager.exception.args, (error_msg,))
- token_uri = client._update_query_params(
+ token_uri = _helpers.update_query_params(
oauth2client.GOOGLE_TOKEN_INFO_URI,
{'fields': 'scope', 'access_token': token})
- self.assertEqual(len(http_request.mock_calls), 1)
- scopes_call = http_request.mock_calls[0]
- call_args = scopes_call[1]
- self.assertEqual(len(call_args), 1)
- called_uri = call_args[0]
- assertUrisEqual(self, token_uri, called_uri)
+
+ # Verify mocks.
+ self.assertEqual(http.requests, 1)
+ assertUrisEqual(self, token_uri, http.uri)
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertIsNone(http.headers)
logger.info.assert_called_once_with('Refreshing scopes')
def test__do_retrieve_scopes_success_bad_json(self):
- response = httplib2.Response({
- 'status': http_client.OK,
- })
+ response = http_mock.ResponseMock()
invalid_json = b'{'
with self.assertRaises(ValueError):
self._do_retrieve_scopes_test_helper(response, invalid_json, None)
def test__do_retrieve_scopes_success(self):
- response = httplib2.Response({
- 'status': http_client.OK,
- })
+ response = http_mock.ResponseMock()
content = b'{"scope": "foo bar"}'
self._do_retrieve_scopes_test_helper(response, content, None,
scopes=set(['foo', 'bar']))
def test__do_retrieve_scopes_non_json_failure(self):
- response = httplib2.Response({
- 'status': http_client.BAD_REQUEST,
- })
+ response = http_mock.ResponseMock({'status': http_client.BAD_REQUEST})
content = u'Bad request'
error_msg = 'Invalid response {0}.'.format(response.status)
self._do_retrieve_scopes_test_helper(response, content, error_msg)
def test__do_retrieve_scopes_basic_failure(self):
- response = httplib2.Response({
- 'status': http_client.INTERNAL_SERVER_ERROR,
- })
+ response = http_mock.ResponseMock(
+ {'status': http_client.INTERNAL_SERVER_ERROR})
content = u'{}'
error_msg = 'Invalid response {0}.'.format(response.status)
self._do_retrieve_scopes_test_helper(response, content, error_msg)
def test__do_retrieve_scopes_failure_w_json_error(self):
- response = httplib2.Response({
- 'status': http_client.BAD_GATEWAY,
- })
+ response = http_mock.ResponseMock({'status': http_client.BAD_GATEWAY})
error_msg = 'Error desc I sit at a desk'
content = json.dumps({'error_description': error_msg})
self._do_retrieve_scopes_test_helper(response, content, error_msg)
@@ -1467,7 +1442,7 @@ class BasicCredentialsTests(unittest2.TestCase):
def test_retrieve_scopes(self):
info_response_first = {'scope': 'foo bar'}
info_response_second = {'error_description': 'abcdef'}
- http = HttpMockSequence([
+ http = http_mock.HttpMockSequence([
({'status': '200'}, json.dumps(info_response_first).encode(
'utf-8')),
({'status': '400'}, json.dumps(info_response_second).encode(
@@ -1496,17 +1471,18 @@ class BasicCredentialsTests(unittest2.TestCase):
b' "expires_in":3600,'
b' "id_token": "' + jwt + b'"'
b'}')
- http = HttpMockSequence([
+ http = http_mock.HttpMockSequence([
({'status': status_code}, b''),
({'status': '200'}, token_response),
({'status': '200'}, 'echo_request_headers'),
])
http = self.credentials.authorize(http)
- resp, content = http.request('http://example.com')
+ resp, content = transport.request(http, 'http://example.com')
self.assertEqual(self.credentials.id_token, body)
+ self.assertEqual(self.credentials.id_token_jwt, jwt.decode())
-class AccessTokenCredentialsTests(unittest2.TestCase):
+class AccessTokenCredentialsTests(unittest.TestCase):
def setUp(self):
access_token = 'foo'
@@ -1517,41 +1493,40 @@ class AccessTokenCredentialsTests(unittest2.TestCase):
def test_token_refresh_success(self):
for status_code in client.REFRESH_STATUS_CODES:
- http = HttpMockSequence([
- ({'status': status_code}, b''),
- ])
+ http = http_mock.HttpMock(
+ headers={'status': status_code}, data=b'')
http = self.credentials.authorize(http)
with self.assertRaises(client.AccessTokenCredentialsError):
- resp, content = http.request('http://example.com')
+ resp, content = transport.request(http, 'http://example.com')
def test_token_revoke_success(self):
+ http = http_mock.HttpMock(headers={'status': http_client.OK})
_token_revoke_test_helper(
- self, '200', revoke_raise=False,
- valid_bool_value=True, token_attr='access_token')
+ self, revoke_raise=False, valid_bool_value=True,
+ token_attr='access_token', http_mock=http)
def test_token_revoke_failure(self):
+ http = http_mock.HttpMock(headers={'status': http_client.BAD_REQUEST})
_token_revoke_test_helper(
- self, '400', revoke_raise=True,
- valid_bool_value=False, token_attr='access_token')
+ self, revoke_raise=True, valid_bool_value=False,
+ token_attr='access_token', http_mock=http)
def test_non_401_error_response(self):
- http = HttpMockSequence([
- ({'status': '400'}, b''),
- ])
+ http = http_mock.HttpMock(headers={'status': http_client.BAD_REQUEST})
http = self.credentials.authorize(http)
- resp, content = http.request('http://example.com')
+ resp, content = transport.request(http, 'http://example.com')
self.assertEqual(http_client.BAD_REQUEST, resp.status)
def test_auth_header_sent(self):
- http = HttpMockSequence([
+ http = http_mock.HttpMockSequence([
({'status': '200'}, 'echo_request_headers'),
])
http = self.credentials.authorize(http)
- resp, content = http.request('http://example.com')
+ resp, content = transport.request(http, 'http://example.com')
self.assertEqual(b'Bearer foo', content[b'Authorization'])
-class TestAssertionCredentials(unittest2.TestCase):
+class TestAssertionCredentials(unittest.TestCase):
assertion_text = 'This is the assertion'
assertion_type = 'http://www.google.com/assertionType'
@@ -1578,23 +1553,25 @@ class TestAssertionCredentials(unittest2.TestCase):
body['grant_type'][0])
def test_assertion_refresh(self):
- http = HttpMockSequence([
+ http = http_mock.HttpMockSequence([
({'status': '200'}, b'{"access_token":"1/3w"}'),
({'status': '200'}, 'echo_request_headers'),
])
http = self.credentials.authorize(http)
- resp, content = http.request('http://example.com')
+ resp, content = transport.request(http, 'http://example.com')
self.assertEqual(b'Bearer 1/3w', content[b'Authorization'])
def test_token_revoke_success(self):
+ http = http_mock.HttpMock(headers={'status': http_client.OK})
_token_revoke_test_helper(
- self, '200', revoke_raise=False,
- valid_bool_value=True, token_attr='access_token')
+ self, revoke_raise=False, valid_bool_value=True,
+ token_attr='access_token', http_mock=http)
def test_token_revoke_failure(self):
+ http = http_mock.HttpMock(headers={'status': http_client.BAD_REQUEST})
_token_revoke_test_helper(
- self, '400', revoke_raise=True,
- valid_bool_value=False, token_attr='access_token')
+ self, revoke_raise=True, valid_bool_value=False,
+ token_attr='access_token', http_mock=http)
def test_sign_blob_abstract(self):
credentials = client.AssertionCredentials(None)
@@ -1602,20 +1579,7 @@ class TestAssertionCredentials(unittest2.TestCase):
credentials.sign_blob(b'blob')
-class UpdateQueryParamsTest(unittest2.TestCase):
- def test_update_query_params_no_params(self):
- uri = 'http://www.google.com'
- updated = client._update_query_params(uri, {'a': 'b'})
- self.assertEqual(updated, uri + '?a=b')
-
- def test_update_query_params_existing_params(self):
- uri = 'http://www.google.com?x=y'
- updated = client._update_query_params(uri, {'a': 'b', 'c': 'd&'})
- hardcoded_update = uri + '&a=b&c=d%26'
- assertUrisEqual(self, updated, hardcoded_update)
-
-
-class ExtractIdTokenTest(unittest2.TestCase):
+class ExtractIdTokenTest(unittest.TestCase):
"""Tests client._extract_id_token()."""
def test_extract_success(self):
@@ -1636,7 +1600,7 @@ class ExtractIdTokenTest(unittest2.TestCase):
client._extract_id_token(jwt)
-class OAuth2WebServerFlowTest(unittest2.TestCase):
+class OAuth2WebServerFlowTest(unittest.TestCase):
def setUp(self):
self.flow = client.OAuth2WebServerFlow(
@@ -1647,6 +1611,9 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
user_agent='unittest-sample/1.0',
revoke_uri='dummy_revoke_uri',
)
+ self.bad_verifier = b'__NOT_THE_VERIFIER_YOURE_LOOKING_FOR__'
+ self.good_verifier = b'__TEST_VERIFIER__'
+ self.good_challenger = b'__TEST_CHALLENGE__'
def test_construct_authorize_url(self):
authorize_url = self.flow.step1_get_authorize_url(state='state+1')
@@ -1711,11 +1678,51 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
'access_type': 'offline',
'response_type': 'code',
}
- expected = client._update_query_params(flow.auth_uri, query_params)
+ expected = _helpers.update_query_params(flow.auth_uri, query_params)
assertUrisEqual(self, expected, result)
# Check stubs.
self.assertEqual(logger.warning.call_count, 1)
+ @mock.patch('oauth2client.client._pkce.code_challenge')
+ @mock.patch('oauth2client.client._pkce.code_verifier')
+ def test_step1_get_authorize_url_pkce(self, fake_verifier, fake_challenge):
+ fake_verifier.return_value = self.good_verifier
+ fake_challenge.return_value = self.good_challenger
+ flow = client.OAuth2WebServerFlow(
+ 'client_id+1',
+ scope='foo',
+ redirect_uri='http://example.com',
+ pkce=True)
+ auth_url = urllib.parse.urlparse(flow.step1_get_authorize_url())
+ self.assertEqual(flow.code_verifier, self.good_verifier)
+ results = dict(urllib.parse.parse_qsl(auth_url.query))
+ self.assertEqual(
+ results['code_challenge'], self.good_challenger.decode())
+ self.assertEqual(results['code_challenge_method'], 'S256')
+ fake_verifier.assert_called()
+ fake_challenge.assert_called_with(self.good_verifier)
+
+ @mock.patch('oauth2client.client._pkce.code_challenge')
+ @mock.patch('oauth2client.client._pkce.code_verifier')
+ def test_step1_get_authorize_url_pkce_invalid_verifier(
+ self, fake_verifier, fake_challenge):
+ fake_verifier.return_value = self.good_verifier
+ fake_challenge.return_value = self.good_challenger
+ flow = client.OAuth2WebServerFlow(
+ 'client_id+1',
+ scope='foo',
+ redirect_uri='http://example.com',
+ pkce=True,
+ code_verifier=self.bad_verifier)
+ auth_url = urllib.parse.urlparse(flow.step1_get_authorize_url())
+ self.assertEqual(flow.code_verifier, self.bad_verifier)
+ results = dict(urllib.parse.parse_qsl(auth_url.query))
+ self.assertEqual(
+ results['code_challenge'], self.good_challenger.decode())
+ self.assertEqual(results['code_challenge_method'], 'S256')
+ fake_verifier.assert_not_called()
+ fake_challenge.assert_called_with(self.bad_verifier)
+
def test_step1_get_authorize_url_without_redirect(self):
flow = client.OAuth2WebServerFlow('client_id+1', scope='foo',
redirect_uri=None)
@@ -1736,7 +1743,7 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
'access_type': 'offline',
'response_type': 'code',
}
- expected = client._update_query_params(flow.auth_uri, query_params)
+ expected = _helpers.update_query_params(flow.auth_uri, query_params)
assertUrisEqual(self, expected, result)
def test_step1_get_device_and_user_codes_wo_device_uri(self):
@@ -1758,12 +1765,15 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
'user_code': user_code,
'verification_url': ver_url,
})
- http = HttpMockSequence([
+ http = http_mock.HttpMockSequence([
({'status': http_client.OK}, content),
])
if default_http:
- with mock.patch('httplib2.Http', return_value=http):
+ with mock.patch('oauth2client.transport.get_http_object',
+ return_value=http) as new_http:
result = flow.step1_get_device_and_user_codes()
+ # Check the mock was called.
+ new_http.assert_called_once_with()
else:
result = flow.step1_get_device_and_user_codes(http=http)
@@ -1771,16 +1781,17 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
device_code, user_code, None, ver_url, None)
self.assertEqual(result, expected)
self.assertEqual(len(http.requests), 1)
- self.assertEqual(
- http.requests[0]['uri'], oauth2client.GOOGLE_DEVICE_URI)
- body = http.requests[0]['body']
- self.assertEqual(urllib.parse.parse_qs(body),
- {'client_id': [flow.client_id],
- 'scope': [flow.scope]})
+ info = http.requests[0]
+ self.assertEqual(info['uri'], oauth2client.GOOGLE_DEVICE_URI)
+ expected_body = {
+ 'client_id': [flow.client_id],
+ 'scope': [flow.scope],
+ }
+ self.assertEqual(urllib.parse.parse_qs(info['body']), expected_body)
headers = {'content-type': 'application/x-www-form-urlencoded'}
if extra_headers is not None:
headers.update(extra_headers)
- self.assertEqual(http.requests[0]['headers'], headers)
+ self.assertEqual(info['headers'], headers)
def test_step1_get_device_and_user_codes(self):
self._step1_get_device_and_user_codes_helper()
@@ -1803,9 +1814,7 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
def _step1_get_device_and_user_codes_fail_helper(self, status,
content, error_msg):
flow = client.OAuth2WebServerFlow('CID', scope='foo')
- http = HttpMockSequence([
- ({'status': status}, content),
- ])
+ http = http_mock.HttpMock(headers={'status': status}, data=content)
with self.assertRaises(client.OAuth2DeviceCodeError) as exc_manager:
flow.step1_get_device_and_user_codes(http=http)
@@ -1849,17 +1858,19 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
client.OAuth2WebServerFlow('client_id+1')
def test_exchange_failure(self):
- http = HttpMockSequence([
- ({'status': '400'}, b'{"error":"invalid_request"}'),
- ])
+ http = http_mock.HttpMock(
+ headers={'status': http_client.BAD_REQUEST},
+ data=b'{"error":"invalid_request"}',
+ )
with self.assertRaises(client.FlowExchangeError):
self.flow.step2_exchange(code='some random code', http=http)
def test_urlencoded_exchange_failure(self):
- http = HttpMockSequence([
- ({'status': '400'}, b'error=invalid_request'),
- ])
+ http = http_mock.HttpMock(
+ headers={'status': http_client.BAD_REQUEST},
+ data=b'error=invalid_request',
+ )
with self.assertRaisesRegexp(client.FlowExchangeError,
'invalid_request'):
@@ -1876,7 +1887,7 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
b' "type": "OAuthException"'
b' }'
b'}')
- http = HttpMockSequence([({'status': '400'}, payload)])
+ http = http_mock.HttpMock(data=payload)
with self.assertRaises(client.FlowExchangeError):
self.flow.step2_exchange(code='some random code', http=http)
@@ -1887,7 +1898,7 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
b' "expires_in":3600,'
b' "refresh_token":"8xLOxBtZp8"'
b'}')
- http = HttpMockSequence([({'status': '200'}, payload)])
+ http = http_mock.HttpMock(data=payload)
credentials = self.flow.step2_exchange(
code=code, device_flow_info=device_flow_info, http=http)
self.assertEqual('SlAV32hkKG', credentials.access_token)
@@ -1916,8 +1927,7 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
' "expires_in":' + expires_in + ','
' "refresh_token":"' + refresh_token + '"'
'}')
- http = HttpMockSequence(
- [({'status': '200'}, _helpers._to_bytes(payload))])
+ http = http_mock.HttpMock(data=_helpers._to_bytes(payload))
credentials = self.flow.step2_exchange(code=binary_code, http=http)
self.assertEqual(access_token, credentials.access_token)
self.assertIsNotNone(credentials.token_expiry)
@@ -1943,7 +1953,9 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
b' "expires_in":3600,'
b' "refresh_token":"8xLOxBtZp8"'
b'}')
- http = HttpMockSequence([({'status': '200'}, payload)])
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK}, payload),
+ ])
credentials = self.flow.step2_exchange(code=not_a_dict, http=http)
self.assertEqual('SlAV32hkKG', credentials.access_token)
@@ -1951,10 +1963,29 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
self.assertEqual('8xLOxBtZp8', credentials.refresh_token)
self.assertEqual('dummy_revoke_uri', credentials.revoke_uri)
self.assertEqual(set(['foo']), credentials.scopes)
+ self.assertEqual(len(http.requests), 1)
request_code = urllib.parse.parse_qs(
http.requests[0]['body'])['code'][0]
self.assertEqual(code, request_code)
+ def test_exchange_with_pkce(self):
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK}, b'access_token=SlAV32hkKG'),
+ ])
+ flow = client.OAuth2WebServerFlow(
+ 'client_id+1',
+ scope='foo',
+ redirect_uri='http://example.com',
+ pkce=True,
+ code_verifier=self.good_verifier)
+ flow.step2_exchange(code='some random code', http=http)
+
+ self.assertEqual(len(http.requests), 1)
+ test_request = http.requests[0]
+ self.assertIn(
+ 'code_verifier={0}'.format(self.good_verifier.decode()),
+ test_request['body'])
+
def test_exchange_using_authorization_header(self):
auth_header = 'Basic Y2xpZW50X2lkKzE6c2Vjexc_managerV0KzE=',
flow = client.OAuth2WebServerFlow(
@@ -1965,13 +1996,14 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
user_agent='unittest-sample/1.0',
revoke_uri='dummy_revoke_uri',
)
- http = HttpMockSequence([
- ({'status': '200'}, b'access_token=SlAV32hkKG'),
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK}, b'access_token=SlAV32hkKG'),
])
credentials = flow.step2_exchange(code='some random code', http=http)
self.assertEqual('SlAV32hkKG', credentials.access_token)
+ self.assertEqual(len(http.requests), 1)
test_request = http.requests[0]
# Did we pass the Authorization header?
self.assertEqual(test_request['headers']['Authorization'], auth_header)
@@ -1979,9 +2011,8 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
self.assertTrue('client_secret' not in test_request['body'])
def test_urlencoded_exchange_success(self):
- http = HttpMockSequence([
- ({'status': '200'}, b'access_token=SlAV32hkKG&expires_in=3600'),
- ])
+ http = http_mock.HttpMock(
+ data=b'access_token=SlAV32hkKG&expires_in=3600')
credentials = self.flow.step2_exchange(code='some random code',
http=http)
@@ -1989,12 +2020,9 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
self.assertNotEqual(None, credentials.token_expiry)
def test_urlencoded_expires_param(self):
- http = HttpMockSequence([
- # Note the 'expires=3600' where you'd normally
- # have if named 'expires_in'
- ({'status': '200'}, b'access_token=SlAV32hkKG&expires=3600'),
- ])
-
+ # Note the 'expires=3600' where you'd normally
+ # have if named 'expires_in'
+ http = http_mock.HttpMock(data=b'access_token=SlAV32hkKG&expires=3600')
credentials = self.flow.step2_exchange(code='some random code',
http=http)
self.assertNotEqual(None, credentials.token_expiry)
@@ -2004,18 +2032,16 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
b' "access_token":"SlAV32hkKG",'
b' "refresh_token":"8xLOxBtZp8"'
b'}')
- http = HttpMockSequence([({'status': '200'}, payload)])
+ http = http_mock.HttpMock(data=payload)
credentials = self.flow.step2_exchange(code='some random code',
http=http)
self.assertEqual(None, credentials.token_expiry)
def test_urlencoded_exchange_no_expires_in(self):
- http = HttpMockSequence([
- # This might be redundant but just to make sure
- # urlencoded access_token gets parsed correctly
- ({'status': '200'}, b'access_token=SlAV32hkKG'),
- ])
+ # This might be redundant but just to make sure
+ # urlencoded access_token gets parsed correctly
+ http = http_mock.HttpMock(data=b'access_token=SlAV32hkKG')
credentials = self.flow.step2_exchange(code='some random code',
http=http)
@@ -2026,7 +2052,7 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
b' "access_token":"SlAV32hkKG",'
b' "refresh_token":"8xLOxBtZp8"'
b'}')
- http = HttpMockSequence([({'status': '200'}, payload)])
+ http = http_mock.HttpMock(data=payload)
code = {'error': 'thou shall not pass'}
with self.assertRaisesRegexp(
@@ -2039,7 +2065,7 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
b' "refresh_token":"8xLOxBtZp8",'
b' "id_token": "stuff.payload"'
b'}')
- http = HttpMockSequence([({'status': '200'}, payload)])
+ http = http_mock.HttpMock(data=payload)
with self.assertRaises(client.VerifyJwtTokenError):
self.flow.step2_exchange(code='some random code', http=http)
@@ -2056,16 +2082,17 @@ class OAuth2WebServerFlowTest(unittest2.TestCase):
b' "refresh_token":"8xLOxBtZp8",'
b' "id_token": "' + jwt + b'"'
b'}')
- http = HttpMockSequence([({'status': '200'}, payload)])
+ http = http_mock.HttpMock(data=payload)
credentials = self.flow.step2_exchange(code='some random code',
http=http)
self.assertEqual(credentials.id_token, body)
+ self.assertEqual(credentials.id_token_jwt, jwt.decode())
-class FlowFromCachedClientsecrets(unittest2.TestCase):
+class FlowFromCachedClientsecrets(unittest.TestCase):
def test_flow_from_clientsecrets_cached(self):
- cache_mock = CacheMock()
+ cache_mock = http_mock.CacheMock()
load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
flow = client.flow_from_clientsecrets(
@@ -2168,7 +2195,7 @@ class FlowFromCachedClientsecrets(unittest2.TestCase):
loadfile_mock.assert_called_once_with(filename, cache=cache)
-class CredentialsFromCodeTests(unittest2.TestCase):
+class CredentialsFromCodeTests(unittest.TestCase):
def setUp(self):
self.client_id = 'client_id_abc'
@@ -2180,9 +2207,7 @@ class CredentialsFromCodeTests(unittest2.TestCase):
def test_exchange_code_for_token(self):
token = 'asdfghjkl'
payload = json.dumps({'access_token': token, 'expires_in': 3600})
- http = HttpMockSequence([
- ({'status': '200'}, payload.encode('utf-8')),
- ])
+ http = http_mock.HttpMock(data=payload.encode('utf-8'))
credentials = client.credentials_from_code(
self.client_id, self.client_secret, self.scope,
self.code, http=http, redirect_uri=self.redirect_uri)
@@ -2191,9 +2216,10 @@ class CredentialsFromCodeTests(unittest2.TestCase):
self.assertEqual(set(['foo']), credentials.scopes)
def test_exchange_code_for_token_fail(self):
- http = HttpMockSequence([
- ({'status': '400'}, b'{"error":"invalid_request"}'),
- ])
+ http = http_mock.HttpMock(
+ headers={'status': http_client.BAD_REQUEST},
+ data=b'{"error":"invalid_request"}',
+ )
with self.assertRaises(client.FlowExchangeError):
client.credentials_from_code(
@@ -2205,7 +2231,7 @@ class CredentialsFromCodeTests(unittest2.TestCase):
b' "access_token":"asdfghjkl",'
b' "expires_in":3600'
b'}')
- http = HttpMockSequence([({'status': '200'}, payload)])
+ http = http_mock.HttpMock(data=payload)
credentials = client.credentials_from_clientsecrets_and_code(
datafile('client_secrets.json'), self.scope,
self.code, http=http)
@@ -2214,10 +2240,8 @@ class CredentialsFromCodeTests(unittest2.TestCase):
self.assertEqual(set(['foo']), credentials.scopes)
def test_exchange_code_and_cached_file_for_token(self):
- http = HttpMockSequence([
- ({'status': '200'}, b'{ "access_token":"asdfghjkl"}'),
- ])
- cache_mock = CacheMock()
+ http = http_mock.HttpMock(data=b'{ "access_token":"asdfghjkl"}')
+ cache_mock = http_mock.CacheMock()
load_and_cache('client_secrets.json', 'some_secrets', cache_mock)
credentials = client.credentials_from_clientsecrets_and_code(
@@ -2227,9 +2251,10 @@ class CredentialsFromCodeTests(unittest2.TestCase):
self.assertEqual(set(['foo']), credentials.scopes)
def test_exchange_code_and_file_for_token_fail(self):
- http = HttpMockSequence([
- ({'status': '400'}, b'{"error":"invalid_request"}'),
- ])
+ http = http_mock.HttpMock(
+ headers={'status': http_client.BAD_REQUEST},
+ data=b'{"error":"invalid_request"}',
+ )
with self.assertRaises(client.FlowExchangeError):
client.credentials_from_clientsecrets_and_code(
@@ -2237,7 +2262,7 @@ class CredentialsFromCodeTests(unittest2.TestCase):
self.code, http=http)
-class Test__save_private_file(unittest2.TestCase):
+class Test__save_private_file(unittest.TestCase):
def _save_helper(self, filename):
contents = []
@@ -2265,7 +2290,7 @@ class Test__save_private_file(unittest2.TestCase):
self._save_helper(filename)
-class Test__get_application_default_credential_GAE(unittest2.TestCase):
+class Test__get_application_default_credential_GAE(unittest.TestCase):
@mock.patch.dict('sys.modules', {
'oauth2client.contrib.appengine': mock.Mock()})
@@ -2278,7 +2303,7 @@ class Test__get_application_default_credential_GAE(unittest2.TestCase):
creds_kls.assert_called_once_with([])
-class Test__get_application_default_credential_GCE(unittest2.TestCase):
+class Test__get_application_default_credential_GCE(unittest.TestCase):
@mock.patch.dict('sys.modules', {
'oauth2client.contrib.gce': mock.Mock()})
@@ -2291,7 +2316,7 @@ class Test__get_application_default_credential_GCE(unittest2.TestCase):
creds_kls.assert_called_once_with()
-class Test__require_crypto_or_die(unittest2.TestCase):
+class Test__require_crypto_or_die(unittest.TestCase):
@mock.patch.object(client, 'HAS_CRYPTO', new=True)
def test_with_crypto(self):
@@ -2303,7 +2328,7 @@ class Test__require_crypto_or_die(unittest2.TestCase):
client._require_crypto_or_die()
-class TestDeviceFlowInfo(unittest2.TestCase):
+class TestDeviceFlowInfo(unittest.TestCase):
DEVICE_CODE = 'e80ff179-fd65-416c-9dbf-56a23e5d23e4'
USER_CODE = '4bbd8b82-fc73-11e5-adf3-00c2c63e5792'
diff --git a/tests/test_clientsecrets.py b/tests/test_clientsecrets.py
index 42eb8c7..3fa9c30 100644
--- a/tests/test_clientsecrets.py
+++ b/tests/test_clientsecrets.py
@@ -18,17 +18,13 @@ import errno
from io import StringIO
import os
import tempfile
-
-import unittest2
+import unittest
import oauth2client
from oauth2client import _helpers
from oauth2client import clientsecrets
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
-
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
VALID_FILE = os.path.join(DATA_DIR, 'client_secrets.json')
INVALID_FILE = os.path.join(DATA_DIR, 'unfilled_client_secrets.json')
@@ -36,7 +32,7 @@ NONEXISTENT_FILE = os.path.join(
os.path.dirname(__file__), 'afilethatisntthere.json')
-class Test__validate_clientsecrets(unittest2.TestCase):
+class Test__validate_clientsecrets(unittest.TestCase):
def test_with_none(self):
with self.assertRaises(clientsecrets.InvalidClientSecretsError):
@@ -147,7 +143,7 @@ class Test__validate_clientsecrets(unittest2.TestCase):
self.assertEqual(result, (clientsecrets.TYPE_INSTALLED, client_info))
-class Test__loadfile(unittest2.TestCase):
+class Test__loadfile(unittest.TestCase):
def test_success(self):
client_type, client_info = clientsecrets._loadfile(VALID_FILE)
@@ -176,7 +172,7 @@ class Test__loadfile(unittest2.TestCase):
clientsecrets._loadfile(filename)
-class OAuth2CredentialsTests(unittest2.TestCase):
+class OAuth2CredentialsTests(unittest.TestCase):
def test_validate_error(self):
payload = (
@@ -223,7 +219,7 @@ class OAuth2CredentialsTests(unittest2.TestCase):
self.assertEquals(exc_manager.exception.args[3], errno.ENOENT)
-class CachedClientsecretsTests(unittest2.TestCase):
+class CachedClientsecretsTests(unittest.TestCase):
class CacheMock(object):
def __init__(self):
diff --git a/tests/test_crypt.py b/tests/test_crypt.py
index b7534bd..bc56697 100644
--- a/tests/test_crypt.py
+++ b/tests/test_crypt.py
@@ -14,9 +14,9 @@
import base64
import os
+import unittest
import mock
-import unittest2
from oauth2client import _helpers
from oauth2client import client
@@ -33,14 +33,14 @@ def datafile(filename):
return file_obj.read()
-class Test__bad_pkcs12_key_as_pem(unittest2.TestCase):
+class Test__bad_pkcs12_key_as_pem(unittest.TestCase):
def test_fails(self):
with self.assertRaises(NotImplementedError):
crypt._bad_pkcs12_key_as_pem()
-class Test_pkcs12_key_as_pem(unittest2.TestCase):
+class Test_pkcs12_key_as_pem(unittest.TestCase):
def _make_svc_account_creds(self, private_key_file='privatekey.p12'):
filename = data_filename(private_key_file)
@@ -72,7 +72,7 @@ class Test_pkcs12_key_as_pem(unittest2.TestCase):
self._succeeds_helper(password)
-class Test__verify_signature(unittest2.TestCase):
+class Test__verify_signature(unittest.TestCase):
def test_success_single_cert(self):
cert_value = 'cert-value'
@@ -80,11 +80,11 @@ class Test__verify_signature(unittest2.TestCase):
message = object()
signature = object()
- verifier = mock.MagicMock()
- verifier.verify = mock.MagicMock(name='verify', return_value=True)
+ verifier = mock.Mock()
+ verifier.verify = mock.Mock(name='verify', return_value=True)
with mock.patch('oauth2client.crypt.Verifier') as Verifier:
- Verifier.from_string = mock.MagicMock(name='from_string',
- return_value=verifier)
+ Verifier.from_string = mock.Mock(name='from_string',
+ return_value=verifier)
result = crypt._verify_signature(message, signature, certs)
self.assertEqual(result, None)
@@ -101,14 +101,14 @@ class Test__verify_signature(unittest2.TestCase):
message = object()
signature = object()
- verifier = mock.MagicMock()
+ verifier = mock.Mock()
# Use side_effect to force all 3 cert values to be used by failing
# to verify on the first two.
- verifier.verify = mock.MagicMock(name='verify',
- side_effect=[False, False, True])
+ verifier.verify = mock.Mock(name='verify',
+ side_effect=[False, False, True])
with mock.patch('oauth2client.crypt.Verifier') as Verifier:
- Verifier.from_string = mock.MagicMock(name='from_string',
- return_value=verifier)
+ Verifier.from_string = mock.Mock(name='from_string',
+ return_value=verifier)
result = crypt._verify_signature(message, signature, certs)
self.assertEqual(result, None)
@@ -130,11 +130,11 @@ class Test__verify_signature(unittest2.TestCase):
message = object()
signature = object()
- verifier = mock.MagicMock()
- verifier.verify = mock.MagicMock(name='verify', return_value=False)
+ verifier = mock.Mock()
+ verifier.verify = mock.Mock(name='verify', return_value=False)
with mock.patch('oauth2client.crypt.Verifier') as Verifier:
- Verifier.from_string = mock.MagicMock(name='from_string',
- return_value=verifier)
+ Verifier.from_string = mock.Mock(name='from_string',
+ return_value=verifier)
with self.assertRaises(crypt.AppIdentityError):
crypt._verify_signature(message, signature, certs)
@@ -144,7 +144,7 @@ class Test__verify_signature(unittest2.TestCase):
verifier.verify.assert_called_once_with(message, signature)
-class Test__check_audience(unittest2.TestCase):
+class Test__check_audience(unittest.TestCase):
def test_null_audience(self):
result = crypt._check_audience(None, None)
@@ -172,7 +172,7 @@ class Test__check_audience(unittest2.TestCase):
crypt._check_audience(payload_dict, audience2)
-class Test__verify_time_range(unittest2.TestCase):
+class Test__verify_time_range(unittest.TestCase):
def _exception_helper(self, payload_dict):
exception_caught = None
@@ -204,8 +204,8 @@ class Test__verify_time_range(unittest2.TestCase):
'exp': current_time + crypt.MAX_TOKEN_LIFETIME_SECS + 1,
}
with mock.patch('oauth2client.crypt.time') as time:
- time.time = mock.MagicMock(name='time',
- return_value=current_time)
+ time.time = mock.Mock(name='time',
+ return_value=current_time)
exception_caught = self._exception_helper(payload_dict)
self.assertNotEqual(exception_caught, None)
@@ -219,8 +219,8 @@ class Test__verify_time_range(unittest2.TestCase):
'exp': current_time + crypt.MAX_TOKEN_LIFETIME_SECS - 1,
}
with mock.patch('oauth2client.crypt.time') as time:
- time.time = mock.MagicMock(name='time',
- return_value=current_time)
+ time.time = mock.Mock(name='time',
+ return_value=current_time)
exception_caught = self._exception_helper(payload_dict)
self.assertNotEqual(exception_caught, None)
@@ -234,8 +234,8 @@ class Test__verify_time_range(unittest2.TestCase):
'exp': current_time - crypt.CLOCK_SKEW_SECS - 1,
}
with mock.patch('oauth2client.crypt.time') as time:
- time.time = mock.MagicMock(name='time',
- return_value=current_time)
+ time.time = mock.Mock(name='time',
+ return_value=current_time)
exception_caught = self._exception_helper(payload_dict)
self.assertNotEqual(exception_caught, None)
@@ -249,14 +249,14 @@ class Test__verify_time_range(unittest2.TestCase):
'exp': current_time + crypt.MAX_TOKEN_LIFETIME_SECS - 1,
}
with mock.patch('oauth2client.crypt.time') as time:
- time.time = mock.MagicMock(name='time',
- return_value=current_time)
+ time.time = mock.Mock(name='time',
+ return_value=current_time)
exception_caught = self._exception_helper(payload_dict)
self.assertEqual(exception_caught, None)
-class Test_verify_signed_jwt_with_certs(unittest2.TestCase):
+class Test_verify_signed_jwt_with_certs(unittest.TestCase):
def test_jwt_no_segments(self):
exception_caught = None
@@ -288,10 +288,10 @@ class Test_verify_signed_jwt_with_certs(unittest2.TestCase):
@mock.patch('oauth2client.crypt._verify_time_range')
@mock.patch('oauth2client.crypt._verify_signature')
def test_success(self, verify_sig, verify_time, check_aud):
- certs = mock.MagicMock()
+ certs = mock.Mock()
cert_values = object()
- certs.values = mock.MagicMock(name='values',
- return_value=cert_values)
+ certs.values = mock.Mock(name='values',
+ return_value=cert_values)
audience = object()
header = b'header'
diff --git a/tests/test_file.py b/tests/test_file.py
index 924acb4..80324d6 100644
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -12,10 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Oauth2client.file tests
-
-Unit tests for oauth2client.file
-"""
+"""Unit tests for oauth2client.file."""
import copy
import datetime
@@ -24,28 +21,31 @@ import os
import pickle
import stat
import tempfile
+import unittest
+import warnings
+import mock
import six
from six.moves import http_client
-import unittest2
+from six.moves import urllib_parse
+from oauth2client import _helpers
from oauth2client import client
-from oauth2client import file
-from .http_mock import HttpMockSequence
+from oauth2client import file as file_module
+from oauth2client import transport
+from tests import http_mock
try:
# Python2
from future_builtins import oct
-except: # pragma: NO COVER
+except ImportError: # pragma: NO COVER
pass
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
_filehandle, FILENAME = tempfile.mkstemp('oauth2client_test.data')
os.close(_filehandle)
-class OAuth2ClientFileTests(unittest2.TestCase):
+class OAuth2ClientFileTests(unittest.TestCase):
def tearDown(self):
try:
@@ -54,6 +54,7 @@ class OAuth2ClientFileTests(unittest2.TestCase):
pass
def setUp(self):
+ warnings.simplefilter("ignore")
try:
os.unlink(FILENAME)
except OSError:
@@ -74,19 +75,31 @@ class OAuth2ClientFileTests(unittest2.TestCase):
user_agent)
return credentials
- def test_non_existent_file_storage(self):
- s = file.Storage(FILENAME)
- credentials = s.get()
- self.assertEquals(None, credentials)
+ @mock.patch('warnings.warn')
+ def test_non_existent_file_storage(self, warn_mock):
+ storage = file_module.Storage(FILENAME)
+ credentials = storage.get()
+ warn_mock.assert_called_with(
+ _helpers._MISSING_FILE_MESSAGE.format(FILENAME))
+ self.assertIsNone(credentials)
+
+ def test_directory_file_storage(self):
+ storage = file_module.Storage(FILENAME)
+ os.mkdir(FILENAME)
+ try:
+ with self.assertRaises(IOError):
+ storage.get()
+ finally:
+ os.rmdir(FILENAME)
- @unittest2.skipIf(not hasattr(os, 'symlink'), 'No symlink available')
+ @unittest.skipIf(not hasattr(os, 'symlink'), 'No symlink available')
def test_no_sym_link_credentials(self):
SYMFILENAME = FILENAME + '.sym'
os.symlink(FILENAME, SYMFILENAME)
- s = file.Storage(SYMFILENAME)
+ storage = file_module.Storage(SYMFILENAME)
try:
- with self.assertRaises(file.CredentialsFileSymbolicLinkError):
- s.get()
+ with self.assertRaises(IOError):
+ storage.get()
finally:
os.unlink(SYMFILENAME)
@@ -94,20 +107,20 @@ class OAuth2ClientFileTests(unittest2.TestCase):
# Write a file with a pickled OAuth2Credentials.
credentials = self._create_test_credentials()
- f = open(FILENAME, 'wb')
- pickle.dump(credentials, f)
- f.close()
+ credentials_file = open(FILENAME, 'wb')
+ pickle.dump(credentials, credentials_file)
+ credentials_file.close()
# Storage should be not be able to read that object, as the capability
# to read and write credentials as pickled objects has been removed.
- s = file.Storage(FILENAME)
- read_credentials = s.get()
- self.assertEquals(None, read_credentials)
+ storage = file_module.Storage(FILENAME)
+ read_credentials = storage.get()
+ self.assertIsNone(read_credentials)
# Now write it back out and confirm it has been rewritten as JSON
- s.put(credentials)
- with open(FILENAME) as f:
- data = json.load(f)
+ storage.put(credentials)
+ with open(FILENAME) as credentials_file:
+ data = json.load(credentials_file)
self.assertEquals(data['access_token'], 'foo')
self.assertEquals(data['_class'], 'OAuth2Credentials')
@@ -118,22 +131,38 @@ class OAuth2ClientFileTests(unittest2.TestCase):
datetime.timedelta(minutes=15))
credentials = self._create_test_credentials(expiration=expiration)
- s = file.Storage(FILENAME)
- s.put(credentials)
- credentials = s.get()
+ storage = file_module.Storage(FILENAME)
+ storage.put(credentials)
+ credentials = storage.get()
new_cred = copy.copy(credentials)
new_cred.access_token = 'bar'
- s.put(new_cred)
+ storage.put(new_cred)
access_token = '1/3w'
token_response = {'access_token': access_token, 'expires_in': 3600}
- http = HttpMockSequence([
- ({'status': '200'}, json.dumps(token_response).encode('utf-8')),
- ])
+ response_content = json.dumps(token_response).encode('utf-8')
+ http = http_mock.HttpMock(data=response_content)
- credentials._refresh(http.request)
+ credentials._refresh(http)
self.assertEquals(credentials.access_token, access_token)
+ # Verify mocks.
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, credentials.token_uri)
+ self.assertEqual(http.method, 'POST')
+ expected_body = {
+ 'grant_type': ['refresh_token'],
+ 'client_id': [credentials.client_id],
+ 'client_secret': [credentials.client_secret],
+ 'refresh_token': [credentials.refresh_token],
+ }
+ self.assertEqual(urllib_parse.parse_qs(http.body), expected_body)
+ expected_headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ 'user-agent': credentials.user_agent,
+ }
+ self.assertEqual(http.headers, expected_headers)
+
def test_token_refresh_store_expires_soon(self):
# Tests the case where an access token that is valid when it is read
# from the store expires before the original request succeeds.
@@ -141,28 +170,28 @@ class OAuth2ClientFileTests(unittest2.TestCase):
datetime.timedelta(minutes=15))
credentials = self._create_test_credentials(expiration=expiration)
- s = file.Storage(FILENAME)
- s.put(credentials)
- credentials = s.get()
+ storage = file_module.Storage(FILENAME)
+ storage.put(credentials)
+ credentials = storage.get()
new_cred = copy.copy(credentials)
new_cred.access_token = 'bar'
- s.put(new_cred)
+ storage.put(new_cred)
access_token = '1/3w'
token_response = {'access_token': access_token, 'expires_in': 3600}
- http = HttpMockSequence([
- ({'status': str(int(http_client.UNAUTHORIZED))},
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.UNAUTHORIZED},
b'Initial token expired'),
- ({'status': str(int(http_client.UNAUTHORIZED))},
+ ({'status': http_client.UNAUTHORIZED},
b'Store token expired'),
- ({'status': str(int(http_client.OK))},
+ ({'status': http_client.OK},
json.dumps(token_response).encode('utf-8')),
- ({'status': str(int(http_client.OK))},
+ ({'status': http_client.OK},
b'Valid response to original request')
])
credentials.authorize(http)
- http.request('https://example.com')
+ transport.request(http, 'https://example.com')
self.assertEqual(credentials.access_token, access_token)
def test_token_refresh_good_store(self):
@@ -170,12 +199,12 @@ class OAuth2ClientFileTests(unittest2.TestCase):
datetime.timedelta(minutes=15))
credentials = self._create_test_credentials(expiration=expiration)
- s = file.Storage(FILENAME)
- s.put(credentials)
- credentials = s.get()
+ storage = file_module.Storage(FILENAME)
+ storage.put(credentials)
+ credentials = storage.get()
new_cred = copy.copy(credentials)
new_cred.access_token = 'bar'
- s.put(new_cred)
+ storage.put(new_cred)
credentials._refresh(None)
self.assertEquals(credentials.access_token, 'bar')
@@ -185,43 +214,43 @@ class OAuth2ClientFileTests(unittest2.TestCase):
datetime.timedelta(minutes=15))
credentials = self._create_test_credentials(expiration=expiration)
- s = file.Storage(FILENAME)
- s.put(credentials)
- credentials = s.get()
+ storage = file_module.Storage(FILENAME)
+ storage.put(credentials)
+ credentials = storage.get()
new_cred = copy.copy(credentials)
new_cred.access_token = 'bar'
- s.put(new_cred)
+ storage.put(new_cred)
valid_access_token = '1/3w'
token_response = {'access_token': valid_access_token,
'expires_in': 3600}
- http = HttpMockSequence([
- ({'status': str(int(http_client.UNAUTHORIZED))},
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.UNAUTHORIZED},
b'Initial token expired'),
- ({'status': str(int(http_client.UNAUTHORIZED))},
+ ({'status': http_client.UNAUTHORIZED},
b'Store token expired'),
- ({'status': str(int(http_client.OK))},
+ ({'status': http_client.OK},
json.dumps(token_response).encode('utf-8')),
- ({'status': str(int(http_client.OK))}, 'echo_request_body')
+ ({'status': http_client.OK}, 'echo_request_body')
])
body = six.StringIO('streaming body')
credentials.authorize(http)
- _, content = http.request('https://example.com', body=body)
+ _, content = transport.request(http, 'https://example.com', body=body)
self.assertEqual(content, 'streaming body')
self.assertEqual(credentials.access_token, valid_access_token)
def test_credentials_delete(self):
credentials = self._create_test_credentials()
- s = file.Storage(FILENAME)
- s.put(credentials)
- credentials = s.get()
- self.assertNotEquals(None, credentials)
- s.delete()
- credentials = s.get()
- self.assertEquals(None, credentials)
+ storage = file_module.Storage(FILENAME)
+ storage.put(credentials)
+ credentials = storage.get()
+ self.assertIsNotNone(credentials)
+ storage.delete()
+ credentials = storage.get()
+ self.assertIsNone(credentials)
def test_access_token_credentials(self):
access_token = 'foo'
@@ -229,11 +258,11 @@ class OAuth2ClientFileTests(unittest2.TestCase):
credentials = client.AccessTokenCredentials(access_token, user_agent)
- s = file.Storage(FILENAME)
- credentials = s.put(credentials)
- credentials = s.get()
+ storage = file_module.Storage(FILENAME)
+ credentials = storage.put(credentials)
+ credentials = storage.get()
- self.assertNotEquals(None, credentials)
+ self.assertIsNotNone(credentials)
self.assertEquals('foo', credentials.access_token)
self.assertTrue(os.path.exists(FILENAME))
diff --git a/tests/test_jwt.py b/tests/test_jwt.py
index ecc58e8..6502a4a 100644
--- a/tests/test_jwt.py
+++ b/tests/test_jwt.py
@@ -17,19 +17,18 @@
import os
import tempfile
import time
+import unittest
import mock
-import unittest2
+from six.moves import http_client
from oauth2client import _helpers
from oauth2client import client
from oauth2client import crypt
-from oauth2client import file
+from oauth2client import file as file_module
from oauth2client import service_account
-from .http_mock import HttpMockSequence
-
-
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+from oauth2client import transport
+from tests import http_mock
_FORMATS_TO_CONSTRUCTOR_ARGS = {
@@ -47,7 +46,7 @@ def datafile(filename):
return file_obj.read()
-class CryptTests(unittest2.TestCase):
+class CryptTests(unittest.TestCase):
def setUp(self):
self.format_ = 'p12'
@@ -114,25 +113,30 @@ class CryptTests(unittest2.TestCase):
self.assertEqual('billy bob', contents['user'])
self.assertEqual('data', contents['metadata']['meta'])
+ def _verify_http_mock(self, http):
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, client.ID_TOKEN_VERIFICATION_CERTS)
+ self.assertEqual(http.method, 'GET')
+ self.assertIsNone(http.body)
+ self.assertIsNone(http.headers)
+
def test_verify_id_token_with_certs_uri(self):
jwt = self._create_signed_jwt()
- http = HttpMockSequence([
- ({'status': '200'}, datafile('certs.json')),
- ])
-
+ http = http_mock.HttpMock(data=datafile('certs.json'))
contents = client.verify_id_token(
jwt, 'some_audience_address@testing.gserviceaccount.com',
http=http)
self.assertEqual('billy bob', contents['user'])
self.assertEqual('data', contents['metadata']['meta'])
+ # Verify mocks.
+ self._verify_http_mock(http)
+
def test_verify_id_token_with_certs_uri_default_http(self):
jwt = self._create_signed_jwt()
- http = HttpMockSequence([
- ({'status': '200'}, datafile('certs.json')),
- ])
+ http = http_mock.HttpMock(data=datafile('certs.json'))
with mock.patch('oauth2client.transport._CACHED_HTTP', new=http):
contents = client.verify_id_token(
@@ -141,17 +145,23 @@ class CryptTests(unittest2.TestCase):
self.assertEqual('billy bob', contents['user'])
self.assertEqual('data', contents['metadata']['meta'])
+ # Verify mocks.
+ self._verify_http_mock(http)
+
def test_verify_id_token_with_certs_uri_fails(self):
jwt = self._create_signed_jwt()
test_email = 'some_audience_address@testing.gserviceaccount.com'
- http = HttpMockSequence([
- ({'status': '404'}, datafile('certs.json')),
- ])
+ http = http_mock.HttpMock(
+ headers={'status': http_client.NOT_FOUND},
+ data=datafile('certs.json'))
with self.assertRaises(client.VerifyJwtTokenError):
client.verify_id_token(jwt, test_email, http=http)
+ # Verify mocks.
+ self._verify_http_mock(http)
+
def test_verify_id_token_bad_tokens(self):
private_key = datafile('privatekey.' + self.format_)
@@ -232,12 +242,16 @@ class PEMCryptTestsOpenSSL(CryptTests):
self.verifier = crypt.OpenSSLVerifier
-class SignedJwtAssertionCredentialsTests(unittest2.TestCase):
+class SignedJwtAssertionCredentialsTests(unittest.TestCase):
def setUp(self):
+ self.orig_signer = crypt.Signer
self.format_ = 'p12'
crypt.Signer = crypt.OpenSSLSigner
+ def tearDown(self):
+ crypt.Signer = self.orig_signer
+
def _make_credentials(self):
private_key = datafile('privatekey.' + self.format_)
signer = crypt.Signer.from_string(private_key)
@@ -257,12 +271,13 @@ class SignedJwtAssertionCredentialsTests(unittest2.TestCase):
def test_credentials_good(self):
credentials = self._make_credentials()
- http = HttpMockSequence([
- ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'),
- ({'status': '200'}, 'echo_request_headers'),
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK},
+ b'{"access_token":"1/3w","expires_in":3600}'),
+ ({'status': http_client.OK}, 'echo_request_headers'),
])
http = credentials.authorize(http)
- resp, content = http.request('http://example.org')
+ resp, content = transport.request(http, 'http://example.org')
self.assertEqual(b'Bearer 1/3w', content[b'Authorization'])
def test_credentials_to_from_json(self):
@@ -276,14 +291,16 @@ class SignedJwtAssertionCredentialsTests(unittest2.TestCase):
self.assertEqual(credentials._kwargs, restored._kwargs)
def _credentials_refresh(self, credentials):
- http = HttpMockSequence([
- ({'status': '200'}, b'{"access_token":"1/3w","expires_in":3600}'),
- ({'status': '401'}, b''),
- ({'status': '200'}, b'{"access_token":"3/3w","expires_in":3600}'),
- ({'status': '200'}, 'echo_request_headers'),
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK},
+ b'{"access_token":"1/3w","expires_in":3600}'),
+ ({'status': http_client.UNAUTHORIZED}, b''),
+ ({'status': http_client.OK},
+ b'{"access_token":"3/3w","expires_in":3600}'),
+ ({'status': http_client.OK}, 'echo_request_headers'),
])
http = credentials.authorize(http)
- _, content = http.request('http://example.org')
+ _, content = transport.request(http, 'http://example.org')
return content
def test_credentials_refresh_without_storage(self):
@@ -296,7 +313,7 @@ class SignedJwtAssertionCredentialsTests(unittest2.TestCase):
filehandle, filename = tempfile.mkstemp()
os.close(filehandle)
- store = file.Storage(filename)
+ store = file_module.Storage(filename)
store.put(credentials)
credentials.set_store(store)
@@ -310,19 +327,27 @@ class PEMSignedJwtAssertionCredentialsOpenSSLTests(
SignedJwtAssertionCredentialsTests):
def setUp(self):
+ self.orig_signer = crypt.Signer
self.format_ = 'pem'
crypt.Signer = crypt.OpenSSLSigner
+ def tearDown(self):
+ crypt.Signer = self.orig_signer
+
class PEMSignedJwtAssertionCredentialsPyCryptoTests(
SignedJwtAssertionCredentialsTests):
def setUp(self):
+ self.orig_signer = crypt.Signer
self.format_ = 'pem'
crypt.Signer = crypt.PyCryptoSigner
+ def tearDown(self):
+ crypt.Signer = self.orig_signer
+
-class TestHasOpenSSLFlag(unittest2.TestCase):
+class TestHasOpenSSLFlag(unittest.TestCase):
def test_true(self):
self.assertEqual(True, client.HAS_OPENSSL)
diff --git a/tests/test_service_account.py b/tests/test_service_account.py
index 699e699..6756d49 100644
--- a/tests/test_service_account.py
+++ b/tests/test_service_account.py
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-"""Oauth2client tests.
+"""oauth2client tests.
Unit tests for service account credentials implemented using RSA.
"""
@@ -21,17 +21,18 @@ import datetime
import json
import os
import tempfile
+import unittest
-import httplib2
import mock
import rsa
-from six import BytesIO
-import unittest2
+import six
+from six.moves import http_client
from oauth2client import client
from oauth2client import crypt
from oauth2client import service_account
-from .http_mock import HttpMockSequence
+from oauth2client import transport
+from tests import http_mock
def data_filename(filename):
@@ -43,9 +44,11 @@ def datafile(filename):
return file_obj.read()
-class ServiceAccountCredentialsTests(unittest2.TestCase):
+class ServiceAccountCredentialsTests(unittest.TestCase):
def setUp(self):
+ self.orig_signer = crypt.Signer
+ self.orig_verifier = crypt.Verifier
self.client_id = '123'
self.service_account_email = 'dummy@google.com'
self.private_key_id = 'ABCDEF'
@@ -59,6 +62,10 @@ class ServiceAccountCredentialsTests(unittest2.TestCase):
client_id=self.client_id,
)
+ def tearDown(self):
+ crypt.Signer = self.orig_signer
+ crypt.Verifier = self.orig_verifier
+
def test__to_json_override(self):
signer = object()
creds = service_account.ServiceAccountCredentials(
@@ -175,7 +182,7 @@ class ServiceAccountCredentialsTests(unittest2.TestCase):
scopes=scopes, token_uri=token_uri, revoke_uri=revoke_uri))
creds_from_file_contents = (
service_account.ServiceAccountCredentials.from_p12_keyfile_buffer(
- service_account_email, BytesIO(key_contents),
+ service_account_email, six.BytesIO(key_contents),
private_key_password=private_key_password,
scopes=scopes, token_uri=token_uri, revoke_uri=revoke_uri))
for creds in (creds_from_filename, creds_from_file_contents):
@@ -270,10 +277,10 @@ class ServiceAccountCredentialsTests(unittest2.TestCase):
utcnow.return_value = NOW
# Create a custom credentials with a mock signer.
- signer = mock.MagicMock()
+ signer = mock.Mock()
signed_value = b'signed-content'
- signer.sign = mock.MagicMock(name='sign',
- return_value=signed_value)
+ signer.sign = mock.Mock(name='sign',
+ return_value=signed_value)
credentials = service_account.ServiceAccountCredentials(
self.service_account_email,
signer,
@@ -296,10 +303,10 @@ class ServiceAccountCredentialsTests(unittest2.TestCase):
'access_token': token2,
'expires_in': lifetime,
}
- http = HttpMockSequence([
- ({'status': '200'},
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK},
json.dumps(token_response_first).encode('utf-8')),
- ({'status': '200'},
+ ({'status': http_client.OK},
json.dumps(token_response_second).encode('utf-8')),
])
@@ -362,6 +369,7 @@ class ServiceAccountCredentialsTests(unittest2.TestCase):
self.assertEqual(credentials.access_token, token2)
+
TOKEN_LIFE = service_account._JWTAccessCredentials._MAX_TOKEN_LIFETIME_SECS
T1 = 42
T1_DATE = datetime.datetime(1970, 1, 1, second=T1)
@@ -379,7 +387,7 @@ T3_EXPIRY = T3 + TOKEN_LIFE
T3_EXPIRY_DATE = T3_DATE + datetime.timedelta(seconds=TOKEN_LIFE)
-class JWTAccessCredentialsTests(unittest2.TestCase):
+class JWTAccessCredentialsTests(unittest.TestCase):
def setUp(self):
self.client_id = '123'
@@ -400,13 +408,15 @@ class JWTAccessCredentialsTests(unittest2.TestCase):
time.return_value = T1
token_info = self.jwt.get_access_token()
+ certs = {'key': datafile('public_cert.pem')}
payload = crypt.verify_signed_jwt_with_certs(
- token_info.access_token,
- {'key': datafile('public_cert.pem')}, audience=self.url)
+ token_info.access_token, certs, audience=self.url)
+ self.assertEqual(len(payload), 5)
self.assertEqual(payload['iss'], self.service_account_email)
self.assertEqual(payload['sub'], self.service_account_email)
self.assertEqual(payload['iat'], T1)
self.assertEqual(payload['exp'], T1_EXPIRY)
+ self.assertEqual(payload['aud'], self.url)
self.assertEqual(token_info.expires_in, T1_EXPIRY - T1)
# Verify that we vend the same token after 100 seconds
@@ -437,19 +447,20 @@ class JWTAccessCredentialsTests(unittest2.TestCase):
utcnow.return_value = T1_DATE
time.return_value = T1
- token_info = self.jwt.get_access_token(
- additional_claims={'aud': 'https://test2.url.com',
- 'sub': 'dummy2@google.com'
- })
+ audience = 'https://test2.url.com'
+ subject = 'dummy2@google.com'
+ claims = {'aud': audience, 'sub': subject}
+ token_info = self.jwt.get_access_token(additional_claims=claims)
+ certs = {'key': datafile('public_cert.pem')}
payload = crypt.verify_signed_jwt_with_certs(
- token_info.access_token,
- {'key': datafile('public_cert.pem')},
- audience='https://test2.url.com')
+ token_info.access_token, certs, audience=audience)
expires_in = token_info.expires_in
+ self.assertEqual(len(payload), 5)
self.assertEqual(payload['iss'], self.service_account_email)
- self.assertEqual(payload['sub'], 'dummy2@google.com')
+ self.assertEqual(payload['sub'], subject)
self.assertEqual(payload['iat'], T1)
self.assertEqual(payload['exp'], T1_EXPIRY)
+ self.assertEqual(payload['aud'], audience)
self.assertEqual(expires_in, T1_EXPIRY - T1)
def test_revoke(self):
@@ -474,30 +485,36 @@ class JWTAccessCredentialsTests(unittest2.TestCase):
utcnow.return_value = T1_DATE
time.return_value = T1
- def mock_request(uri, method='GET', body=None, headers=None,
- redirections=0, connection_type=None):
- self.assertEqual(uri, self.url)
- bearer, token = headers[b'Authorization'].split()
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK}, b''),
+ ({'status': http_client.OK}, b''),
+ ])
+
+ self.jwt.authorize(http)
+ transport.request(http, self.url)
+
+ # Ensure we use the cached token
+ utcnow.return_value = T2_DATE
+ transport.request(http, self.url)
+
+ # Verify mocks.
+ certs = {'key': datafile('public_cert.pem')}
+ self.assertEqual(len(http.requests), 2)
+ for info in http.requests:
+ self.assertEqual(info['method'], 'GET')
+ self.assertEqual(info['uri'], self.url)
+ self.assertIsNone(info['body'])
+ self.assertEqual(len(info['headers']), 1)
+ bearer, token = info['headers'][b'Authorization'].split()
+ self.assertEqual(bearer, b'Bearer')
payload = crypt.verify_signed_jwt_with_certs(
- token,
- {'key': datafile('public_cert.pem')},
- audience=self.url)
+ token, certs, audience=self.url)
+ self.assertEqual(len(payload), 5)
self.assertEqual(payload['iss'], self.service_account_email)
self.assertEqual(payload['sub'], self.service_account_email)
self.assertEqual(payload['iat'], T1)
self.assertEqual(payload['exp'], T1_EXPIRY)
- self.assertEqual(uri, self.url)
- self.assertEqual(bearer, b'Bearer')
- return (httplib2.Response({'status': '200'}), b'')
-
- h = httplib2.Http()
- h.request = mock_request
- self.jwt.authorize(h)
- h.request(self.url)
-
- # Ensure we use the cached token
- utcnow.return_value = T2_DATE
- h.request(self.url)
+ self.assertEqual(payload['aud'], self.url)
@mock.patch('oauth2client.client._UTCNOW')
@mock.patch('time.time')
@@ -509,65 +526,126 @@ class JWTAccessCredentialsTests(unittest2.TestCase):
self.service_account_email, self.signer,
private_key_id=self.private_key_id, client_id=self.client_id)
- def mock_request(uri, method='GET', body=None, headers=None,
- redirections=0, connection_type=None):
- self.assertEqual(uri, self.url)
- bearer, token = headers[b'Authorization'].split()
- payload = crypt.verify_signed_jwt_with_certs(
- token,
- {'key': datafile('public_cert.pem')},
- audience=self.url)
- self.assertEqual(payload['iss'], self.service_account_email)
- self.assertEqual(payload['sub'], self.service_account_email)
- self.assertEqual(payload['iat'], T1)
- self.assertEqual(payload['exp'], T1_EXPIRY)
- self.assertEqual(uri, self.url)
- self.assertEqual(bearer, b'Bearer')
- return httplib2.Response({'status': '200'}), b''
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK}, b''),
+ ])
- h = httplib2.Http()
- h.request = mock_request
- jwt.authorize(h)
- h.request(self.url)
+ jwt.authorize(http)
+ transport.request(http, self.url)
# Ensure we do not cache the token
self.assertIsNone(jwt.access_token)
+ # Verify mocks.
+ self.assertEqual(len(http.requests), 1)
+ info = http.requests[0]
+ self.assertEqual(info['method'], 'GET')
+ self.assertEqual(info['uri'], self.url)
+ self.assertIsNone(info['body'])
+ self.assertEqual(len(info['headers']), 1)
+ bearer, token = info['headers'][b'Authorization'].split()
+ self.assertEqual(bearer, b'Bearer')
+ certs = {'key': datafile('public_cert.pem')}
+ payload = crypt.verify_signed_jwt_with_certs(
+ token, certs, audience=self.url)
+ self.assertEqual(len(payload), 5)
+ self.assertEqual(payload['iss'], self.service_account_email)
+ self.assertEqual(payload['sub'], self.service_account_email)
+ self.assertEqual(payload['iat'], T1)
+ self.assertEqual(payload['exp'], T1_EXPIRY)
+ self.assertEqual(payload['aud'], self.url)
+
@mock.patch('oauth2client.client._UTCNOW')
def test_authorize_stale_token(self, utcnow):
utcnow.return_value = T1_DATE
# Create an initial token
- h = HttpMockSequence([({'status': '200'}, b''),
- ({'status': '200'}, b'')])
- self.jwt.authorize(h)
- h.request(self.url)
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK}, b''),
+ ({'status': http_client.OK}, b''),
+ ])
+ self.jwt.authorize(http)
+ transport.request(http, self.url)
token_1 = self.jwt.access_token
# Expire the token
utcnow.return_value = T3_DATE
- h.request(self.url)
+ transport.request(http, self.url)
token_2 = self.jwt.access_token
self.assertEquals(self.jwt.token_expiry, T3_EXPIRY_DATE)
self.assertNotEqual(token_1, token_2)
+ # Verify mocks.
+ certs = {'key': datafile('public_cert.pem')}
+ self.assertEqual(len(http.requests), 2)
+ issued_at_vals = (T1, T3)
+ exp_vals = (T1_EXPIRY, T3_EXPIRY)
+ for info, issued_at, exp_val in zip(http.requests, issued_at_vals,
+ exp_vals):
+ self.assertEqual(info['uri'], self.url)
+ self.assertEqual(info['method'], 'GET')
+ self.assertIsNone(info['body'])
+ self.assertEqual(len(info['headers']), 1)
+ bearer, token = info['headers'][b'Authorization'].split()
+ self.assertEqual(bearer, b'Bearer')
+ # To parse the token, skip the time check, since this
+ # test intentionally has stale tokens.
+ with mock.patch('oauth2client.crypt._verify_time_range',
+ return_value=True):
+ payload = crypt.verify_signed_jwt_with_certs(
+ token, certs, audience=self.url)
+ self.assertEqual(len(payload), 5)
+ self.assertEqual(payload['iss'], self.service_account_email)
+ self.assertEqual(payload['sub'], self.service_account_email)
+ self.assertEqual(payload['iat'], issued_at)
+ self.assertEqual(payload['exp'], exp_val)
+ self.assertEqual(payload['aud'], self.url)
+
@mock.patch('oauth2client.client._UTCNOW')
def test_authorize_401(self, utcnow):
utcnow.return_value = T1_DATE
- h = HttpMockSequence([
- ({'status': '200'}, b''),
- ({'status': '401'}, b''),
- ({'status': '200'}, b'')])
- self.jwt.authorize(h)
- h.request(self.url)
+ http = http_mock.HttpMockSequence([
+ ({'status': http_client.OK}, b''),
+ ({'status': http_client.UNAUTHORIZED}, b''),
+ ({'status': http_client.OK}, b''),
+ ])
+ self.jwt.authorize(http)
+ transport.request(http, self.url)
token_1 = self.jwt.access_token
utcnow.return_value = T2_DATE
- self.assertEquals(h.request(self.url)[0].status, 200)
+ response, _ = transport.request(http, self.url)
+ self.assertEquals(response.status, http_client.OK)
token_2 = self.jwt.access_token
# Check the 401 forced a new token
self.assertNotEqual(token_1, token_2)
+ # Verify mocks.
+ certs = {'key': datafile('public_cert.pem')}
+ self.assertEqual(len(http.requests), 3)
+ issued_at_vals = (T1, T1, T2)
+ exp_vals = (T1_EXPIRY, T1_EXPIRY, T2_EXPIRY)
+ for info, issued_at, exp_val in zip(http.requests, issued_at_vals,
+ exp_vals):
+ self.assertEqual(info['uri'], self.url)
+ self.assertEqual(info['method'], 'GET')
+ self.assertIsNone(info['body'])
+ self.assertEqual(len(info['headers']), 1)
+ bearer, token = info['headers'][b'Authorization'].split()
+ self.assertEqual(bearer, b'Bearer')
+ # To parse the token, skip the time check, since this
+ # test intentionally has stale tokens.
+ with mock.patch('oauth2client.crypt._verify_time_range',
+ return_value=True):
+ payload = crypt.verify_signed_jwt_with_certs(
+ token, certs, audience=self.url)
+ self.assertEqual(len(payload), 5)
+ self.assertEqual(payload['iss'], self.service_account_email)
+ self.assertEqual(payload['sub'], self.service_account_email)
+ self.assertEqual(payload['iat'], issued_at)
+ self.assertEqual(payload['exp'], exp_val)
+ self.assertEqual(payload['aud'], self.url)
+
@mock.patch('oauth2client.client._UTCNOW')
def test_refresh(self, utcnow):
utcnow.return_value = T1_DATE
diff --git a/tests/test_tools.py b/tests/test_tools.py
index 369f567..52191f0 100644
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -12,24 +12,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import argparse
import socket
import sys
import threading
+import unittest
import mock
from six.moves.urllib import request
-import unittest2
from oauth2client import client
from oauth2client import tools
-try:
- import argparse
-except ImportError: # pragma: NO COVER
- raise unittest2.SkipTest('argparase unavailable.')
-
-class TestClientRedirectServer(unittest2.TestCase):
+class TestClientRedirectServer(unittest.TestCase):
"""Test the ClientRedirectServer and ClientRedirectHandler classes."""
def test_ClientRedirectServer(self):
@@ -51,7 +47,7 @@ class TestClientRedirectServer(unittest2.TestCase):
self.assertEqual(httpd.query_params.get('code'), code)
-class TestRunFlow(unittest2.TestCase):
+class TestRunFlow(unittest.TestCase):
def setUp(self):
self.server = mock.Mock()
@@ -187,6 +183,6 @@ class TestRunFlow(unittest2.TestCase):
self.assertFalse(self.server.handle_request.called)
-class TestMessageIfMissing(unittest2.TestCase):
+class TestMessageIfMissing(unittest.TestCase):
def test_message_if_missing(self):
self.assertIn('somefile.txt', tools.message_if_missing('somefile.txt'))
diff --git a/tests/test_transport.py b/tests/test_transport.py
index e9782a8..2884200 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -12,15 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import unittest
+
import httplib2
import mock
-import unittest2
from oauth2client import client
from oauth2client import transport
+from tests import http_mock
-class TestMemoryCache(unittest2.TestCase):
+class TestMemoryCache(unittest.TestCase):
def test_get_set_delete(self):
cache = transport.MemoryCache()
@@ -32,7 +34,7 @@ class TestMemoryCache(unittest2.TestCase):
self.assertIsNone(cache.get('foo'))
-class Test_get_cached_http(unittest2.TestCase):
+class Test_get_cached_http(unittest.TestCase):
def test_global(self):
cached_http = transport.get_cached_http()
@@ -46,15 +48,22 @@ class Test_get_cached_http(unittest2.TestCase):
self.assertIs(result, cache)
-class Test_get_http_object(unittest2.TestCase):
+class Test_get_http_object(unittest.TestCase):
@mock.patch.object(httplib2, 'Http', return_value=object())
def test_it(self, http_klass):
result = transport.get_http_object()
self.assertEqual(result, http_klass.return_value)
+ http_klass.assert_called_once_with()
+
+ @mock.patch.object(httplib2, 'Http', return_value=object())
+ def test_with_args(self, http_klass):
+ result = transport.get_http_object(1, 2, foo='bar')
+ self.assertEqual(result, http_klass.return_value)
+ http_klass.assert_called_once_with(1, 2, foo='bar')
-class Test__initialize_headers(unittest2.TestCase):
+class Test__initialize_headers(unittest.TestCase):
def test_null(self):
result = transport._initialize_headers(None)
@@ -67,7 +76,7 @@ class Test__initialize_headers(unittest2.TestCase):
self.assertIsNot(result, headers)
-class Test__apply_user_agent(unittest2.TestCase):
+class Test__apply_user_agent(unittest.TestCase):
def test_null(self):
headers = object()
@@ -91,7 +100,7 @@ class Test__apply_user_agent(unittest2.TestCase):
self.assertEqual(result, {'user-agent': final_agent})
-class Test_clean_headers(unittest2.TestCase):
+class Test_clean_headers(unittest.TestCase):
def test_no_modify(self):
headers = {b'key': b'val'}
@@ -119,7 +128,7 @@ class Test_clean_headers(unittest2.TestCase):
self.assertEqual(result, header_str)
-class Test_wrap_http_for_auth(unittest2.TestCase):
+class Test_wrap_http_for_auth(unittest.TestCase):
def test_wrap(self):
credentials = object()
@@ -129,3 +138,45 @@ class Test_wrap_http_for_auth(unittest2.TestCase):
self.assertIsNone(result)
self.assertNotEqual(http.request, orig_req_method)
self.assertIs(http.request.credentials, credentials)
+
+
+class Test_request(unittest.TestCase):
+
+ uri = 'http://localhost'
+ method = 'POST'
+ body = 'abc'
+ redirections = 3
+
+ def test_with_request_attr(self):
+ mock_result = object()
+ headers = {'foo': 'bar'}
+ http = http_mock.HttpMock(headers=headers, data=mock_result)
+
+ response, content = transport.request(
+ http, self.uri, method=self.method, body=self.body,
+ redirections=self.redirections)
+ self.assertEqual(response, headers)
+ self.assertIs(content, mock_result)
+ # Verify mocks.
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, self.uri)
+ self.assertEqual(http.method, self.method)
+ self.assertEqual(http.body, self.body)
+ self.assertIsNone(http.headers)
+
+ def test_with_callable_http(self):
+ headers = {}
+ mock_result = object()
+ http = http_mock.HttpMock(headers=headers, data=mock_result)
+
+ result = transport.request(http, self.uri, method=self.method,
+ body=self.body,
+ redirections=self.redirections)
+ self.assertEqual(result, (headers, mock_result))
+ # Verify mock.
+ self.assertEqual(http.requests, 1)
+ self.assertEqual(http.uri, self.uri)
+ self.assertEqual(http.method, self.method)
+ self.assertEqual(http.body, self.body)
+ self.assertIsNone(http.headers)
+ self.assertEqual(http.redirections, self.redirections)
diff --git a/tests/test_util.py b/tests/test_util.py
deleted file mode 100644
index 533460f..0000000
--- a/tests/test_util.py
+++ /dev/null
@@ -1,122 +0,0 @@
-"""Unit tests for oauth2client.util."""
-
-import mock
-import unittest2
-
-from oauth2client import util
-
-
-__author__ = 'jcgregorio@google.com (Joe Gregorio)'
-
-
-class PositionalTests(unittest2.TestCase):
-
- def test_usage(self):
- util.positional_parameters_enforcement = util.POSITIONAL_EXCEPTION
-
- # 1 positional arg, 1 keyword-only arg.
- @util.positional(1)
- def fn(pos, kwonly=None):
- return True
-
- self.assertTrue(fn(1))
- self.assertTrue(fn(1, kwonly=2))
- with self.assertRaises(TypeError):
- fn(1, 2)
-
- # No positional, but a required keyword arg.
- @util.positional(0)
- def fn2(required_kw):
- return True
-
- self.assertTrue(fn2(required_kw=1))
- with self.assertRaises(TypeError):
- fn2(1)
-
- # Unspecified positional, should automatically figure out 1 positional
- # 1 keyword-only (same as first case above).
- @util.positional
- def fn3(pos, kwonly=None):
- return True
-
- self.assertTrue(fn3(1))
- self.assertTrue(fn3(1, kwonly=2))
- with self.assertRaises(TypeError):
- fn3(1, 2)
-
- @mock.patch('oauth2client.util.logger')
- def test_enforcement_warning(self, mock_logger):
- util.positional_parameters_enforcement = util.POSITIONAL_WARNING
-
- @util.positional(1)
- def fn(pos, kwonly=None):
- return True
-
- self.assertTrue(fn(1, 2))
- self.assertTrue(mock_logger.warning.called)
-
- @mock.patch('oauth2client.util.logger')
- def test_enforcement_ignore(self, mock_logger):
- util.positional_parameters_enforcement = util.POSITIONAL_IGNORE
-
- @util.positional(1)
- def fn(pos, kwonly=None):
- return True
-
- self.assertTrue(fn(1, 2))
- self.assertFalse(mock_logger.warning.called)
-
-
-class ScopeToStringTests(unittest2.TestCase):
-
- def test_iterables(self):
- cases = [
- ('', ''),
- ('', ()),
- ('', []),
- ('', ('',)),
- ('', ['', ]),
- ('a', ('a',)),
- ('b', ['b', ]),
- ('a b', ['a', 'b']),
- ('a b', ('a', 'b')),
- ('a b', 'a b'),
- ('a b', (s for s in ['a', 'b'])),
- ]
- for expected, case in cases:
- self.assertEqual(expected, util.scopes_to_string(case))
-
-
-class StringToScopeTests(unittest2.TestCase):
-
- def test_conversion(self):
- cases = [
- (['a', 'b'], ['a', 'b']),
- ('', []),
- ('a', ['a']),
- ('a b c d e f', ['a', 'b', 'c', 'd', 'e', 'f']),
- ]
-
- for case, expected in cases:
- self.assertEqual(expected, util.string_to_scopes(case))
-
-
-class AddQueryParameterTests(unittest2.TestCase):
-
- def test__add_query_parameter(self):
- self.assertEqual(
- util._add_query_parameter('/action', 'a', None),
- '/action')
- self.assertEqual(
- util._add_query_parameter('/action', 'a', 'b'),
- '/action?a=b')
- self.assertEqual(
- util._add_query_parameter('/action?a=b', 'a', 'c'),
- '/action?a=c')
- # Order is non-deterministic.
- self.assertIn(
- util._add_query_parameter('/action?a=b', 'c', 'd'),
- ['/action?a=b&c=d', '/action?c=d&a=b'])
- self.assertEqual(
- util._add_query_parameter('/action', 'a', ' ='),
- '/action?a=+%3D')
diff --git a/tox.ini b/tox.ini
index b0781a8..d725881 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py26,py27,py33,py34,py35,pypy,gae,cover
+envlist = flake8,py27,py34,py35,gae,cover
[testenv]
basedeps = mock>=1.3.0
@@ -7,98 +7,43 @@ basedeps = mock>=1.3.0
cryptography>=1.0
pyopenssl>=0.14
webtest
- nose
+ pytest
flask
- unittest2
sqlalchemy
fasteners
deps = {[testenv]basedeps}
django
keyring
+ jsonpickle
setenv =
pypy: with_gmp=no
DJANGO_SETTINGS_MODULE=tests.contrib.django_util.settings
-commands = nosetests --ignore-files=test_appengine\.py --ignore-files=test__appengine_ndb\.py {posargs}
+commands =
+ py.test {posargs}
[coverbase]
basepython = python2.7
commands =
- nosetests \
- --with-coverage \
- --cover-package=oauth2client \
- --cover-package=tests \
- --cover-erase \
- --cover-tests \
- --cover-branches \
- --ignore-files=test_appengine\.py \
- --ignore-files=test__appengine_ndb\.py
- nosetests \
- --with-coverage \
- --cover-package=oauth2client.contrib.appengine \
- --cover-package=oauth2client.contrib._appengine_ndb \
- --cover-package=tests.contrib.test_appengine \
- --cover-package=tests.contrib.test__appengine_ndb \
- --with-gae \
- --cover-tests \
- --cover-branches \
- --gae-application=tests/data \
- --gae-lib-root={env:GAE_PYTHONPATH:google_appengine} \
- --logging-level=INFO \
- tests/contrib/test_appengine.py \
- tests/contrib/test__appengine_ndb.py
+ py.test \
+ --cov=oauth2client \
+ --cov=tests
+ py.test \
+ --cov=oauth2client \
+ --cov=tests \
+ --cov-append \
+ --gae-sdk={env:GAE_PYTHONPATH:} \
+ tests/contrib/appengine
deps = {[testenv]deps}
coverage
- nosegae
-
-[testenv:py26]
-basepython =
- python2.6
-commands =
- nosetests \
- --ignore-files=test_appengine\.py \
- --ignore-files=test__appengine_ndb\.py \
- --ignore-files=test_keyring_storage\.py \
- --exclude-dir=oauth2client/contrib/django_util \
- --exclude-dir=tests/contrib/django_util \
- {posargs}
-deps = {[testenv]basedeps}
- nose-exclude
-
-[testenv:py33]
-basepython =
- python3.3
-commands =
- nosetests \
- --ignore-files=test_appengine\.py \
- --ignore-files=test__appengine_ndb\.py \
- --ignore-files=test_django_orm\.py \
- --ignore-files=test_django_settings\.py \
- --ignore-files=test_django_util\.py \
- --exclude-dir=oauth2client/contrib/django_util \
- --exclude-dir=tests/contrib/django_util \
- {posargs}
-deps = {[testenv]basedeps}
- keyring
- nose-exclude
+ pytest-cov
[testenv:cover]
basepython = {[coverbase]basepython}
commands =
{[coverbase]commands}
- coverage report --show-missing --cover-min-percentage=100
-deps =
- {[coverbase]deps}
-
-[testenv:coveralls]
-basepython = {[coverbase]basepython}
-commands =
- {[coverbase]commands}
- coverage report --show-missing
- coveralls
+ coverage report --show-missing --fail-under=100
deps =
{[coverbase]deps}
- coveralls
-passenv = {[testenv:system-tests]passenv}
[testenv:docs]
basepython = python2.7
@@ -114,15 +59,8 @@ commands = {toxinidir}/scripts/build_docs.sh
[testenv:gae]
basepython = python2.7
deps = {[testenv]basedeps}
- nosegae
commands =
- nosetests \
- --with-gae \
- --gae-lib-root={env:GAE_PYTHONPATH:google_appengine} \
- --gae-application=tests/data \
- --logging-level=INFO \
- tests/contrib/test_appengine.py \
- tests/contrib/test__appengine_ndb.py
+ py.test --gae-sdk={env:GAE_PYTHONPATH:} tests/contrib/appengine
[testenv:system-tests]
basepython =
@@ -133,7 +71,7 @@ deps =
pycrypto>=2.6
cryptography>=1.0
pyopenssl>=0.14
-passenv = GOOGLE_* OAUTH2CLIENT_* TRAVIS*
+passenv = GOOGLE_* OAUTH2CLIENT_* TRAVIS* encrypted_*
[testenv:system-tests3]
basepython =
@@ -153,7 +91,6 @@ commands =
python {toxinidir}/scripts/run_gce_system_tests.py
deps =
pycrypto>=2.6
- unittest2
passenv = {[testenv:system-tests]passenv}
[testenv:flake8]
@@ -163,16 +100,17 @@ deps =
flake8-import-order
[flake8]
-exclude = .tox,.git,./*.egg,build,
-application-import-names = oauth2client
+exclude = .tox,.git,./*.egg,build,.cache,env,__pycache__
+application-import-names = oauth2client, tests
putty-ignore =
# E402 module level import not at top of file
- # These files have needed configurations defined before import
+ # This file has needed configurations defined before import
docs/conf.py : E402
- tests/contrib/test_appengine.py : E402
- # Additionally, ignore E100 (imports in wrong order) for Django configuration
- tests/contrib/test_django_orm.py : E402,I100
# E501 line too long
# Ignore lines over 80 chars that include "http:" or "https:"
/http:/ : E501
/https:/ : E501
+ # E722 do not use bare except
+ # Existing sloppy usages.
+ oauth2client/crypt.py : E722
+ oauth2client/contrib/multiprocess_file_storage.py : E722