blob: a0f32b25ab6bd5342e2949db608edd858c5bc0c5 [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()
38
JacobMoshenko8e905102011-06-20 09:53:10 -040039from apiclient.http import HttpMockSequence
40from google.appengine.api import apiproxy_stub
41from google.appengine.api import apiproxy_stub_map
Joe Gregoriod84d6b82012-02-28 14:53:00 -050042from google.appengine.api import app_identity
JacobMoshenko8e905102011-06-20 09:53:10 -040043from google.appengine.api import users
Joe Gregoriod84d6b82012-02-28 14:53:00 -050044from google.appengine.api.memcache import memcache_stub
JacobMoshenko8e905102011-06-20 09:53:10 -040045from google.appengine.ext import testbed
46from google.appengine.ext import webapp
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 Gregorio432f17e2011-05-22 23:18:00 -0400150 class TestRequiredHandler(webapp.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 Gregorio432f17e2011-05-22 23:18:00 -0400156 class TestAwareHandler(webapp.RequestHandler):
JacobMoshenko8e905102011-06-20 09:53:10 -0400157
Joe Gregorio432f17e2011-05-22 23:18:00 -0400158 @decorator.oauth_aware
159 def get(self):
160 self.response.out.write('Hello World!')
161
162
163 application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler),
164 ('/foo_path', TestRequiredHandler),
165 ('/bar_path', TestAwareHandler)],
166 debug=True)
167 self.app = TestApp(application)
168 users.get_current_user = UserMock
Joe Gregorio922b78c2011-05-26 21:36:34 -0400169 self.httplib2_orig = httplib2.Http
Joe Gregorio432f17e2011-05-22 23:18:00 -0400170 httplib2.Http = Http2Mock
171
172 def tearDown(self):
173 self.testbed.deactivate()
Joe Gregorio922b78c2011-05-26 21:36:34 -0400174 httplib2.Http = self.httplib2_orig
Joe Gregorio432f17e2011-05-22 23:18:00 -0400175
176 def test_required(self):
177 # An initial request to an oauth_required decorated path should be a
178 # redirect to start the OAuth dance.
179 response = self.app.get('/foo_path')
180 self.assertTrue(response.status.startswith('302'))
181 q = parse_qs(response.headers['Location'].split('?', 1)[1])
182 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
183 self.assertEqual('foo_client_id', q['client_id'][0])
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400184 self.assertEqual('foo_scope bar_scope', q['scope'][0])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400185 self.assertEqual('http://localhost/foo_path', q['state'][0])
186 self.assertEqual('code', q['response_type'][0])
187 self.assertEqual(False, self.decorator.has_credentials())
188
Joe Gregorio562b7312011-09-15 09:06:38 -0400189 # Now simulate the callback to /oauth2callback.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400190 response = self.app.get('/oauth2callback', {
191 'code': 'foo_access_code',
JacobMoshenko8e905102011-06-20 09:53:10 -0400192 'state': 'foo_path',
Joe Gregorio432f17e2011-05-22 23:18:00 -0400193 })
194 self.assertEqual('http://localhost/foo_path', response.headers['Location'])
195 self.assertEqual(None, self.decorator.credentials)
196
Joe Gregorio562b7312011-09-15 09:06:38 -0400197 # Now requesting the decorated path should work.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400198 response = self.app.get('/foo_path')
199 self.assertEqual('200 OK', response.status)
200 self.assertEqual(True, self.decorator.has_credentials())
JacobMoshenko8e905102011-06-20 09:53:10 -0400201 self.assertEqual('foo_refresh_token',
202 self.decorator.credentials.refresh_token)
203 self.assertEqual('foo_access_token',
204 self.decorator.credentials.access_token)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400205
Joe Gregorio562b7312011-09-15 09:06:38 -0400206 # Invalidate the stored Credentials.
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400207 self.decorator.credentials.invalid = True
208 self.decorator.credentials.store.put(self.decorator.credentials)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400209
Joe Gregorio562b7312011-09-15 09:06:38 -0400210 # Invalid Credentials should start the OAuth dance again.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400211 response = self.app.get('/foo_path')
212 self.assertTrue(response.status.startswith('302'))
213 q = parse_qs(response.headers['Location'].split('?', 1)[1])
214 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
215
Joe Gregorioec75dc12012-02-06 13:40:42 -0500216 def test_storage_delete(self):
217 # An initial request to an oauth_required decorated path should be a
218 # redirect to start the OAuth dance.
219 response = self.app.get('/foo_path')
220 self.assertTrue(response.status.startswith('302'))
221
222 # Now simulate the callback to /oauth2callback.
223 response = self.app.get('/oauth2callback', {
224 'code': 'foo_access_code',
225 'state': 'foo_path',
226 })
227 self.assertEqual('http://localhost/foo_path', response.headers['Location'])
228 self.assertEqual(None, self.decorator.credentials)
229
230 # Now requesting the decorated path should work.
231 response = self.app.get('/foo_path')
232
233 # Invalidate the stored Credentials.
234 self.decorator.credentials.store.delete()
235
236 # Invalid Credentials should start the OAuth dance again.
237 response = self.app.get('/foo_path')
238 self.assertTrue(response.status.startswith('302'))
239
Joe Gregorio432f17e2011-05-22 23:18:00 -0400240 def test_aware(self):
Joe Gregorio562b7312011-09-15 09:06:38 -0400241 # An initial request to an oauth_aware decorated path should not redirect.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400242 response = self.app.get('/bar_path')
243 self.assertEqual('Hello World!', response.body)
244 self.assertEqual('200 OK', response.status)
245 self.assertEqual(False, self.decorator.has_credentials())
246 url = self.decorator.authorize_url()
247 q = parse_qs(url.split('?', 1)[1])
248 self.assertEqual('http://localhost/oauth2callback', q['redirect_uri'][0])
249 self.assertEqual('foo_client_id', q['client_id'][0])
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400250 self.assertEqual('foo_scope bar_scope', q['scope'][0])
Joe Gregorio432f17e2011-05-22 23:18:00 -0400251 self.assertEqual('http://localhost/bar_path', q['state'][0])
252 self.assertEqual('code', q['response_type'][0])
253
Joe Gregorio562b7312011-09-15 09:06:38 -0400254 # Now simulate the callback to /oauth2callback.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400255 url = self.decorator.authorize_url()
256 response = self.app.get('/oauth2callback', {
257 'code': 'foo_access_code',
JacobMoshenko8e905102011-06-20 09:53:10 -0400258 'state': 'bar_path',
Joe Gregorio432f17e2011-05-22 23:18:00 -0400259 })
260 self.assertEqual('http://localhost/bar_path', response.headers['Location'])
261 self.assertEqual(False, self.decorator.has_credentials())
262
Joe Gregorio562b7312011-09-15 09:06:38 -0400263 # Now requesting the decorated path will have credentials.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400264 response = self.app.get('/bar_path')
265 self.assertEqual('200 OK', response.status)
266 self.assertEqual('Hello World!', response.body)
267 self.assertEqual(True, self.decorator.has_credentials())
JacobMoshenko8e905102011-06-20 09:53:10 -0400268 self.assertEqual('foo_refresh_token',
269 self.decorator.credentials.refresh_token)
270 self.assertEqual('foo_access_token',
271 self.decorator.credentials.access_token)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400272
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500273
274 def test_kwargs_are_passed_to_underlying_flow(self):
275 decorator = OAuth2Decorator(client_id='foo_client_id',
276 client_secret='foo_client_secret',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100277 user_agent='foo_user_agent',
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500278 scope=['foo_scope', 'bar_scope'],
279 access_type='offline',
280 approval_prompt='force')
281 self.assertEqual('offline', decorator.flow.params['access_type'])
282 self.assertEqual('force', decorator.flow.params['approval_prompt'])
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100283 self.assertEqual('foo_user_agent', decorator.flow.user_agent)
284 self.assertEqual(None, decorator.flow.params.get('user_agent', None))
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500285
286
Joe Gregorio432f17e2011-05-22 23:18:00 -0400287if __name__ == '__main__':
288 unittest.main()