Add robot helpers and a sample.
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index 71aa8f0..a9766c7 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -21,14 +21,29 @@
import httplib2
import pickle
+import time
+import base64
+import logging
+
+try: # pragma: no cover
+ import simplejson
+except ImportError: # pragma: no cover
+ try:
+ # Try to import from django, should work on App Engine
+ from django.utils import simplejson
+ except ImportError:
+ # Should work for Python2.6 and higher.
+ import json as simplejson
from client import AccessTokenRefreshError
+from client import AssertionCredentials
from client import Credentials
from client import Flow
from client import OAuth2WebServerFlow
from client import Storage
from google.appengine.api import memcache
from google.appengine.api import users
+from google.appengine.api.app_identity import app_identity
from google.appengine.ext import db
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import login_required
@@ -36,6 +51,76 @@
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
+
+class AppAssertionCredentials(AssertionCredentials):
+ """Credentials object for App Engine Assertion Grants
+
+ This object will allow an App Engine application to identify itself to Google
+ and other OAuth 2.0 servers that can verify assertions. It can be used for
+ the purpose of accessing data stored under an account assigned to the App
+ Engine application itself. The algorithm used for generating the assertion is
+ the Signed JSON Web Token (JWT) algorithm. Additional details can be found at
+ the following link:
+
+ http://self-issued.info/docs/draft-jones-json-web-token.html
+
+ This credential does not require a flow to instantiate because it represents
+ a two legged flow, and therefore has all of the required information to
+ generate and refresh its own access tokens.
+
+ AssertionFlowCredentials objects may be safely pickled and unpickled.
+ """
+
+ def __init__(self, scope, user_agent,
+ audience='https://accounts.google.com/o/oauth2/token',
+ assertion_type='http://oauth.net/grant_type/jwt/1.0/bearer',
+ token_uri='https://accounts.google.com/o/oauth2/token', **kwargs):
+ """Constructor for AppAssertionCredentials
+
+ Args:
+ scope: string, scope of the credentials being requested.
+ user_agent: string, The HTTP User-Agent to provide for this application.
+ audience: string, The audience, or verifier of the assertion. For
+ convenience defaults to Google's audience.
+ assertion_type: string, Type name that will identify the format of the
+ assertion string. For convience, defaults to the JSON Web Token (JWT)
+ assertion type string.
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ """
+ self.scope = scope
+ self.audience = audience
+ self.app_name = app_identity.get_service_account_name()
+
+ super(AppAssertionCredentials, self).__init__(
+ assertion_type,
+ user_agent,
+ token_uri)
+
+ def _generate_assertion(self):
+ header = {
+ 'typ': 'JWT',
+ 'alg': 'RS256',
+ }
+
+ now = int(time.time())
+ claims = {
+ 'aud': self.audience,
+ 'scope': self.scope,
+ 'iat': now,
+ 'exp': now + 3600,
+ 'iss': self.app_name,
+ }
+
+ jwt_components = [base64.b64encode(simplejson.dumps(seg))
+ for seg in [header, claims]]
+
+ base_str = ".".join(jwt_components)
+ key_name, signature = app_identity.sign_blob(base_str)
+ jwt_components.append(base64.b64encode(signature))
+ return ".".join(jwt_components)
+
+
class FlowProperty(db.Property):
"""App Engine datastore Property for Flow.
@@ -117,7 +202,7 @@
Args:
model: db.Model, model class
key_name: string, key name for the entity that has the credentials
- property_name: string, name of the property that is an CredentialsProperty
+ property_name: string, name of the property that is a CredentialsProperty
cache: memcache, a write-through cache to put in front of the datastore
"""
self._model = model
@@ -189,6 +274,7 @@
# in API calls
"""
+
def __init__(self, client_id, client_secret, scope, user_agent,
auth_uri='https://accounts.google.com/o/oauth2/auth',
token_uri='https://accounts.google.com/o/oauth2/token'):
@@ -205,8 +291,8 @@
token_uri: string, URI for token endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
"""
- self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
- auth_uri, token_uri)
+ self.flow = OAuth2WebServerFlow(client_id, client_secret, scope,
+ user_agent, auth_uri, token_uri)
self.credentials = None
self._request_handler = None
@@ -220,6 +306,7 @@
method: callable, to be decorated method of a webapp.RequestHandler
instance.
"""
+
def check_oauth(request_handler, *args):
user = users.get_current_user()
# Don't use @login_decorator as this could be used in a POST request.
@@ -255,6 +342,7 @@
method: callable, to be decorated method of a webapp.RequestHandler
instance.
"""
+
def setup_oauth(request_handler, *args):
user = users.get_current_user()
# Don't use @login_decorator as this could be used in a POST request.
@@ -308,10 +396,12 @@
error = self.request.get('error')
if error:
errormsg = self.request.get('error_description', error)
- self.response.out.write('The authorization request failed: %s' % errormsg)
+ self.response.out.write(
+ 'The authorization request failed: %s' % errormsg)
else:
user = users.get_current_user()
- flow = pickle.loads(memcache.get(user.user_id(), namespace=OAUTH2CLIENT_NAMESPACE))
+ flow = pickle.loads(memcache.get(user.user_id(),
+ namespace=OAUTH2CLIENT_NAMESPACE))
# This code should be ammended with application specific error
# handling. The following cases should be considered:
# 1. What if the flow doesn't exist in memcache? Or is corrupt?
@@ -328,6 +418,7 @@
application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
+
def main():
run_wsgi_app(application)
diff --git a/oauth2client/client.py b/oauth2client/client.py
index 3c59980..523a185 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -83,6 +83,7 @@
"""
_abstract()
+
class Flow(object):
"""Base class for all Flow objects."""
pass
@@ -94,7 +95,6 @@
Store and retrieve a single credential.
"""
-
def get(self):
"""Retrieve credential.
@@ -187,6 +187,26 @@
self.__dict__.update(state)
self.store = None
+ def _generate_refresh_request_body(self):
+ """Generate the body that will be used in the refresh request
+ """
+ body = urllib.urlencode({
+ 'grant_type': 'refresh_token',
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'refresh_token': self.refresh_token,
+ })
+ return body
+
+ def _generate_refresh_request_headers(self):
+ """Generate the headers that will be used in the refresh request
+ """
+ headers = {
+ 'user-agent': self.user_agent,
+ 'content-type': 'application/x-www-form-urlencoded',
+ }
+ return headers
+
def _refresh(self, http_request):
"""Refresh the access_token using the refresh_token.
@@ -194,16 +214,9 @@
http: An instance of httplib2.Http.request
or something that acts like it.
"""
- body = urllib.urlencode({
- 'grant_type': 'refresh_token',
- 'client_id': self.client_id,
- 'client_secret': self.client_secret,
- 'refresh_token' : self.refresh_token
- })
- headers = {
- 'user-agent': self.user_agent,
- 'content-type': 'application/x-www-form-urlencoded'
- }
+ body = self._generate_refresh_request_body()
+ headers = self._generate_refresh_request_headers()
+
logging.info("Refresing access_token")
resp, content = http_request(
self.token_uri, method='POST', body=body, headers=headers)
@@ -214,14 +227,14 @@
self.refresh_token = d.get('refresh_token', self.refresh_token)
if 'expires_in' in d:
self.token_expiry = datetime.timedelta(
- seconds = int(d['expires_in'])) + datetime.datetime.now()
+ seconds=int(d['expires_in'])) + datetime.datetime.now()
else:
self.token_expiry = None
if self.store is not None:
self.store(self)
else:
- # An {'error':...} response body means the token is expired or revoked, so
- # we flag the credentials as such.
+ # An {'error':...} response body means the token is expired or revoked,
+ # so we flag the credentials as such.
logging.error('Failed to retrieve access token: %s' % content)
error_msg = 'Invalid response %s.' % resp['status']
try:
@@ -232,7 +245,8 @@
if self.store is not None:
self.store(self)
else:
- logging.warning("Unable to store refreshed credentials, no Storage provided.")
+ logging.warning(
+ "Unable to store refreshed credentials, no Storage provided.")
except:
pass
raise AccessTokenRefreshError(error_msg)
@@ -266,6 +280,10 @@
def new_request(uri, method='GET', body=None, headers=None,
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
connection_type=None):
+ if not self.access_token:
+ logging.info("Attempting refresh to obtain initial access_token")
+ self._refresh(request_orig)
+
"""Modify the request headers to add the appropriate
Authorization header."""
if headers == None:
@@ -275,8 +293,10 @@
headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
else:
headers['user-agent'] = self.user_agent
+
resp, content = request_orig(uri, method, body, headers,
redirections, connection_type)
+
if resp.status == 401:
logging.info("Refreshing because we got a 401")
self._refresh(request_orig)
@@ -341,6 +361,57 @@
raise AccessTokenCredentialsError(
"The access_token is expired or invalid and can't be refreshed.")
+
+class AssertionCredentials(OAuth2Credentials):
+ """Abstract Credentials object used for OAuth 2.0 assertion grants
+
+ This credential does not require a flow to instantiate because it represents
+ a two legged flow, and therefore has all of the required information to
+ generate and refresh its own access tokens. It must be subclassed to
+ generate the appropriate assertion string.
+
+ AssertionCredentials objects may be safely pickled and unpickled.
+ """
+
+ def __init__(self, assertion_type, user_agent,
+ token_uri='https://accounts.google.com/o/oauth2/token', **kwargs):
+ """Constructor for AssertionFlowCredentials
+
+ Args:
+ assertion_type: string, assertion type that will be declared to the auth
+ server
+ user_agent: string, The HTTP User-Agent to provide for this application.
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ """
+ super(AssertionCredentials, self).__init__(
+ None,
+ None,
+ None,
+ None,
+ None,
+ token_uri,
+ user_agent)
+ self.assertion_type = assertion_type
+
+ def _generate_refresh_request_body(self):
+ assertion = self._generate_assertion()
+
+ body = urllib.urlencode({
+ 'assertion_type': self.assertion_type,
+ 'assertion': assertion,
+ 'grant_type': "assertion",
+ })
+
+ return body
+
+ def _generate_assertion(self):
+ """Generate the assertion string that will be used in the access token
+ request.
+ """
+ _abstract()
+
+
class OAuth2WebServerFlow(Flow):
"""Does the Web Server Flow for OAuth 2.0.
@@ -420,15 +491,16 @@
'client_secret': self.client_secret,
'code': code,
'redirect_uri': self.redirect_uri,
- 'scope': self.scope
+ 'scope': self.scope,
})
headers = {
'user-agent': self.user_agent,
- 'content-type': 'application/x-www-form-urlencoded'
+ 'content-type': 'application/x-www-form-urlencoded',
}
if http is None:
http = httplib2.Http()
- resp, content = http.request(self.token_uri, method='POST', body=body, headers=headers)
+ resp, content = http.request(self.token_uri, method='POST', body=body,
+ headers=headers)
if resp.status == 200:
# TODO(jcgregorio) Raise an error if simplejson.loads fails?
d = simplejson.loads(content)
@@ -436,12 +508,13 @@
refresh_token = d.get('refresh_token', None)
token_expiry = None
if 'expires_in' in d:
- token_expiry = datetime.datetime.now() + datetime.timedelta(seconds = int(d['expires_in']))
+ token_expiry = datetime.datetime.now() + datetime.timedelta(
+ seconds=int(d['expires_in']))
logging.info('Successfully retrieved access token: %s' % content)
- return OAuth2Credentials(access_token, self.client_id, self.client_secret,
- refresh_token, token_expiry, self.token_uri,
- self.user_agent)
+ return OAuth2Credentials(access_token, self.client_id,
+ self.client_secret, refresh_token, token_expiry,
+ self.token_uri, self.user_agent)
else:
logging.error('Failed to retrieve access token: %s' % content)
error_msg = 'Invalid response %s.' % resp['status']
diff --git a/samples/appengine_with_robots/apiclient b/samples/appengine_with_robots/apiclient
new file mode 120000
index 0000000..24fe0bc
--- /dev/null
+++ b/samples/appengine_with_robots/apiclient
@@ -0,0 +1 @@
+../appengine/apiclient
\ No newline at end of file
diff --git a/samples/appengine_with_robots/app.yaml b/samples/appengine_with_robots/app.yaml
new file mode 100644
index 0000000..e83ac03
--- /dev/null
+++ b/samples/appengine_with_robots/app.yaml
@@ -0,0 +1,9 @@
+application: urlshortener-robot
+version: 2
+runtime: python
+api_version: 1
+
+handlers:
+- url: .*
+ script: main.py
+
diff --git a/samples/appengine_with_robots/gflags.py b/samples/appengine_with_robots/gflags.py
new file mode 120000
index 0000000..5a2ff94
--- /dev/null
+++ b/samples/appengine_with_robots/gflags.py
@@ -0,0 +1 @@
+../../gflags.py
\ No newline at end of file
diff --git a/samples/appengine_with_robots/gflags_validators.py b/samples/appengine_with_robots/gflags_validators.py
new file mode 120000
index 0000000..25d8ce8
--- /dev/null
+++ b/samples/appengine_with_robots/gflags_validators.py
@@ -0,0 +1 @@
+../../gflags_validators.py
\ No newline at end of file
diff --git a/samples/appengine_with_robots/httplib2 b/samples/appengine_with_robots/httplib2
new file mode 120000
index 0000000..4cd2774
--- /dev/null
+++ b/samples/appengine_with_robots/httplib2
@@ -0,0 +1 @@
+../appengine/httplib2
\ No newline at end of file
diff --git a/samples/appengine_with_robots/main.py b/samples/appengine_with_robots/main.py
new file mode 100644
index 0000000..01ee3a8
--- /dev/null
+++ b/samples/appengine_with_robots/main.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+#
+# Copyright 2007 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.
+#
+"""Starting template for Google App Engine applications.
+
+Use this project as a starting point if you are just beginning to build a
+Google App Engine project which will access and manage data held under a role
+account for the App Engine app. More information about using Google App Engine
+apps to call Google APIs can be found in Scenario 1 of the following document:
+
+<https://sites.google.com/site/oauthgoog/Home/google-oauth2-assertion-flow>
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+import httplib2
+import logging
+import os
+import pickle
+
+from apiclient.discovery import build
+from google.appengine.api import memcache
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+from google.appengine.ext.webapp.util import run_wsgi_app
+from oauth2client.appengine import AppAssertionCredentials
+
+credentials = AppAssertionCredentials(
+ scope='https://www.googleapis.com/auth/urlshortener',
+ user_agent='my-sample-app/1.0')
+
+http = credentials.authorize(httplib2.Http(memcache))
+service = build("urlshortener", "v1", http=http)
+
+
+class MainHandler(webapp.RequestHandler):
+
+ def get(self):
+ path = os.path.join(os.path.dirname(__file__), 'welcome.html')
+ shortened = service.url().list().execute()
+ short_and_long = [(item["id"], item["longUrl"]) for item in
+ shortened["items"]]
+
+ variables = {
+ 'short_and_long': short_and_long,
+ }
+ self.response.out.write(template.render(path, variables))
+
+ def post(self):
+ long_url = self.request.get("longUrl")
+ shortened = service.url().insert(body={"longUrl": long_url}).execute()
+ self.redirect("/")
+
+
+def main():
+ application = webapp.WSGIApplication(
+ [
+ ('/', MainHandler),
+ ],
+ debug=True)
+ run_wsgi_app(application)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/samples/appengine_with_robots/oauth2 b/samples/appengine_with_robots/oauth2
new file mode 120000
index 0000000..ee61c25
--- /dev/null
+++ b/samples/appengine_with_robots/oauth2
@@ -0,0 +1 @@
+../appengine/oauth2
\ No newline at end of file
diff --git a/samples/appengine_with_robots/oauth2client b/samples/appengine_with_robots/oauth2client
new file mode 120000
index 0000000..9013119
--- /dev/null
+++ b/samples/appengine_with_robots/oauth2client
@@ -0,0 +1 @@
+../../oauth2client/
\ No newline at end of file
diff --git a/samples/appengine_with_robots/uritemplate b/samples/appengine_with_robots/uritemplate
new file mode 120000
index 0000000..1c98e41
--- /dev/null
+++ b/samples/appengine_with_robots/uritemplate
@@ -0,0 +1 @@
+../appengine/uritemplate
\ No newline at end of file
diff --git a/samples/appengine_with_robots/welcome.html b/samples/appengine_with_robots/welcome.html
new file mode 100644
index 0000000..a035ce8
--- /dev/null
+++ b/samples/appengine_with_robots/welcome.html
@@ -0,0 +1,18 @@
+<html>
+ <head>
+ <title>Welcome</title>
+ </head>
+ <body>
+ <form action="." method="post">
+ Long Url: <input name="longUrl" type="text" />
+ <input type="submit" />
+ </form>
+
+ <table>
+ <tr><th>Shortened</th><th>Original</th></tr>
+ {% for item in short_and_long %}
+ <tr><td><a href="{{ item.0 }}">{{ item.0 }}</a></td><td><a href="{{ item.1 }}">{{ item.1 }}</a></td></tr>
+ {% endfor %}
+ </table>
+ </body>
+</html>
diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py
index 3c6e0ba..417807a 100644
--- a/tests/test_oauth2client.py
+++ b/tests/test_oauth2client.py
@@ -35,6 +35,7 @@
from oauth2client.client import AccessTokenCredentials
from oauth2client.client import AccessTokenCredentialsError
from oauth2client.client import AccessTokenRefreshError
+from oauth2client.client import AssertionCredentials
from oauth2client.client import FlowExchangeError
from oauth2client.client import OAuth2Credentials
from oauth2client.client import OAuth2WebServerFlow
@@ -115,6 +116,35 @@
self.assertEqual(400, resp.status)
+class TestAssertionCredentials(unittest.TestCase):
+ assertion_text = "This is the assertion"
+ assertion_type = "http://www.google.com/assertionType"
+
+ class AssertionCredentialsTestImpl(AssertionCredentials):
+
+ def _generate_assertion(self):
+ return TestAssertionCredentials.assertion_text
+
+ def setUp(self):
+ user_agent = "fun/2.0"
+ self.credentials = self.AssertionCredentialsTestImpl(self.assertion_type,
+ user_agent)
+
+ def test_assertion_body(self):
+ body = urlparse.parse_qs(self.credentials._generate_refresh_request_body())
+ self.assertEqual(body['assertion'][0], self.assertion_text)
+ self.assertEqual(body['assertion_type'][0], self.assertion_type)
+
+ def test_assertion_refresh(self):
+ http = HttpMockSequence([
+ ({'status': '200'}, '{"access_token":"1/3w"}'),
+ ({'status': '200'}, 'echo_request_headers'),
+ ])
+ http = self.credentials.authorize(http)
+ resp, content = http.request("http://example.com")
+ self.assertEqual(content['authorization'], 'OAuth 1/3w')
+
+
class OAuth2WebServerFlowTest(unittest.TestCase):
def setUp(self):
@@ -137,7 +167,7 @@
def test_exchange_failure(self):
http = HttpMockSequence([
- ({'status': '400'}, '{"error":"invalid_request"}')
+ ({'status': '400'}, '{"error":"invalid_request"}'),
])
try:
@@ -159,7 +189,6 @@
self.assertNotEqual(credentials.token_expiry, None)
self.assertEqual(credentials.refresh_token, '8xLOxBtZp8')
-
def test_exchange_no_expires_in(self):
http = HttpMockSequence([
({'status': '200'}, """{ "access_token":"SlAV32hkKG",
diff --git a/tests/test_oauth2client_appengine.py b/tests/test_oauth2client_appengine.py
index 964092d..7f370b3 100644
--- a/tests/test_oauth2client_appengine.py
+++ b/tests/test_oauth2client_appengine.py
@@ -22,6 +22,7 @@
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+import base64
import httplib2
import unittest
import urlparse
@@ -31,20 +32,24 @@
except ImportError:
from cgi import parse_qs
-from apiclient.http import HttpMockSequence
from apiclient.anyjson import simplejson
-from webtest import TestApp
+from apiclient.http import HttpMockSequence
+from google.appengine.api import apiproxy_stub
+from google.appengine.api import apiproxy_stub_map
+from google.appengine.api import users
+from google.appengine.ext import testbed
+from google.appengine.ext import webapp
from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import FlowExchangeError
+from oauth2client.appengine import AppAssertionCredentials
from oauth2client.appengine import OAuth2Decorator
-from google.appengine.ext import webapp
-from google.appengine.api import users
from oauth2client.appengine import OAuth2Handler
-from google.appengine.ext import testbed
+from webtest import TestApp
class UserMock(object):
"""Mock the app engine user service"""
+
def user_id(self):
return 'foo_user'
@@ -55,7 +60,7 @@
content = {
'access_token': 'foo_access_token',
'refresh_token': 'foo_refresh_token',
- 'expires_in': 3600
+ 'expires_in': 3600,
}
def request(self, token_uri, method, body, headers, *args, **kwargs):
@@ -64,6 +69,59 @@
return (self, simplejson.dumps(self.content))
+class TestAppAssertionCredentials(unittest.TestCase):
+ account_name = "service_account_name@appspot.com"
+ signature = "signature"
+
+ class AppIdentityStubImpl(apiproxy_stub.APIProxyStub):
+
+ def __init__(self):
+ super(TestAppAssertionCredentials.AppIdentityStubImpl, self).__init__(
+ 'app_identity_service')
+
+ def _Dynamic_GetServiceAccountName(self, request, response):
+ return response.set_service_account_name(
+ TestAppAssertionCredentials.account_name)
+
+ def _Dynamic_SignForApp(self, request, response):
+ return response.set_signature_bytes(
+ TestAppAssertionCredentials.signature)
+
+ def setUp(self):
+ app_identity_stub = self.AppIdentityStubImpl()
+ apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
+ app_identity_stub)
+
+ self.scope = "http://www.googleapis.com/scope"
+ user_agent = "hal/3.0"
+
+ self.credentials = AppAssertionCredentials(self.scope, user_agent)
+
+ def test_assertion(self):
+ assertion = self.credentials._generate_assertion()
+
+ parts = assertion.split(".")
+ self.assertTrue(len(parts) == 3)
+
+ header, body, signature = [base64.b64decode(part) for part in parts]
+
+ header_dict = simplejson.loads(header)
+ self.assertEqual(header_dict['typ'], 'JWT')
+ self.assertEqual(header_dict['alg'], 'RS256')
+
+ body_dict = simplejson.loads(body)
+ self.assertEqual(body_dict['aud'],
+ 'https://accounts.google.com/o/oauth2/token')
+ self.assertEqual(body_dict['scope'], self.scope)
+ self.assertEqual(body_dict['iss'], self.account_name)
+
+ issuedAt = body_dict['iat']
+ self.assertTrue(issuedAt > 0)
+ self.assertEqual(body_dict['exp'], issuedAt + 3600)
+
+ self.assertEqual(signature, self.signature)
+
+
class DecoratorTests(unittest.TestCase):
def setUp(self):
@@ -79,14 +137,14 @@
user_agent='foo_user_agent')
self.decorator = decorator
-
class TestRequiredHandler(webapp.RequestHandler):
+
@decorator.oauth_required
def get(self):
pass
-
class TestAwareHandler(webapp.RequestHandler):
+
@decorator.oauth_aware
def get(self):
self.response.out.write('Hello World!')
@@ -121,7 +179,7 @@
# Now simulate the callback to /oauth2callback
response = self.app.get('/oauth2callback', {
'code': 'foo_access_code',
- 'state': 'foo_path'
+ 'state': 'foo_path',
})
self.assertEqual('http://localhost/foo_path', response.headers['Location'])
self.assertEqual(None, self.decorator.credentials)
@@ -130,8 +188,10 @@
response = self.app.get('/foo_path')
self.assertEqual('200 OK', response.status)
self.assertEqual(True, self.decorator.has_credentials())
- self.assertEqual('foo_refresh_token', self.decorator.credentials.refresh_token)
- self.assertEqual('foo_access_token', self.decorator.credentials.access_token)
+ self.assertEqual('foo_refresh_token',
+ self.decorator.credentials.refresh_token)
+ self.assertEqual('foo_access_token',
+ self.decorator.credentials.access_token)
# Invalidate the stored Credentials
self.decorator.credentials._invalid = True
@@ -161,7 +221,7 @@
url = self.decorator.authorize_url()
response = self.app.get('/oauth2callback', {
'code': 'foo_access_code',
- 'state': 'bar_path'
+ 'state': 'bar_path',
})
self.assertEqual('http://localhost/bar_path', response.headers['Location'])
self.assertEqual(False, self.decorator.has_credentials())
@@ -171,8 +231,10 @@
self.assertEqual('200 OK', response.status)
self.assertEqual('Hello World!', response.body)
self.assertEqual(True, self.decorator.has_credentials())
- self.assertEqual('foo_refresh_token', self.decorator.credentials.refresh_token)
- self.assertEqual('foo_access_token', self.decorator.credentials.access_token)
+ self.assertEqual('foo_refresh_token',
+ self.decorator.credentials.refresh_token)
+ self.assertEqual('foo_access_token',
+ self.decorator.credentials.access_token)
if __name__ == '__main__':
unittest.main()