blob: 20e8f09bd9bc37dcd18679fc2bdefce7b4713a48 [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 Gregorioe84c9442012-03-12 08:45:57 -040026import datetime
Joe Gregorio432f17e2011-05-22 23:18:00 -040027import httplib2
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040028import mox
Joe Gregorio08cdcb82012-03-14 00:09:33 -040029import os
Joe Gregoriod84d6b82012-02-28 14:53:00 -050030import time
Joe Gregorio432f17e2011-05-22 23:18:00 -040031import unittest
Joe Gregoriocda87522013-02-22 16:22:48 -050032import urllib
Joe Gregorio432f17e2011-05-22 23:18:00 -040033
34try:
35 from urlparse import parse_qs
36except ImportError:
37 from cgi import parse_qs
38
Joe Gregorio8b4c1732011-12-06 11:28:29 -050039import dev_appserver
Joe Gregorioe366ef02013-03-04 14:32:57 -050040_EXTRA_PATHS = dev_appserver.EXTRA_PATHS
41_DIR_PATH = _EXTRA_PATHS[0]
42_OLD_WEBOB = os.path.join(_DIR_PATH, 'lib', 'webob_0_9')
43_WEBOB_INDEX = _EXTRA_PATHS.index(_OLD_WEBOB)
44_NEW_WEBOB = os.path.join(_DIR_PATH, 'lib', 'webob-1.2.3')
45_EXTRA_PATHS[_WEBOB_INDEX] = _NEW_WEBOB
Joe Gregorio8b4c1732011-12-06 11:28:29 -050046dev_appserver.fix_sys_path()
Joe Gregorio17774972012-03-01 11:11:59 -050047import webapp2
Joe Gregorio8b4c1732011-12-06 11:28:29 -050048
JacobMoshenko8e905102011-06-20 09:53:10 -040049from apiclient.http import HttpMockSequence
50from google.appengine.api import apiproxy_stub
51from google.appengine.api import apiproxy_stub_map
Joe Gregoriod84d6b82012-02-28 14:53:00 -050052from google.appengine.api import app_identity
Joe Gregorioe84c9442012-03-12 08:45:57 -040053from google.appengine.api import memcache
Joe Gregorio08cdcb82012-03-14 00:09:33 -040054from google.appengine.api import users
Joe Gregoriod84d6b82012-02-28 14:53:00 -050055from google.appengine.api.memcache import memcache_stub
Joe Gregorioe84c9442012-03-12 08:45:57 -040056from google.appengine.ext import db
dhermes@google.com47154822012-11-26 10:44:09 -080057from google.appengine.ext import ndb
JacobMoshenko8e905102011-06-20 09:53:10 -040058from google.appengine.ext import testbed
Joe Gregoriod84d6b82012-02-28 14:53:00 -050059from google.appengine.runtime import apiproxy_errors
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040060from oauth2client import appengine
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -080061from oauth2client import GOOGLE_TOKEN_URI
Joe Gregorio549230c2012-01-11 10:38:05 -050062from oauth2client.anyjson import simplejson
Joe Gregorioc29aaa92012-07-16 16:16:31 -040063from oauth2client.clientsecrets import _loadfile
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040064from oauth2client.clientsecrets import InvalidClientSecretsError
JacobMoshenko8e905102011-06-20 09:53:10 -040065from oauth2client.appengine import AppAssertionCredentials
Joe Gregorioe84c9442012-03-12 08:45:57 -040066from oauth2client.appengine import CredentialsModel
dhermes@google.com47154822012-11-26 10:44:09 -080067from oauth2client.appengine import CredentialsNDBModel
68from oauth2client.appengine import FlowNDBProperty
Joe Gregorio4fbde1c2012-07-11 14:47:39 -040069from oauth2client.appengine import FlowProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -040070from oauth2client.appengine import OAuth2Decorator
Joe Gregorioe84c9442012-03-12 08:45:57 -040071from oauth2client.appengine import StorageByKeyName
Joe Gregorio08cdcb82012-03-14 00:09:33 -040072from oauth2client.appengine import oauth2decorator_from_clientsecrets
Joe Gregorio549230c2012-01-11 10:38:05 -050073from oauth2client.client import AccessTokenRefreshError
Joe Gregorio08cdcb82012-03-14 00:09:33 -040074from oauth2client.client import Credentials
Joe Gregorio549230c2012-01-11 10:38:05 -050075from oauth2client.client import FlowExchangeError
Joe Gregorioe84c9442012-03-12 08:45:57 -040076from oauth2client.client import OAuth2Credentials
Joe Gregorio08cdcb82012-03-14 00:09:33 -040077from oauth2client.client import flow_from_clientsecrets
JacobMoshenko8e905102011-06-20 09:53:10 -040078from webtest import TestApp
Joe Gregorio432f17e2011-05-22 23:18:00 -040079
Joe Gregorio4fbde1c2012-07-11 14:47:39 -040080
Joe Gregorio08cdcb82012-03-14 00:09:33 -040081DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
82
83
84def datafile(filename):
85 return os.path.join(DATA_DIR, filename)
86
87
Joe Gregorioc29aaa92012-07-16 16:16:31 -040088def load_and_cache(existing_file, fakename, cache_mock):
89 client_type, client_info = _loadfile(datafile(existing_file))
90 cache_mock.cache[fakename] = {client_type: client_info}
91
92
93class CacheMock(object):
94 def __init__(self):
95 self.cache = {}
96
97 def get(self, key, namespace=''):
98 # ignoring namespace for easier testing
99 return self.cache.get(key, None)
100
101 def set(self, key, value, namespace=''):
102 # ignoring namespace for easier testing
103 self.cache[key] = value
104
105
Joe Gregorio432f17e2011-05-22 23:18:00 -0400106class UserMock(object):
107 """Mock the app engine user service"""
JacobMoshenko8e905102011-06-20 09:53:10 -0400108
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400109 def __call__(self):
110 return self
111
Joe Gregorio432f17e2011-05-22 23:18:00 -0400112 def user_id(self):
113 return 'foo_user'
114
115
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400116class UserNotLoggedInMock(object):
117 """Mock the app engine user service"""
118
119 def __call__(self):
120 return None
121
122
Joe Gregorio432f17e2011-05-22 23:18:00 -0400123class Http2Mock(object):
124 """Mock httplib2.Http"""
125 status = 200
126 content = {
127 'access_token': 'foo_access_token',
128 'refresh_token': 'foo_refresh_token',
JacobMoshenko8e905102011-06-20 09:53:10 -0400129 'expires_in': 3600,
Joe Gregoriocda87522013-02-22 16:22:48 -0500130 'extra': 'value',
Joe Gregorio432f17e2011-05-22 23:18:00 -0400131 }
132
133 def request(self, token_uri, method, body, headers, *args, **kwargs):
134 self.body = body
135 self.headers = headers
136 return (self, simplejson.dumps(self.content))
137
138
JacobMoshenko8e905102011-06-20 09:53:10 -0400139class TestAppAssertionCredentials(unittest.TestCase):
140 account_name = "service_account_name@appspot.com"
141 signature = "signature"
142
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500143
JacobMoshenko8e905102011-06-20 09:53:10 -0400144 class AppIdentityStubImpl(apiproxy_stub.APIProxyStub):
145
146 def __init__(self):
147 super(TestAppAssertionCredentials.AppIdentityStubImpl, self).__init__(
148 'app_identity_service')
149
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500150 def _Dynamic_GetAccessToken(self, request, response):
151 response.set_access_token('a_token_123')
152 response.set_expiration_time(time.time() + 1800)
JacobMoshenko8e905102011-06-20 09:53:10 -0400153
JacobMoshenko8e905102011-06-20 09:53:10 -0400154
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500155 class ErroringAppIdentityStubImpl(apiproxy_stub.APIProxyStub):
156
157 def __init__(self):
158 super(TestAppAssertionCredentials.ErroringAppIdentityStubImpl, self).__init__(
159 'app_identity_service')
160
161 def _Dynamic_GetAccessToken(self, request, response):
162 raise app_identity.BackendDeadlineExceeded()
163
164 def test_raise_correct_type_of_exception(self):
165 app_identity_stub = self.ErroringAppIdentityStubImpl()
166 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800167 apiproxy_stub_map.apiproxy.RegisterStub('app_identity_service',
JacobMoshenko8e905102011-06-20 09:53:10 -0400168 app_identity_stub)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500169 apiproxy_stub_map.apiproxy.RegisterStub(
170 'memcache', memcache_stub.MemcacheServiceStub())
JacobMoshenko8e905102011-06-20 09:53:10 -0400171
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800172 scope = 'http://www.googleapis.com/scope'
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500173 try:
174 credentials = AppAssertionCredentials(scope)
175 http = httplib2.Http()
176 credentials.refresh(http)
177 self.fail('Should have raised an AccessTokenRefreshError')
178 except AccessTokenRefreshError:
179 pass
JacobMoshenko8e905102011-06-20 09:53:10 -0400180
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500181 def test_get_access_token_on_refresh(self):
182 app_identity_stub = self.AppIdentityStubImpl()
183 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
184 apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
185 app_identity_stub)
186 apiproxy_stub_map.apiproxy.RegisterStub(
187 'memcache', memcache_stub.MemcacheServiceStub())
JacobMoshenko8e905102011-06-20 09:53:10 -0400188
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500189 scope = [
190 "http://www.googleapis.com/scope",
191 "http://www.googleapis.com/scope2"]
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500192 credentials = AppAssertionCredentials(scope)
193 http = httplib2.Http()
194 credentials.refresh(http)
195 self.assertEqual('a_token_123', credentials.access_token)
JacobMoshenko8e905102011-06-20 09:53:10 -0400196
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400197 json = credentials.to_json()
198 credentials = Credentials.new_from_json(json)
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500199 self.assertEqual(
200 'http://www.googleapis.com/scope http://www.googleapis.com/scope2',
201 credentials.scope)
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400202
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500203 scope = "http://www.googleapis.com/scope http://www.googleapis.com/scope2"
204 credentials = AppAssertionCredentials(scope)
205 http = httplib2.Http()
206 credentials.refresh(http)
207 self.assertEqual('a_token_123', credentials.access_token)
208 self.assertEqual(
209 'http://www.googleapis.com/scope http://www.googleapis.com/scope2',
210 credentials.scope)
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400211
dhermes@google.com47154822012-11-26 10:44:09 -0800212
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400213class TestFlowModel(db.Model):
214 flow = FlowProperty()
215
216
217class FlowPropertyTest(unittest.TestCase):
218
219 def setUp(self):
220 self.testbed = testbed.Testbed()
221 self.testbed.activate()
222 self.testbed.init_datastore_v3_stub()
223
224 def tearDown(self):
225 self.testbed.deactivate()
226
227 def test_flow_get_put(self):
228 instance = TestFlowModel(
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400229 flow=flow_from_clientsecrets(datafile('client_secrets.json'), 'foo',
230 redirect_uri='oob'),
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400231 key_name='foo'
232 )
233 instance.put()
234 retrieved = TestFlowModel.get_by_key_name('foo')
235
236 self.assertEqual('foo_client_id', retrieved.flow.client_id)
237
JacobMoshenko8e905102011-06-20 09:53:10 -0400238
dhermes@google.com47154822012-11-26 10:44:09 -0800239class TestFlowNDBModel(ndb.Model):
240 flow = FlowNDBProperty()
241
242
243class FlowNDBPropertyTest(unittest.TestCase):
244
245 def setUp(self):
246 self.testbed = testbed.Testbed()
247 self.testbed.activate()
248 self.testbed.init_datastore_v3_stub()
249 self.testbed.init_memcache_stub()
250
251 def tearDown(self):
252 self.testbed.deactivate()
253
254 def test_flow_get_put(self):
255 instance = TestFlowNDBModel(
256 flow=flow_from_clientsecrets(datafile('client_secrets.json'), 'foo',
257 redirect_uri='oob'),
258 id='foo'
259 )
260 instance.put()
261 retrieved = TestFlowNDBModel.get_by_id('foo')
262
263 self.assertEqual('foo_client_id', retrieved.flow.client_id)
264
265
Joe Gregorioe84c9442012-03-12 08:45:57 -0400266def _http_request(*args, **kwargs):
267 resp = httplib2.Response({'status': '200'})
268 content = simplejson.dumps({'access_token': 'bar'})
269
270 return resp, content
271
272
273class StorageByKeyNameTest(unittest.TestCase):
274
275 def setUp(self):
276 self.testbed = testbed.Testbed()
277 self.testbed.activate()
278 self.testbed.init_datastore_v3_stub()
279 self.testbed.init_memcache_stub()
280 self.testbed.init_user_stub()
281
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800282 access_token = 'foo'
283 client_id = 'some_client_id'
284 client_secret = 'cOuDdkfjxxnv+'
285 refresh_token = '1/0/a.df219fjls0'
Joe Gregorioe84c9442012-03-12 08:45:57 -0400286 token_expiry = datetime.datetime.utcnow()
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800287 user_agent = 'refresh_checker/1.0'
Joe Gregorioe84c9442012-03-12 08:45:57 -0400288 self.credentials = OAuth2Credentials(
289 access_token, client_id, client_secret,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800290 refresh_token, token_expiry, GOOGLE_TOKEN_URI,
Joe Gregorioe84c9442012-03-12 08:45:57 -0400291 user_agent)
292
293 def tearDown(self):
294 self.testbed.deactivate()
295
296 def test_get_and_put_simple(self):
297 storage = StorageByKeyName(
298 CredentialsModel, 'foo', 'credentials')
299
300 self.assertEqual(None, storage.get())
301 self.credentials.set_store(storage)
302
303 self.credentials._refresh(_http_request)
304 credmodel = CredentialsModel.get_by_key_name('foo')
305 self.assertEqual('bar', credmodel.credentials.access_token)
306
307 def test_get_and_put_cached(self):
308 storage = StorageByKeyName(
309 CredentialsModel, 'foo', 'credentials', cache=memcache)
310
311 self.assertEqual(None, storage.get())
312 self.credentials.set_store(storage)
313
314 self.credentials._refresh(_http_request)
315 credmodel = CredentialsModel.get_by_key_name('foo')
316 self.assertEqual('bar', credmodel.credentials.access_token)
317
318 # Now remove the item from the cache.
319 memcache.delete('foo')
320
321 # Check that getting refreshes the cache.
322 credentials = storage.get()
323 self.assertEqual('bar', credentials.access_token)
324 self.assertNotEqual(None, memcache.get('foo'))
325
326 # Deleting should clear the cache.
327 storage.delete()
328 credentials = storage.get()
329 self.assertEqual(None, credentials)
330 self.assertEqual(None, memcache.get('foo'))
331
dhermes@google.com47154822012-11-26 10:44:09 -0800332 def test_get_and_put_ndb(self):
333 # Start empty
334 storage = StorageByKeyName(
335 CredentialsNDBModel, 'foo', 'credentials')
336 self.assertEqual(None, storage.get())
337
338 # Refresh storage and retrieve without using storage
339 self.credentials.set_store(storage)
340 self.credentials._refresh(_http_request)
341 credmodel = CredentialsNDBModel.get_by_id('foo')
342 self.assertEqual('bar', credmodel.credentials.access_token)
343 self.assertEqual(credmodel.credentials.to_json(),
344 self.credentials.to_json())
345
346 def test_delete_ndb(self):
347 # Start empty
348 storage = StorageByKeyName(
349 CredentialsNDBModel, 'foo', 'credentials')
350 self.assertEqual(None, storage.get())
351
352 # Add credentials to model with storage, and check equivalent w/o storage
353 storage.put(self.credentials)
354 credmodel = CredentialsNDBModel.get_by_id('foo')
355 self.assertEqual(credmodel.credentials.to_json(),
356 self.credentials.to_json())
357
358 # Delete and make sure empty
359 storage.delete()
360 self.assertEqual(None, storage.get())
361
362 def test_get_and_put_mixed_ndb_storage_db_get(self):
363 # Start empty
364 storage = StorageByKeyName(
365 CredentialsNDBModel, 'foo', 'credentials')
366 self.assertEqual(None, storage.get())
367
368 # Set NDB store and refresh to add to storage
369 self.credentials.set_store(storage)
370 self.credentials._refresh(_http_request)
371
372 # Retrieve same key from DB model to confirm mixing works
373 credmodel = CredentialsModel.get_by_key_name('foo')
374 self.assertEqual('bar', credmodel.credentials.access_token)
375 self.assertEqual(self.credentials.to_json(),
376 credmodel.credentials.to_json())
377
378 def test_get_and_put_mixed_db_storage_ndb_get(self):
379 # Start empty
380 storage = StorageByKeyName(
381 CredentialsModel, 'foo', 'credentials')
382 self.assertEqual(None, storage.get())
383
384 # Set DB store and refresh to add to storage
385 self.credentials.set_store(storage)
386 self.credentials._refresh(_http_request)
387
388 # Retrieve same key from NDB model to confirm mixing works
389 credmodel = CredentialsNDBModel.get_by_id('foo')
390 self.assertEqual('bar', credmodel.credentials.access_token)
391 self.assertEqual(self.credentials.to_json(),
392 credmodel.credentials.to_json())
393
394 def test_delete_db_ndb_mixed(self):
395 # Start empty
396 storage_ndb = StorageByKeyName(
397 CredentialsNDBModel, 'foo', 'credentials')
398 storage = StorageByKeyName(
399 CredentialsModel, 'foo', 'credentials')
400
401 # First DB, then NDB
402 self.assertEqual(None, storage.get())
403 storage.put(self.credentials)
404 self.assertNotEqual(None, storage.get())
405
406 storage_ndb.delete()
407 self.assertEqual(None, storage.get())
408
409 # First NDB, then DB
410 self.assertEqual(None, storage_ndb.get())
411 storage_ndb.put(self.credentials)
412
413 storage.delete()
414 self.assertNotEqual(None, storage_ndb.get())
415 # NDB uses memcache and an instance cache (Context)
416 ndb.get_context().clear_cache()
417 memcache.flush_all()
418 self.assertEqual(None, storage_ndb.get())
419
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400420
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400421class MockRequest(object):
422 url = 'https://example.org'
423
424 def relative_url(self, rel):
425 return self.url + rel
426
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400427
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400428class MockRequestHandler(object):
429 request = MockRequest()
Joe Gregorioe84c9442012-03-12 08:45:57 -0400430
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400431
Joe Gregorio432f17e2011-05-22 23:18:00 -0400432class DecoratorTests(unittest.TestCase):
433
434 def setUp(self):
435 self.testbed = testbed.Testbed()
436 self.testbed.activate()
437 self.testbed.init_datastore_v3_stub()
438 self.testbed.init_memcache_stub()
439 self.testbed.init_user_stub()
440
441 decorator = OAuth2Decorator(client_id='foo_client_id',
442 client_secret='foo_client_secret',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100443 scope=['foo_scope', 'bar_scope'],
444 user_agent='foo')
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400445
446 self._finish_setup(decorator, user_mock=UserMock)
447
448 def _finish_setup(self, decorator, user_mock):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400449 self.decorator = decorator
450
Joe Gregorio17774972012-03-01 11:11:59 -0500451 class TestRequiredHandler(webapp2.RequestHandler):
JacobMoshenko8e905102011-06-20 09:53:10 -0400452
Joe Gregorio432f17e2011-05-22 23:18:00 -0400453 @decorator.oauth_required
454 def get(self):
455 pass
456
Joe Gregorio17774972012-03-01 11:11:59 -0500457 class TestAwareHandler(webapp2.RequestHandler):
JacobMoshenko8e905102011-06-20 09:53:10 -0400458
Joe Gregorio432f17e2011-05-22 23:18:00 -0400459 @decorator.oauth_aware
Joe Gregorio17774972012-03-01 11:11:59 -0500460 def get(self, *args, **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400461 self.response.out.write('Hello World!')
Joe Gregorio17774972012-03-01 11:11:59 -0500462 assert(kwargs['year'] == '2012')
463 assert(kwargs['month'] == '01')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400464
465
Joe Gregorio17774972012-03-01 11:11:59 -0500466 application = webapp2.WSGIApplication([
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400467 ('/oauth2callback', self.decorator.callback_handler()),
Joe Gregorio17774972012-03-01 11:11:59 -0500468 ('/foo_path', TestRequiredHandler),
469 webapp2.Route(r'/bar_path/<year:\d{4}>/<month:\d{2}>',
470 handler=TestAwareHandler, name='bar')],
471 debug=True)
Joe Gregorio77254c12012-08-27 14:13:22 -0400472 self.app = TestApp(application, extra_environ={
473 'wsgi.url_scheme': 'http',
474 'HTTP_HOST': 'localhost',
475 })
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400476 users.get_current_user = user_mock()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400477 self.httplib2_orig = httplib2.Http
Joe Gregorio432f17e2011-05-22 23:18:00 -0400478 httplib2.Http = Http2Mock
479
480 def tearDown(self):
481 self.testbed.deactivate()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400482 httplib2.Http = self.httplib2_orig
Joe Gregorio432f17e2011-05-22 23:18:00 -0400483
484 def test_required(self):
485 # An initial request to an oauth_required decorated path should be a
486 # redirect to start the OAuth dance.
Joe Gregorio77254c12012-08-27 14:13:22 -0400487 response = self.app.get('http://localhost/foo_path')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400488 self.assertTrue(response.status.startswith('302'))
489 q = parse_qs(response.headers['Location'].split('?', 1)[1])
490 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
491 self.assertEqual('foo_client_id', q['client_id'][0])
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400492 self.assertEqual('foo_scope bar_scope', q['scope'][0])
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400493 self.assertEqual('http://localhost/foo_path',
494 q['state'][0].rsplit(':', 1)[0])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400495 self.assertEqual('code', q['response_type'][0])
496 self.assertEqual(False, self.decorator.has_credentials())
497
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400498 m = mox.Mox()
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800499 m.StubOutWithMock(appengine, '_parse_state_value')
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400500 appengine._parse_state_value('foo_path:xsrfkey123',
501 mox.IgnoreArg()).AndReturn('foo_path')
502 m.ReplayAll()
503
Joe Gregorio562b7312011-09-15 09:06:38 -0400504 # Now simulate the callback to /oauth2callback.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400505 response = self.app.get('/oauth2callback', {
506 'code': 'foo_access_code',
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400507 'state': 'foo_path:xsrfkey123',
Joe Gregorio432f17e2011-05-22 23:18:00 -0400508 })
Joe Gregoriocda87522013-02-22 16:22:48 -0500509 parts = response.headers['Location'].split('?', 1)
510 self.assertEqual('http://localhost/foo_path', parts[0])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400511 self.assertEqual(None, self.decorator.credentials)
Joe Gregoriocda87522013-02-22 16:22:48 -0500512 if self.decorator._token_response_param:
513 response = parse_qs(parts[1])[self.decorator._token_response_param][0]
514 self.assertEqual(Http2Mock.content,
515 simplejson.loads(urllib.unquote(response)))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400516
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400517 m.UnsetStubs()
518 m.VerifyAll()
519
Joe Gregorio562b7312011-09-15 09:06:38 -0400520 # Now requesting the decorated path should work.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400521 response = self.app.get('/foo_path')
522 self.assertEqual('200 OK', response.status)
523 self.assertEqual(True, self.decorator.has_credentials())
JacobMoshenko8e905102011-06-20 09:53:10 -0400524 self.assertEqual('foo_refresh_token',
525 self.decorator.credentials.refresh_token)
526 self.assertEqual('foo_access_token',
527 self.decorator.credentials.access_token)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400528
Joe Gregorio562b7312011-09-15 09:06:38 -0400529 # Invalidate the stored Credentials.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400530 self.decorator.credentials.invalid = True
531 self.decorator.credentials.store.put(self.decorator.credentials)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400532
Joe Gregorio562b7312011-09-15 09:06:38 -0400533 # Invalid Credentials should start the OAuth dance again.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400534 response = self.app.get('/foo_path')
535 self.assertTrue(response.status.startswith('302'))
536 q = parse_qs(response.headers['Location'].split('?', 1)[1])
537 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
538
Joe Gregorioec75dc12012-02-06 13:40:42 -0500539 def test_storage_delete(self):
540 # An initial request to an oauth_required decorated path should be a
541 # redirect to start the OAuth dance.
542 response = self.app.get('/foo_path')
543 self.assertTrue(response.status.startswith('302'))
544
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400545 m = mox.Mox()
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800546 m.StubOutWithMock(appengine, '_parse_state_value')
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400547 appengine._parse_state_value('foo_path:xsrfkey123',
548 mox.IgnoreArg()).AndReturn('foo_path')
549 m.ReplayAll()
550
Joe Gregorioec75dc12012-02-06 13:40:42 -0500551 # Now simulate the callback to /oauth2callback.
552 response = self.app.get('/oauth2callback', {
553 'code': 'foo_access_code',
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400554 'state': 'foo_path:xsrfkey123',
Joe Gregorioec75dc12012-02-06 13:40:42 -0500555 })
556 self.assertEqual('http://localhost/foo_path', response.headers['Location'])
557 self.assertEqual(None, self.decorator.credentials)
558
559 # Now requesting the decorated path should work.
560 response = self.app.get('/foo_path')
561
562 # Invalidate the stored Credentials.
563 self.decorator.credentials.store.delete()
564
565 # Invalid Credentials should start the OAuth dance again.
566 response = self.app.get('/foo_path')
567 self.assertTrue(response.status.startswith('302'))
568
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400569 m.UnsetStubs()
570 m.VerifyAll()
571
Joe Gregorio432f17e2011-05-22 23:18:00 -0400572 def test_aware(self):
Joe Gregorio562b7312011-09-15 09:06:38 -0400573 # An initial request to an oauth_aware decorated path should not redirect.
Joe Gregorio77254c12012-08-27 14:13:22 -0400574 response = self.app.get('http://localhost/bar_path/2012/01')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400575 self.assertEqual('Hello World!', response.body)
576 self.assertEqual('200 OK', response.status)
577 self.assertEqual(False, self.decorator.has_credentials())
578 url = self.decorator.authorize_url()
579 q = parse_qs(url.split('?', 1)[1])
580 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
581 self.assertEqual('foo_client_id', q['client_id'][0])
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400582 self.assertEqual('foo_scope bar_scope', q['scope'][0])
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400583 self.assertEqual('http://localhost/bar_path/2012/01',
584 q['state'][0].rsplit(':', 1)[0])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400585 self.assertEqual('code', q['response_type'][0])
586
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400587 m = mox.Mox()
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800588 m.StubOutWithMock(appengine, '_parse_state_value')
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400589 appengine._parse_state_value('bar_path:xsrfkey456',
590 mox.IgnoreArg()).AndReturn('bar_path')
591 m.ReplayAll()
592
Joe Gregorio562b7312011-09-15 09:06:38 -0400593 # Now simulate the callback to /oauth2callback.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400594 url = self.decorator.authorize_url()
595 response = self.app.get('/oauth2callback', {
596 'code': 'foo_access_code',
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400597 'state': 'bar_path:xsrfkey456',
Joe Gregorio432f17e2011-05-22 23:18:00 -0400598 })
599 self.assertEqual('http://localhost/bar_path', response.headers['Location'])
600 self.assertEqual(False, self.decorator.has_credentials())
601
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400602 m.UnsetStubs()
603 m.VerifyAll()
604
Joe Gregorio562b7312011-09-15 09:06:38 -0400605 # Now requesting the decorated path will have credentials.
Joe Gregorio17774972012-03-01 11:11:59 -0500606 response = self.app.get('/bar_path/2012/01')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400607 self.assertEqual('200 OK', response.status)
608 self.assertEqual('Hello World!', response.body)
609 self.assertEqual(True, self.decorator.has_credentials())
JacobMoshenko8e905102011-06-20 09:53:10 -0400610 self.assertEqual('foo_refresh_token',
611 self.decorator.credentials.refresh_token)
612 self.assertEqual('foo_access_token',
613 self.decorator.credentials.access_token)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400614
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400615 def test_error_in_step2(self):
616 # An initial request to an oauth_aware decorated path should not redirect.
617 response = self.app.get('/bar_path/2012/01')
618 url = self.decorator.authorize_url()
619 response = self.app.get('/oauth2callback', {
Joe Gregorio77254c12012-08-27 14:13:22 -0400620 'error': 'Bad<Stuff>Happened\''
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400621 })
622 self.assertEqual('200 OK', response.status)
Joe Gregorio77254c12012-08-27 14:13:22 -0400623 self.assertTrue('Bad&lt;Stuff&gt;Happened&#39;' in response.body)
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400624
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500625 def test_kwargs_are_passed_to_underlying_flow(self):
626 decorator = OAuth2Decorator(client_id='foo_client_id',
627 client_secret='foo_client_secret',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100628 user_agent='foo_user_agent',
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500629 scope=['foo_scope', 'bar_scope'],
630 access_type='offline',
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800631 approval_prompt='force',
632 revoke_uri='dummy_revoke_uri')
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400633 request_handler = MockRequestHandler()
634 decorator._create_flow(request_handler)
635
636 self.assertEqual('https://example.org/oauth2callback',
637 decorator.flow.redirect_uri)
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500638 self.assertEqual('offline', decorator.flow.params['access_type'])
639 self.assertEqual('force', decorator.flow.params['approval_prompt'])
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100640 self.assertEqual('foo_user_agent', decorator.flow.user_agent)
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800641 self.assertEqual('dummy_revoke_uri', decorator.flow.revoke_uri)
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100642 self.assertEqual(None, decorator.flow.params.get('user_agent', None))
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500643
Joe Gregoriocda87522013-02-22 16:22:48 -0500644 def test_token_response_param(self):
645 self.decorator._token_response_param = 'foobar'
646 self.test_required()
647
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400648 def test_decorator_from_client_secrets(self):
649 decorator = oauth2decorator_from_clientsecrets(
650 datafile('client_secrets.json'),
651 scope=['foo_scope', 'bar_scope'])
652 self._finish_setup(decorator, user_mock=UserMock)
653
654 self.assertFalse(decorator._in_error)
655 self.decorator = decorator
656 self.test_required()
657 http = self.decorator.http()
658 self.assertEquals('foo_access_token', http.request.credentials.access_token)
659
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800660 # revoke_uri is not required
661 self.assertEqual(self.decorator._revoke_uri,
662 'https://accounts.google.com/o/oauth2/revoke')
663 self.assertEqual(self.decorator._revoke_uri,
664 self.decorator.credentials.revoke_uri)
665
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400666 def test_decorator_from_cached_client_secrets(self):
667 cache_mock = CacheMock()
668 load_and_cache('client_secrets.json', 'secret', cache_mock)
669 decorator = oauth2decorator_from_clientsecrets(
670 # filename, scope, message=None, cache=None
671 'secret', '', cache=cache_mock)
672 self.assertFalse(decorator._in_error)
673
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400674 def test_decorator_from_client_secrets_not_logged_in_required(self):
675 decorator = oauth2decorator_from_clientsecrets(
676 datafile('client_secrets.json'),
677 scope=['foo_scope', 'bar_scope'], message='NotLoggedInMessage')
678 self.decorator = decorator
679 self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
680
681 self.assertFalse(decorator._in_error)
682
683 # An initial request to an oauth_required decorated path should be a
684 # redirect to login.
685 response = self.app.get('/foo_path')
686 self.assertTrue(response.status.startswith('302'))
687 self.assertTrue('Login' in str(response))
688
689 def test_decorator_from_client_secrets_not_logged_in_aware(self):
690 decorator = oauth2decorator_from_clientsecrets(
691 datafile('client_secrets.json'),
692 scope=['foo_scope', 'bar_scope'], message='NotLoggedInMessage')
693 self.decorator = decorator
694 self._finish_setup(decorator, user_mock=UserNotLoggedInMock)
695
696 # An initial request to an oauth_aware decorated path should be a
697 # redirect to login.
698 response = self.app.get('/bar_path/2012/03')
699 self.assertTrue(response.status.startswith('302'))
700 self.assertTrue('Login' in str(response))
701
702 def test_decorator_from_unfilled_client_secrets_required(self):
703 MESSAGE = 'File is missing'
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400704 try:
705 decorator = oauth2decorator_from_clientsecrets(
706 datafile('unfilled_client_secrets.json'),
707 scope=['foo_scope', 'bar_scope'], message=MESSAGE)
708 except InvalidClientSecretsError:
709 pass
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400710
711 def test_decorator_from_unfilled_client_secrets_aware(self):
712 MESSAGE = 'File is missing'
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400713 try:
714 decorator = oauth2decorator_from_clientsecrets(
715 datafile('unfilled_client_secrets.json'),
716 scope=['foo_scope', 'bar_scope'], message=MESSAGE)
717 except InvalidClientSecretsError:
718 pass
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400719
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400720
721class DecoratorXsrfSecretTests(unittest.TestCase):
722 """Test xsrf_secret_key."""
723
724 def setUp(self):
725 self.testbed = testbed.Testbed()
726 self.testbed.activate()
727 self.testbed.init_datastore_v3_stub()
728 self.testbed.init_memcache_stub()
729
730 def tearDown(self):
731 self.testbed.deactivate()
732
733 def test_build_and_parse_state(self):
734 secret = appengine.xsrf_secret_key()
735
736 # Secret shouldn't change from call to call.
737 secret2 = appengine.xsrf_secret_key()
738 self.assertEqual(secret, secret2)
739
740 # Secret shouldn't change if memcache goes away.
741 memcache.delete(appengine.XSRF_MEMCACHE_ID,
dhermes@google.com47154822012-11-26 10:44:09 -0800742 namespace=appengine.OAUTH2CLIENT_NAMESPACE)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400743 secret3 = appengine.xsrf_secret_key()
744 self.assertEqual(secret2, secret3)
745
746 # Secret should change if both memcache and the model goes away.
747 memcache.delete(appengine.XSRF_MEMCACHE_ID,
dhermes@google.com47154822012-11-26 10:44:09 -0800748 namespace=appengine.OAUTH2CLIENT_NAMESPACE)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400749 model = appengine.SiteXsrfSecretKey.get_or_insert('site')
750 model.delete()
751
752 secret4 = appengine.xsrf_secret_key()
753 self.assertNotEqual(secret3, secret4)
754
dhermes@google.com47154822012-11-26 10:44:09 -0800755 def test_ndb_insert_db_get(self):
756 secret = appengine._generate_new_xsrf_secret_key()
757 appengine.SiteXsrfSecretKeyNDB(id='site', secret=secret).put()
758
759 site_key = appengine.SiteXsrfSecretKey.get_by_key_name('site')
760 self.assertEqual(site_key.secret, secret)
761
762 def test_db_insert_ndb_get(self):
763 secret = appengine._generate_new_xsrf_secret_key()
764 appengine.SiteXsrfSecretKey(key_name='site', secret=secret).put()
765
766 site_key = appengine.SiteXsrfSecretKeyNDB.get_by_id('site')
767 self.assertEqual(site_key.secret, secret)
768
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400769
770class DecoratorXsrfProtectionTests(unittest.TestCase):
771 """Test _build_state_value and _parse_state_value."""
772
773 def setUp(self):
774 self.testbed = testbed.Testbed()
775 self.testbed.activate()
776 self.testbed.init_datastore_v3_stub()
777 self.testbed.init_memcache_stub()
778
779 def tearDown(self):
780 self.testbed.deactivate()
781
782 def test_build_and_parse_state(self):
783 state = appengine._build_state_value(MockRequestHandler(), UserMock())
784 self.assertEqual(
785 'https://example.org',
786 appengine._parse_state_value(state, UserMock()))
787 self.assertRaises(appengine.InvalidXsrfTokenError,
788 appengine._parse_state_value, state[1:], UserMock())
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400789
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500790
Joe Gregorio432f17e2011-05-22 23:18:00 -0400791if __name__ == '__main__':
792 unittest.main()