blob: 1520ae1f0ffc800bca02ab6559a29128655d5872 [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
Joe Gregoriod84d6b82012-02-28 14:53:00 -050027import time
Joe Gregorio432f17e2011-05-22 23:18:00 -040028import unittest
29import urlparse
30
31try:
32 from urlparse import parse_qs
33except ImportError:
34 from cgi import parse_qs
35
Joe Gregorio8b4c1732011-12-06 11:28:29 -050036import dev_appserver
37dev_appserver.fix_sys_path()
Joe Gregorio17774972012-03-01 11:11:59 -050038import webapp2
Joe Gregorio8b4c1732011-12-06 11:28:29 -050039
JacobMoshenko8e905102011-06-20 09:53:10 -040040from apiclient.http import HttpMockSequence
41from google.appengine.api import apiproxy_stub
42from google.appengine.api import apiproxy_stub_map
Joe Gregoriod84d6b82012-02-28 14:53:00 -050043from google.appengine.api import app_identity
JacobMoshenko8e905102011-06-20 09:53:10 -040044from google.appengine.api import users
Joe Gregoriod84d6b82012-02-28 14:53:00 -050045from google.appengine.api.memcache import memcache_stub
JacobMoshenko8e905102011-06-20 09:53:10 -040046from google.appengine.ext import testbed
Joe Gregoriod84d6b82012-02-28 14:53:00 -050047from google.appengine.runtime import apiproxy_errors
Joe Gregorio549230c2012-01-11 10:38:05 -050048from oauth2client.anyjson import simplejson
JacobMoshenko8e905102011-06-20 09:53:10 -040049from oauth2client.appengine import AppAssertionCredentials
Joe Gregorio432f17e2011-05-22 23:18:00 -040050from oauth2client.appengine import OAuth2Decorator
Joe Gregorio432f17e2011-05-22 23:18:00 -040051from oauth2client.appengine import OAuth2Handler
Joe Gregorio549230c2012-01-11 10:38:05 -050052from oauth2client.client import AccessTokenRefreshError
53from oauth2client.client import FlowExchangeError
JacobMoshenko8e905102011-06-20 09:53:10 -040054from webtest import TestApp
Joe Gregorio432f17e2011-05-22 23:18:00 -040055
Joe Gregorio432f17e2011-05-22 23:18:00 -040056class UserMock(object):
57 """Mock the app engine user service"""
JacobMoshenko8e905102011-06-20 09:53:10 -040058
Joe Gregorio432f17e2011-05-22 23:18:00 -040059 def user_id(self):
60 return 'foo_user'
61
62
63class Http2Mock(object):
64 """Mock httplib2.Http"""
65 status = 200
66 content = {
67 'access_token': 'foo_access_token',
68 'refresh_token': 'foo_refresh_token',
JacobMoshenko8e905102011-06-20 09:53:10 -040069 'expires_in': 3600,
Joe Gregorio432f17e2011-05-22 23:18:00 -040070 }
71
72 def request(self, token_uri, method, body, headers, *args, **kwargs):
73 self.body = body
74 self.headers = headers
75 return (self, simplejson.dumps(self.content))
76
77
JacobMoshenko8e905102011-06-20 09:53:10 -040078class TestAppAssertionCredentials(unittest.TestCase):
79 account_name = "service_account_name@appspot.com"
80 signature = "signature"
81
Joe Gregoriod84d6b82012-02-28 14:53:00 -050082
JacobMoshenko8e905102011-06-20 09:53:10 -040083 class AppIdentityStubImpl(apiproxy_stub.APIProxyStub):
84
85 def __init__(self):
86 super(TestAppAssertionCredentials.AppIdentityStubImpl, self).__init__(
87 'app_identity_service')
88
Joe Gregoriod84d6b82012-02-28 14:53:00 -050089 def _Dynamic_GetAccessToken(self, request, response):
90 response.set_access_token('a_token_123')
91 response.set_expiration_time(time.time() + 1800)
JacobMoshenko8e905102011-06-20 09:53:10 -040092
JacobMoshenko8e905102011-06-20 09:53:10 -040093
Joe Gregoriod84d6b82012-02-28 14:53:00 -050094 class ErroringAppIdentityStubImpl(apiproxy_stub.APIProxyStub):
95
96 def __init__(self):
97 super(TestAppAssertionCredentials.ErroringAppIdentityStubImpl, self).__init__(
98 'app_identity_service')
99
100 def _Dynamic_GetAccessToken(self, request, response):
101 raise app_identity.BackendDeadlineExceeded()
102
103 def test_raise_correct_type_of_exception(self):
104 app_identity_stub = self.ErroringAppIdentityStubImpl()
105 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
JacobMoshenko8e905102011-06-20 09:53:10 -0400106 apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
107 app_identity_stub)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500108 apiproxy_stub_map.apiproxy.RegisterStub(
109 'memcache', memcache_stub.MemcacheServiceStub())
JacobMoshenko8e905102011-06-20 09:53:10 -0400110
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500111 scope = "http://www.googleapis.com/scope"
112 try:
113 credentials = AppAssertionCredentials(scope)
114 http = httplib2.Http()
115 credentials.refresh(http)
116 self.fail('Should have raised an AccessTokenRefreshError')
117 except AccessTokenRefreshError:
118 pass
JacobMoshenko8e905102011-06-20 09:53:10 -0400119
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500120 def test_get_access_token_on_refresh(self):
121 app_identity_stub = self.AppIdentityStubImpl()
122 apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
123 apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
124 app_identity_stub)
125 apiproxy_stub_map.apiproxy.RegisterStub(
126 'memcache', memcache_stub.MemcacheServiceStub())
JacobMoshenko8e905102011-06-20 09:53:10 -0400127
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500128 scope = "http://www.googleapis.com/scope"
129 credentials = AppAssertionCredentials(scope)
130 http = httplib2.Http()
131 credentials.refresh(http)
132 self.assertEqual('a_token_123', credentials.access_token)
JacobMoshenko8e905102011-06-20 09:53:10 -0400133
134
Joe Gregorio432f17e2011-05-22 23:18:00 -0400135class DecoratorTests(unittest.TestCase):
136
137 def setUp(self):
138 self.testbed = testbed.Testbed()
139 self.testbed.activate()
140 self.testbed.init_datastore_v3_stub()
141 self.testbed.init_memcache_stub()
142 self.testbed.init_user_stub()
143
144 decorator = OAuth2Decorator(client_id='foo_client_id',
145 client_secret='foo_client_secret',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100146 scope=['foo_scope', 'bar_scope'],
147 user_agent='foo')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400148 self.decorator = decorator
149
Joe Gregorio17774972012-03-01 11:11:59 -0500150 class TestRequiredHandler(webapp2.RequestHandler):
JacobMoshenko8e905102011-06-20 09:53:10 -0400151
Joe Gregorio432f17e2011-05-22 23:18:00 -0400152 @decorator.oauth_required
153 def get(self):
154 pass
155
Joe Gregorio17774972012-03-01 11:11:59 -0500156 class TestAwareHandler(webapp2.RequestHandler):
JacobMoshenko8e905102011-06-20 09:53:10 -0400157
Joe Gregorio432f17e2011-05-22 23:18:00 -0400158 @decorator.oauth_aware
Joe Gregorio17774972012-03-01 11:11:59 -0500159 def get(self, *args, **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400160 self.response.out.write('Hello World!')
Joe Gregorio17774972012-03-01 11:11:59 -0500161 assert(kwargs['year'] == '2012')
162 assert(kwargs['month'] == '01')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400163
164
Joe Gregorio17774972012-03-01 11:11:59 -0500165 application = webapp2.WSGIApplication([
166 ('/oauth2callback', OAuth2Handler),
167 ('/foo_path', TestRequiredHandler),
168 webapp2.Route(r'/bar_path/<year:\d{4}>/<month:\d{2}>',
169 handler=TestAwareHandler, name='bar')],
170 debug=True)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400171 self.app = TestApp(application)
172 users.get_current_user = UserMock
Joe Gregorio922b78c2011-05-26 21:36:34 -0400173 self.httplib2_orig = httplib2.Http
Joe Gregorio432f17e2011-05-22 23:18:00 -0400174 httplib2.Http = Http2Mock
175
176 def tearDown(self):
177 self.testbed.deactivate()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400178 httplib2.Http = self.httplib2_orig
Joe Gregorio432f17e2011-05-22 23:18:00 -0400179
180 def test_required(self):
181 # An initial request to an oauth_required decorated path should be a
182 # redirect to start the OAuth dance.
183 response = self.app.get('/foo_path')
184 self.assertTrue(response.status.startswith('302'))
185 q = parse_qs(response.headers['Location'].split('?', 1)[1])
186 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
187 self.assertEqual('foo_client_id', q['client_id'][0])
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400188 self.assertEqual('foo_scope bar_scope', q['scope'][0])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400189 self.assertEqual('http://localhost/foo_path', q['state'][0])
190 self.assertEqual('code', q['response_type'][0])
191 self.assertEqual(False, self.decorator.has_credentials())
192
Joe Gregorio562b7312011-09-15 09:06:38 -0400193 # Now simulate the callback to /oauth2callback.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400194 response = self.app.get('/oauth2callback', {
195 'code': 'foo_access_code',
JacobMoshenko8e905102011-06-20 09:53:10 -0400196 'state': 'foo_path',
Joe Gregorio432f17e2011-05-22 23:18:00 -0400197 })
198 self.assertEqual('http://localhost/foo_path', response.headers['Location'])
199 self.assertEqual(None, self.decorator.credentials)
200
Joe Gregorio562b7312011-09-15 09:06:38 -0400201 # Now requesting the decorated path should work.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400202 response = self.app.get('/foo_path')
203 self.assertEqual('200 OK', response.status)
204 self.assertEqual(True, self.decorator.has_credentials())
JacobMoshenko8e905102011-06-20 09:53:10 -0400205 self.assertEqual('foo_refresh_token',
206 self.decorator.credentials.refresh_token)
207 self.assertEqual('foo_access_token',
208 self.decorator.credentials.access_token)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400209
Joe Gregorio562b7312011-09-15 09:06:38 -0400210 # Invalidate the stored Credentials.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400211 self.decorator.credentials.invalid = True
212 self.decorator.credentials.store.put(self.decorator.credentials)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400213
Joe Gregorio562b7312011-09-15 09:06:38 -0400214 # Invalid Credentials should start the OAuth dance again.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400215 response = self.app.get('/foo_path')
216 self.assertTrue(response.status.startswith('302'))
217 q = parse_qs(response.headers['Location'].split('?', 1)[1])
218 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
219
Joe Gregorioec75dc12012-02-06 13:40:42 -0500220 def test_storage_delete(self):
221 # An initial request to an oauth_required decorated path should be a
222 # redirect to start the OAuth dance.
223 response = self.app.get('/foo_path')
224 self.assertTrue(response.status.startswith('302'))
225
226 # Now simulate the callback to /oauth2callback.
227 response = self.app.get('/oauth2callback', {
228 'code': 'foo_access_code',
229 'state': 'foo_path',
230 })
231 self.assertEqual('http://localhost/foo_path', response.headers['Location'])
232 self.assertEqual(None, self.decorator.credentials)
233
234 # Now requesting the decorated path should work.
235 response = self.app.get('/foo_path')
236
237 # Invalidate the stored Credentials.
238 self.decorator.credentials.store.delete()
239
240 # Invalid Credentials should start the OAuth dance again.
241 response = self.app.get('/foo_path')
242 self.assertTrue(response.status.startswith('302'))
243
Joe Gregorio432f17e2011-05-22 23:18:00 -0400244 def test_aware(self):
Joe Gregorio562b7312011-09-15 09:06:38 -0400245 # An initial request to an oauth_aware decorated path should not redirect.
Joe Gregorio17774972012-03-01 11:11:59 -0500246 response = self.app.get('/bar_path/2012/01')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400247 self.assertEqual('Hello World!', response.body)
248 self.assertEqual('200 OK', response.status)
249 self.assertEqual(False, self.decorator.has_credentials())
250 url = self.decorator.authorize_url()
251 q = parse_qs(url.split('?', 1)[1])
252 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
253 self.assertEqual('foo_client_id', q['client_id'][0])
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400254 self.assertEqual('foo_scope bar_scope', q['scope'][0])
Joe Gregorio17774972012-03-01 11:11:59 -0500255 self.assertEqual('http://localhost/bar_path/2012/01', q['state'][0])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400256 self.assertEqual('code', q['response_type'][0])
257
Joe Gregorio562b7312011-09-15 09:06:38 -0400258 # Now simulate the callback to /oauth2callback.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400259 url = self.decorator.authorize_url()
260 response = self.app.get('/oauth2callback', {
261 'code': 'foo_access_code',
JacobMoshenko8e905102011-06-20 09:53:10 -0400262 'state': 'bar_path',
Joe Gregorio432f17e2011-05-22 23:18:00 -0400263 })
264 self.assertEqual('http://localhost/bar_path', response.headers['Location'])
265 self.assertEqual(False, self.decorator.has_credentials())
266
Joe Gregorio562b7312011-09-15 09:06:38 -0400267 # Now requesting the decorated path will have credentials.
Joe Gregorio17774972012-03-01 11:11:59 -0500268 response = self.app.get('/bar_path/2012/01')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400269 self.assertEqual('200 OK', response.status)
270 self.assertEqual('Hello World!', response.body)
271 self.assertEqual(True, self.decorator.has_credentials())
JacobMoshenko8e905102011-06-20 09:53:10 -0400272 self.assertEqual('foo_refresh_token',
273 self.decorator.credentials.refresh_token)
274 self.assertEqual('foo_access_token',
275 self.decorator.credentials.access_token)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400276
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500277
278 def test_kwargs_are_passed_to_underlying_flow(self):
279 decorator = OAuth2Decorator(client_id='foo_client_id',
280 client_secret='foo_client_secret',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100281 user_agent='foo_user_agent',
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500282 scope=['foo_scope', 'bar_scope'],
283 access_type='offline',
284 approval_prompt='force')
285 self.assertEqual('offline', decorator.flow.params['access_type'])
286 self.assertEqual('force', decorator.flow.params['approval_prompt'])
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100287 self.assertEqual('foo_user_agent', decorator.flow.user_agent)
288 self.assertEqual(None, decorator.flow.params.get('user_agent', None))
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500289
290
Joe Gregorio432f17e2011-05-22 23:18:00 -0400291if __name__ == '__main__':
292 unittest.main()