blob: 90ee33598be8b75ad29128aa3d09d74aa794ffbb [file] [log] [blame]
Joe Gregorio432f17e2011-05-22 23:18:00 -04001#!/usr/bin/python2.4
2#
3# Copyright 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17
18"""Discovery document tests
19
20Unit tests for objects created from discovery documents.
21"""
22
23__author__ = 'jcgregorio@google.com (Joe Gregorio)'
24
JacobMoshenko8e905102011-06-20 09:53:10 -040025import base64
Joe Gregorio432f17e2011-05-22 23:18:00 -040026import httplib2
27import unittest
28import urlparse
29
30try:
31 from urlparse import parse_qs
32except ImportError:
33 from cgi import parse_qs
34
Joe Gregorio8b4c1732011-12-06 11:28:29 -050035import dev_appserver
36dev_appserver.fix_sys_path()
37
JacobMoshenko8e905102011-06-20 09:53:10 -040038from apiclient.http import HttpMockSequence
39from google.appengine.api import apiproxy_stub
40from google.appengine.api import apiproxy_stub_map
41from google.appengine.api import users
42from google.appengine.ext import testbed
43from google.appengine.ext import webapp
Joe Gregorio549230c2012-01-11 10:38:05 -050044from oauth2client.anyjson import simplejson
JacobMoshenko8e905102011-06-20 09:53:10 -040045from oauth2client.appengine import AppAssertionCredentials
Joe Gregorio432f17e2011-05-22 23:18:00 -040046from oauth2client.appengine import OAuth2Decorator
Joe Gregorio432f17e2011-05-22 23:18:00 -040047from oauth2client.appengine import OAuth2Handler
Joe Gregorio549230c2012-01-11 10:38:05 -050048from oauth2client.client import AccessTokenRefreshError
49from oauth2client.client import FlowExchangeError
JacobMoshenko8e905102011-06-20 09:53:10 -040050from webtest import TestApp
Joe Gregorio432f17e2011-05-22 23:18:00 -040051
52
53class UserMock(object):
54 """Mock the app engine user service"""
JacobMoshenko8e905102011-06-20 09:53:10 -040055
Joe Gregorio432f17e2011-05-22 23:18:00 -040056 def user_id(self):
57 return 'foo_user'
58
59
60class Http2Mock(object):
61 """Mock httplib2.Http"""
62 status = 200
63 content = {
64 'access_token': 'foo_access_token',
65 'refresh_token': 'foo_refresh_token',
JacobMoshenko8e905102011-06-20 09:53:10 -040066 'expires_in': 3600,
Joe Gregorio432f17e2011-05-22 23:18:00 -040067 }
68
69 def request(self, token_uri, method, body, headers, *args, **kwargs):
70 self.body = body
71 self.headers = headers
72 return (self, simplejson.dumps(self.content))
73
74
JacobMoshenko8e905102011-06-20 09:53:10 -040075class TestAppAssertionCredentials(unittest.TestCase):
76 account_name = "service_account_name@appspot.com"
77 signature = "signature"
78
79 class AppIdentityStubImpl(apiproxy_stub.APIProxyStub):
80
81 def __init__(self):
82 super(TestAppAssertionCredentials.AppIdentityStubImpl, self).__init__(
83 'app_identity_service')
84
85 def _Dynamic_GetServiceAccountName(self, request, response):
86 return response.set_service_account_name(
87 TestAppAssertionCredentials.account_name)
88
89 def _Dynamic_SignForApp(self, request, response):
90 return response.set_signature_bytes(
91 TestAppAssertionCredentials.signature)
92
93 def setUp(self):
94 app_identity_stub = self.AppIdentityStubImpl()
95 apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
96 app_identity_stub)
97
98 self.scope = "http://www.googleapis.com/scope"
JacobMoshenkocb6d8912011-07-08 13:35:15 -040099 self.credentials = AppAssertionCredentials(self.scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400100
101 def test_assertion(self):
102 assertion = self.credentials._generate_assertion()
103
104 parts = assertion.split(".")
105 self.assertTrue(len(parts) == 3)
106
107 header, body, signature = [base64.b64decode(part) for part in parts]
108
109 header_dict = simplejson.loads(header)
110 self.assertEqual(header_dict['typ'], 'JWT')
111 self.assertEqual(header_dict['alg'], 'RS256')
112
113 body_dict = simplejson.loads(body)
114 self.assertEqual(body_dict['aud'],
115 'https://accounts.google.com/o/oauth2/token')
116 self.assertEqual(body_dict['scope'], self.scope)
117 self.assertEqual(body_dict['iss'], self.account_name)
118
119 issuedAt = body_dict['iat']
120 self.assertTrue(issuedAt > 0)
121 self.assertEqual(body_dict['exp'], issuedAt + 3600)
122
123 self.assertEqual(signature, self.signature)
124
125
Joe Gregorio432f17e2011-05-22 23:18:00 -0400126class DecoratorTests(unittest.TestCase):
127
128 def setUp(self):
129 self.testbed = testbed.Testbed()
130 self.testbed.activate()
131 self.testbed.init_datastore_v3_stub()
132 self.testbed.init_memcache_stub()
133 self.testbed.init_user_stub()
134
135 decorator = OAuth2Decorator(client_id='foo_client_id',
136 client_secret='foo_client_secret',
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400137 scope=['foo_scope', 'bar_scope'])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400138 self.decorator = decorator
139
Joe Gregorio432f17e2011-05-22 23:18:00 -0400140 class TestRequiredHandler(webapp.RequestHandler):
JacobMoshenko8e905102011-06-20 09:53:10 -0400141
Joe Gregorio432f17e2011-05-22 23:18:00 -0400142 @decorator.oauth_required
143 def get(self):
144 pass
145
Joe Gregorio432f17e2011-05-22 23:18:00 -0400146 class TestAwareHandler(webapp.RequestHandler):
JacobMoshenko8e905102011-06-20 09:53:10 -0400147
Joe Gregorio432f17e2011-05-22 23:18:00 -0400148 @decorator.oauth_aware
149 def get(self):
150 self.response.out.write('Hello World!')
151
152
153 application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler),
154 ('/foo_path', TestRequiredHandler),
155 ('/bar_path', TestAwareHandler)],
156 debug=True)
157 self.app = TestApp(application)
158 users.get_current_user = UserMock
Joe Gregorio922b78c2011-05-26 21:36:34 -0400159 self.httplib2_orig = httplib2.Http
Joe Gregorio432f17e2011-05-22 23:18:00 -0400160 httplib2.Http = Http2Mock
161
162 def tearDown(self):
163 self.testbed.deactivate()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400164 httplib2.Http = self.httplib2_orig
Joe Gregorio432f17e2011-05-22 23:18:00 -0400165
166 def test_required(self):
167 # An initial request to an oauth_required decorated path should be a
168 # redirect to start the OAuth dance.
169 response = self.app.get('/foo_path')
170 self.assertTrue(response.status.startswith('302'))
171 q = parse_qs(response.headers['Location'].split('?', 1)[1])
172 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
173 self.assertEqual('foo_client_id', q['client_id'][0])
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400174 self.assertEqual('foo_scope bar_scope', q['scope'][0])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400175 self.assertEqual('http://localhost/foo_path', q['state'][0])
176 self.assertEqual('code', q['response_type'][0])
177 self.assertEqual(False, self.decorator.has_credentials())
178
Joe Gregorio562b7312011-09-15 09:06:38 -0400179 # Now simulate the callback to /oauth2callback.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400180 response = self.app.get('/oauth2callback', {
181 'code': 'foo_access_code',
JacobMoshenko8e905102011-06-20 09:53:10 -0400182 'state': 'foo_path',
Joe Gregorio432f17e2011-05-22 23:18:00 -0400183 })
184 self.assertEqual('http://localhost/foo_path', response.headers['Location'])
185 self.assertEqual(None, self.decorator.credentials)
186
Joe Gregorio562b7312011-09-15 09:06:38 -0400187 # Now requesting the decorated path should work.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400188 response = self.app.get('/foo_path')
189 self.assertEqual('200 OK', response.status)
190 self.assertEqual(True, self.decorator.has_credentials())
JacobMoshenko8e905102011-06-20 09:53:10 -0400191 self.assertEqual('foo_refresh_token',
192 self.decorator.credentials.refresh_token)
193 self.assertEqual('foo_access_token',
194 self.decorator.credentials.access_token)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400195
Joe Gregorio562b7312011-09-15 09:06:38 -0400196 # Invalidate the stored Credentials.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400197 self.decorator.credentials.invalid = True
198 self.decorator.credentials.store.put(self.decorator.credentials)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400199
Joe Gregorio562b7312011-09-15 09:06:38 -0400200 # Invalid Credentials should start the OAuth dance again.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400201 response = self.app.get('/foo_path')
202 self.assertTrue(response.status.startswith('302'))
203 q = parse_qs(response.headers['Location'].split('?', 1)[1])
204 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
205
Joe Gregorioec75dc12012-02-06 13:40:42 -0500206 def test_storage_delete(self):
207 # An initial request to an oauth_required decorated path should be a
208 # redirect to start the OAuth dance.
209 response = self.app.get('/foo_path')
210 self.assertTrue(response.status.startswith('302'))
211
212 # Now simulate the callback to /oauth2callback.
213 response = self.app.get('/oauth2callback', {
214 'code': 'foo_access_code',
215 'state': 'foo_path',
216 })
217 self.assertEqual('http://localhost/foo_path', response.headers['Location'])
218 self.assertEqual(None, self.decorator.credentials)
219
220 # Now requesting the decorated path should work.
221 response = self.app.get('/foo_path')
222
223 # Invalidate the stored Credentials.
224 self.decorator.credentials.store.delete()
225
226 # Invalid Credentials should start the OAuth dance again.
227 response = self.app.get('/foo_path')
228 self.assertTrue(response.status.startswith('302'))
229
Joe Gregorio432f17e2011-05-22 23:18:00 -0400230 def test_aware(self):
Joe Gregorio562b7312011-09-15 09:06:38 -0400231 # An initial request to an oauth_aware decorated path should not redirect.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400232 response = self.app.get('/bar_path')
233 self.assertEqual('Hello World!', response.body)
234 self.assertEqual('200 OK', response.status)
235 self.assertEqual(False, self.decorator.has_credentials())
236 url = self.decorator.authorize_url()
237 q = parse_qs(url.split('?', 1)[1])
238 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
239 self.assertEqual('foo_client_id', q['client_id'][0])
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400240 self.assertEqual('foo_scope bar_scope', q['scope'][0])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400241 self.assertEqual('http://localhost/bar_path', q['state'][0])
242 self.assertEqual('code', q['response_type'][0])
243
Joe Gregorio562b7312011-09-15 09:06:38 -0400244 # Now simulate the callback to /oauth2callback.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400245 url = self.decorator.authorize_url()
246 response = self.app.get('/oauth2callback', {
247 'code': 'foo_access_code',
JacobMoshenko8e905102011-06-20 09:53:10 -0400248 'state': 'bar_path',
Joe Gregorio432f17e2011-05-22 23:18:00 -0400249 })
250 self.assertEqual('http://localhost/bar_path', response.headers['Location'])
251 self.assertEqual(False, self.decorator.has_credentials())
252
Joe Gregorio562b7312011-09-15 09:06:38 -0400253 # Now requesting the decorated path will have credentials.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400254 response = self.app.get('/bar_path')
255 self.assertEqual('200 OK', response.status)
256 self.assertEqual('Hello World!', response.body)
257 self.assertEqual(True, self.decorator.has_credentials())
JacobMoshenko8e905102011-06-20 09:53:10 -0400258 self.assertEqual('foo_refresh_token',
259 self.decorator.credentials.refresh_token)
260 self.assertEqual('foo_access_token',
261 self.decorator.credentials.access_token)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400262
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500263
264 def test_kwargs_are_passed_to_underlying_flow(self):
265 decorator = OAuth2Decorator(client_id='foo_client_id',
266 client_secret='foo_client_secret',
267 scope=['foo_scope', 'bar_scope'],
268 access_type='offline',
269 approval_prompt='force')
270 self.assertEqual('offline', decorator.flow.params['access_type'])
271 self.assertEqual('force', decorator.flow.params['approval_prompt'])
272
273
Joe Gregorio432f17e2011-05-22 23:18:00 -0400274if __name__ == '__main__':
275 unittest.main()