Increase coverage % for oauth2client.appengine.
Reviewed in http://codereview.appspot.com/5795070/.
diff --git a/Makefile b/Makefile
index 2ae75e3..9206715 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,14 @@
python runtests.py tests/test_schema.py
python runtests.py tests/test_oauth2client_appengine.py
+
+.PHONY: coverage
+coverage:
+ coverage erase
+ find tests -name "test_*.py" | xargs --max-args=1 coverage run -a runtests.py
+ coverage report
+ coverage html
+
.PHONY: docs
docs:
cd docs; ./build.sh
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index 1420279..b484c1e 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -448,7 +448,8 @@
Args:
filename: string, File name of client secrets.
- scope: string, Space separated list of scopes.
+ scope: string or list of strings, scope(s) of the credentials being
+ requested.
message: string, A friendly string to display to the user if the
clientsecrets file is missing or invalid. The message may contain HTML and
will be presented on the web interface for any method that uses the
@@ -479,7 +480,8 @@
Args:
filename: string, File name of client secrets.
- scope: string, Space separated list of scopes.
+ scope: string or list of strings, scope(s) of the credentials being
+ requested.
message: string, A friendly string to display to the user if the
clientsecrets file is missing or invalid. The message may contain HTML and
will be presented on the web interface for any method that uses the
diff --git a/oauth2client/client.py b/oauth2client/client.py
index 71c9d81..f6f4a1a 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -159,7 +159,8 @@
t = type(self)
d = copy.copy(self.__dict__)
for member in strip:
- del d[member]
+ if member in d:
+ del d[member]
if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
# Add in information we will need later to reconsistitue this instance.
@@ -203,6 +204,19 @@
from_json = getattr(kls, 'from_json')
return from_json(s)
+ @classmethod
+ def from_json(cls, s):
+ """Instantiate a Credentials object from a JSON description of it. The JSON
+ should have been produced by calling .to_json() on the object.
+
+ Args:
+ data: dict, A deserialized JSON object.
+
+ Returns:
+ An instance of a Credentials subclass.
+ """
+ return Credentials()
+
class Flow(object):
"""Base class for all Flow objects."""
diff --git a/runtests.py b/runtests.py
index cf97afc..9e695ea 100644
--- a/runtests.py
+++ b/runtests.py
@@ -27,9 +27,10 @@
def main():
- module = imp.load_source('test', sys.argv[1])
- test = unittest.TestLoader().loadTestsFromModule(module)
- result = unittest.TextTestRunner(verbosity=1).run(test)
+ for t in sys.argv[1:]:
+ module = imp.load_source('test', t)
+ test = unittest.TestLoader().loadTestsFromModule(module)
+ result = unittest.TextTestRunner(verbosity=1).run(test)
if __name__ == '__main__':
diff --git a/tests/data/client_secrets.json b/tests/data/client_secrets.json
new file mode 100644
index 0000000..dee5c6e
--- /dev/null
+++ b/tests/data/client_secrets.json
@@ -0,0 +1,9 @@
+{
+ "web": {
+ "client_id": "foo_client_id",
+ "client_secret": "foo_client_secret",
+ "redirect_uris": [],
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://accounts.google.com/o/oauth2/token"
+ }
+}
diff --git a/tests/data/unfilled_client_secrets.json b/tests/data/unfilled_client_secrets.json
new file mode 100644
index 0000000..a232f37
--- /dev/null
+++ b/tests/data/unfilled_client_secrets.json
@@ -0,0 +1,9 @@
+{
+ "web": {
+ "client_id": "[[INSERT CLIENT ID HERE]]",
+ "client_secret": "[[INSERT CLIENT SECRET HERE]]",
+ "redirect_uris": [],
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://accounts.google.com/o/oauth2/token"
+ }
+}
diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py
index 3e512a1..95ca066 100644
--- a/tests/test_oauth2client.py
+++ b/tests/test_oauth2client.py
@@ -39,7 +39,9 @@
from oauth2client.client import AccessTokenCredentialsError
from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import AssertionCredentials
+from oauth2client.client import Credentials
from oauth2client.client import FlowExchangeError
+from oauth2client.client import MemoryCache
from oauth2client.client import OAuth2Credentials
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.client import OOB_CALLBACK_URN
@@ -47,6 +49,14 @@
from oauth2client.client import _extract_id_token
+class CredentialsTests(unittest.TestCase):
+
+ def test_to_from_json(self):
+ credentials = Credentials()
+ json = credentials.to_json()
+ restored = Credentials.new_from_json(json)
+
+
class OAuth2CredentialsTests(unittest.TestCase):
def setUp(self):
@@ -71,6 +81,7 @@
http = self.credentials.authorize(http)
resp, content = http.request("http://example.com")
self.assertEqual('Bearer 1/3w', content['Authorization'])
+ self.assertFalse(self.credentials.access_token_expired)
def test_token_refresh_failure(self):
http = HttpMockSequence([
@@ -83,6 +94,7 @@
self.fail("should raise AccessTokenRefreshError exception")
except AccessTokenRefreshError:
pass
+ self.assertTrue(self.credentials.access_token_expired)
def test_non_401_error_response(self):
http = HttpMockSequence([
@@ -285,5 +297,17 @@
self.assertEqual(credentials.id_token, body)
+class MemoryCacheTests(unittest.TestCase):
+
+ def test_get_set_delete(self):
+ m = MemoryCache()
+ self.assertEqual(None, m.get('foo'))
+ self.assertEqual(None, m.delete('foo'))
+ m.set('foo', 'bar')
+ self.assertEqual('bar', m.get('foo'))
+ m.delete('foo')
+ self.assertEqual(None, m.get('foo'))
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/test_oauth2client_appengine.py b/tests/test_oauth2client_appengine.py
index 680cea2..a11c5e9 100644
--- a/tests/test_oauth2client_appengine.py
+++ b/tests/test_oauth2client_appengine.py
@@ -25,6 +25,7 @@
import base64
import datetime
import httplib2
+import os
import time
import unittest
import urlparse
@@ -42,30 +43,51 @@
from google.appengine.api import apiproxy_stub
from google.appengine.api import apiproxy_stub_map
from google.appengine.api import app_identity
-from google.appengine.api import users
from google.appengine.api import memcache
+from google.appengine.api import users
from google.appengine.api.memcache import memcache_stub
from google.appengine.ext import db
from google.appengine.ext import testbed
from google.appengine.runtime import apiproxy_errors
from oauth2client.anyjson import simplejson
from oauth2client.appengine import AppAssertionCredentials
+from oauth2client.appengine import FlowProperty
from oauth2client.appengine import CredentialsModel
from oauth2client.appengine import OAuth2Decorator
from oauth2client.appengine import OAuth2Handler
from oauth2client.appengine import StorageByKeyName
+from oauth2client.appengine import oauth2decorator_from_clientsecrets
from oauth2client.client import AccessTokenRefreshError
+from oauth2client.client import Credentials
from oauth2client.client import FlowExchangeError
from oauth2client.client import OAuth2Credentials
+from oauth2client.client import flow_from_clientsecrets
from webtest import TestApp
+DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+
+
+def datafile(filename):
+ return os.path.join(DATA_DIR, filename)
+
+
class UserMock(object):
"""Mock the app engine user service"""
+ def __call__(self):
+ return self
+
def user_id(self):
return 'foo_user'
+class UserNotLoggedInMock(object):
+ """Mock the app engine user service"""
+
+ def __call__(self):
+ return None
+
+
class Http2Mock(object):
"""Mock httplib2.Http"""
status = 200
@@ -131,12 +153,41 @@
apiproxy_stub_map.apiproxy.RegisterStub(
'memcache', memcache_stub.MemcacheServiceStub())
- scope = "http://www.googleapis.com/scope"
+ scope = ["http://www.googleapis.com/scope"]
credentials = AppAssertionCredentials(scope)
http = httplib2.Http()
credentials.refresh(http)
self.assertEqual('a_token_123', credentials.access_token)
+ json = credentials.to_json()
+ credentials = Credentials.new_from_json(json)
+ self.assertEqual(scope[0], credentials.scope)
+
+
+class TestFlowModel(db.Model):
+ flow = FlowProperty()
+
+
+class FlowPropertyTest(unittest.TestCase):
+
+ def setUp(self):
+ self.testbed = testbed.Testbed()
+ self.testbed.activate()
+ self.testbed.init_datastore_v3_stub()
+
+ def tearDown(self):
+ self.testbed.deactivate()
+
+ def test_flow_get_put(self):
+ instance = TestFlowModel(
+ flow=flow_from_clientsecrets(datafile('client_secrets.json'), 'foo'),
+ key_name='foo'
+ )
+ instance.put()
+ retrieved = TestFlowModel.get_by_key_name('foo')
+
+ self.assertEqual('foo_client_id', retrieved.flow.client_id)
+
def _http_request(*args, **kwargs):
resp = httplib2.Response({'status': '200'})
@@ -206,7 +257,6 @@
self.assertEqual(None, memcache.get('foo'))
-
class DecoratorTests(unittest.TestCase):
def setUp(self):
@@ -220,6 +270,10 @@
client_secret='foo_client_secret',
scope=['foo_scope', 'bar_scope'],
user_agent='foo')
+
+ self._finish_setup(decorator, user_mock=UserMock)
+
+ def _finish_setup(self, decorator, user_mock):
self.decorator = decorator
class TestRequiredHandler(webapp2.RequestHandler):
@@ -244,7 +298,7 @@
handler=TestAwareHandler, name='bar')],
debug=True)
self.app = TestApp(application)
- users.get_current_user = UserMock
+ users.get_current_user = user_mock()
self.httplib2_orig = httplib2.Http
httplib2.Http = Http2Mock
@@ -350,6 +404,16 @@
self.decorator.credentials.access_token)
+ def test_error_in_step2(self):
+ # An initial request to an oauth_aware decorated path should not redirect.
+ response = self.app.get('/bar_path/2012/01')
+ url = self.decorator.authorize_url()
+ response = self.app.get('/oauth2callback', {
+ 'error': 'BadStuffHappened'
+ })
+ self.assertEqual('200 OK', response.status)
+ self.assertTrue('BadStuffHappened' in response.body)
+
def test_kwargs_are_passed_to_underlying_flow(self):
decorator = OAuth2Decorator(client_id='foo_client_id',
client_secret='foo_client_secret',
@@ -362,6 +426,76 @@
self.assertEqual('foo_user_agent', decorator.flow.user_agent)
self.assertEqual(None, decorator.flow.params.get('user_agent', None))
+ def test_decorator_from_client_secrets(self):
+ decorator = oauth2decorator_from_clientsecrets(
+ datafile('client_secrets.json'),
+ scope=['foo_scope', 'bar_scope'])
+ self._finish_setup(decorator, user_mock=UserMock)
+
+ self.assertFalse(decorator._in_error)
+ self.decorator = decorator
+ self.test_required()
+ http = self.decorator.http()
+ self.assertEquals('foo_access_token', http.request.credentials.access_token)
+
+ def test_decorator_from_client_secrets_not_logged_in_required(self):
+ decorator = oauth2decorator_from_clientsecrets(
+ datafile('client_secrets.json'),
+ scope=['foo_scope', 'bar_scope'], message='NotLoggedInMessage')
+ self.decorator = decorator
+ self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
+
+ self.assertFalse(decorator._in_error)
+
+ # An initial request to an oauth_required decorated path should be a
+ # redirect to login.
+ response = self.app.get('/foo_path')
+ self.assertTrue(response.status.startswith('302'))
+ self.assertTrue('Login' in str(response))
+
+ def test_decorator_from_client_secrets_not_logged_in_aware(self):
+ decorator = oauth2decorator_from_clientsecrets(
+ datafile('client_secrets.json'),
+ scope=['foo_scope', 'bar_scope'], message='NotLoggedInMessage')
+ self.decorator = decorator
+ self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
+
+ # An initial request to an oauth_aware decorated path should be a
+ # redirect to login.
+ response = self.app.get('/bar_path/2012/03')
+ self.assertTrue(response.status.startswith('302'))
+ self.assertTrue('Login' in str(response))
+
+ def test_decorator_from_unfilled_client_secrets_required(self):
+ MESSAGE = 'File is missing'
+ decorator = oauth2decorator_from_clientsecrets(
+ datafile('unfilled_client_secrets.json'),
+ scope=['foo_scope', 'bar_scope'], message=MESSAGE)
+ self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
+ self.assertTrue(decorator._in_error)
+ self.assertEqual(MESSAGE, decorator._message)
+
+ # An initial request to an oauth_required decorated path should be an
+ # error message.
+ response = self.app.get('/foo_path')
+ self.assertTrue(response.status.startswith('200'))
+ self.assertTrue(MESSAGE in str(response))
+
+ def test_decorator_from_unfilled_client_secrets_aware(self):
+ MESSAGE = 'File is missing'
+ decorator = oauth2decorator_from_clientsecrets(
+ datafile('unfilled_client_secrets.json'),
+ scope=['foo_scope', 'bar_scope'], message=MESSAGE)
+ self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
+ self.assertTrue(decorator._in_error)
+ self.assertEqual(MESSAGE, decorator._message)
+
+ # An initial request to an oauth_aware decorated path should be an
+ # error message.
+ response = self.app.get('/bar_path/2012/03')
+ self.assertTrue(response.status.startswith('200'))
+ self.assertTrue(MESSAGE in str(response))
+
if __name__ == '__main__':
unittest.main()