Skip to content

Commit 41d7204

Browse files
authored
Remove oauth2client from AppEngine Standard Firebase (#3718)
Closes #2648 This is the last project using oauth2client. [search result](https://github.com/GoogleCloudPlatform/python-docs-samples/search?q=oauth2client&unscoped_q=oauth2client)
1 parent 67d4aeb commit 41d7204

File tree

5 files changed

+143
-113
lines changed

5 files changed

+143
-113
lines changed

appengine/standard/firebase/firetactoe/firetactoe.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
from google.appengine.api import app_identity
3232
from google.appengine.api import users
3333
from google.appengine.ext import ndb
34-
import httplib2
35-
from oauth2client.client import GoogleCredentials
34+
from google.auth.transport.requests import AuthorizedSession
35+
import google.auth
3636

3737

3838
_FIREBASE_CONFIG = '_firebase_config.html'
@@ -73,17 +73,13 @@ def _get_firebase_db_url():
7373
return url.group(1)
7474

7575

76-
# Memoize the authorized http, to avoid fetching new access tokens
76+
# Memoize the authorized session, to avoid fetching new access tokens
7777
@lru_cache()
78-
def _get_http():
79-
"""Provides an authed http object."""
80-
http = httplib2.Http()
81-
# Use application default credentials to make the Firebase calls
82-
# https://firebase.google.com/docs/reference/rest/database/user-auth
83-
creds = GoogleCredentials.get_application_default().create_scoped(
84-
_FIREBASE_SCOPES)
85-
creds.authorize(http)
86-
return http
78+
def _get_session():
79+
"""Provides an authed requests session object."""
80+
creds, _ = google.auth.default(scopes=[_FIREBASE_SCOPES])
81+
authed_session = AuthorizedSession(creds)
82+
return authed_session
8783

8884

8985
def _send_firebase_message(u_id, message=None):
@@ -95,9 +91,9 @@ def _send_firebase_message(u_id, message=None):
9591
url = '{}/channels/{}.json'.format(_get_firebase_db_url(), u_id)
9692

9793
if message:
98-
return _get_http().request(url, 'PATCH', body=message)
94+
return _get_session().patch(url, body=message)
9995
else:
100-
return _get_http().request(url, 'DELETE')
96+
return _get_session().delete(url)
10197

10298

10399
def create_custom_token(uid, valid_minutes=60):

appengine/standard/firebase/firetactoe/firetactoe_test.py

Lines changed: 118 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,31 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import json
15+
import mock
1616
import re
1717

1818
from google.appengine.api import users
1919
from google.appengine.ext import ndb
20-
import httplib2
20+
from six.moves import http_client
2121
import pytest
2222
import webtest
2323

2424
import firetactoe
2525

2626

27-
class MockHttp(object):
28-
"""Mock the Http object, so we can set what the response will be."""
29-
def __init__(self, status, content=''):
30-
self.content = content
31-
self.status = status
32-
self.request_url = None
27+
class MockResponse:
28+
def __init__(self, json_data, status_code):
29+
self.json_data = json_data
30+
self.status_code = status_code
3331

34-
def __call__(self, *args, **kwargs):
35-
return self
36-
37-
def request(self, url, method, content='', *args, **kwargs):
38-
self.request_url = url
39-
self.request_method = method
40-
self.request_content = content
41-
return self, self.content
32+
def json(self):
33+
return self.json_data
4234

4335

4436
@pytest.fixture
4537
def app(testbed, monkeypatch, login):
4638
# Don't let the _get_http function memoize its value
47-
firetactoe._get_http.cache_clear()
39+
firetactoe._get_session.cache_clear()
4840

4941
# Provide a test firebase config. The following will set the databaseURL
5042
# databaseURL: "http://firebase.com/test-db-url"
@@ -58,104 +50,147 @@ def app(testbed, monkeypatch, login):
5850

5951

6052
def test_index_new_game(app, monkeypatch):
61-
mock_http = MockHttp(200, content=json.dumps({'access_token': '123'}))
62-
monkeypatch.setattr(httplib2, 'Http', mock_http)
53+
with mock.patch(
54+
"google.auth.transport.requests.AuthorizedSession.request", autospec=True
55+
) as auth_session:
56+
data = {'access_token': '123'}
57+
auth_session.return_value = MockResponse(data, http_client.OK)
6358

64-
response = app.get('/')
59+
response = app.get('/')
6560

66-
assert 'g=' in response.body
67-
# Look for the unique game token
68-
assert re.search(
69-
r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body)
61+
assert 'g=' in response.body
62+
# Look for the unique game token
63+
assert re.search(
64+
r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body)
7065

71-
assert firetactoe.Game.query().count() == 1
66+
assert firetactoe.Game.query().count() == 1
7267

73-
assert mock_http.request_url.startswith(
74-
'http://firebase.com/test-db-url/channels/')
75-
assert mock_http.request_method == 'PATCH'
68+
auth_session.assert_called_once_with(
69+
mock.ANY, # AuthorizedSession object
70+
method="PATCH",
71+
url="http://firebase.com/test-db-url/channels/3838.json",
72+
body='{"winner": null, "userX": "38", "moveX": true, "winningBoard": null, "board": " ", "userO": null}',
73+
data=None,
74+
)
7675

7776

7877
def test_index_existing_game(app, monkeypatch):
79-
mock_http = MockHttp(200, content=json.dumps({'access_token': '123'}))
80-
monkeypatch.setattr(httplib2, 'Http', mock_http)
81-
userX = users.User('[email protected]', _user_id='123')
82-
firetactoe.Game(id='razem', userX=userX).put()
78+
with mock.patch(
79+
"google.auth.transport.requests.AuthorizedSession.request", autospec=True
80+
) as auth_session:
81+
data = {'access_token': '123'}
82+
auth_session.return_value = MockResponse(data, http_client.OK)
83+
84+
userX = users.User('[email protected]', _user_id='123')
85+
firetactoe.Game(id='razem', userX=userX).put()
8386

84-
response = app.get('/?g=razem')
87+
response = app.get('/?g=razem')
8588

86-
assert 'g=' in response.body
87-
# Look for the unique game token
88-
assert re.search(
89-
r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body)
89+
assert 'g=' in response.body
90+
# Look for the unique game token
91+
assert re.search(
92+
r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body)
9093

91-
assert firetactoe.Game.query().count() == 1
92-
game = ndb.Key('Game', 'razem').get()
93-
assert game is not None
94-
assert game.userO.user_id() == '38'
94+
assert firetactoe.Game.query().count() == 1
95+
game = ndb.Key('Game', 'razem').get()
96+
assert game is not None
97+
assert game.userO.user_id() == '38'
9598

96-
assert mock_http.request_url.startswith(
97-
'http://firebase.com/test-db-url/channels/')
98-
assert mock_http.request_method == 'PATCH'
99+
auth_session.assert_called_once_with(
100+
mock.ANY, # AuthorizedSession object
101+
method="PATCH",
102+
url="http://firebase.com/test-db-url/channels/38razem.json",
103+
body='{"winner": null, "userX": "123", "moveX": null, "winningBoard": null, "board": null, "userO": "38"}',
104+
data=None,
105+
)
99106

100107

101108
def test_index_nonexisting_game(app, monkeypatch):
102-
mock_http = MockHttp(200, content=json.dumps({'access_token': '123'}))
103-
monkeypatch.setattr(httplib2, 'Http', mock_http)
104-
firetactoe.Game(id='razem', userX=users.get_current_user()).put()
109+
with mock.patch(
110+
"google.auth.transport.requests.AuthorizedSession.request", autospec=True
111+
) as auth_session:
112+
data = {'access_token': '123'}
113+
auth_session.return_value = MockResponse(data, http_client.OK)
105114

106-
app.get('/?g=razemfrazem', status=404)
115+
firetactoe.Game(id='razem', userX=users.get_current_user()).put()
107116

108-
assert mock_http.request_url is None
117+
app.get('/?g=razemfrazem', status=404)
118+
119+
assert not auth_session.called
109120

110121

111122
def test_opened(app, monkeypatch):
112-
mock_http = MockHttp(200, content=json.dumps({'access_token': '123'}))
113-
monkeypatch.setattr(httplib2, 'Http', mock_http)
114-
firetactoe.Game(id='razem', userX=users.get_current_user()).put()
123+
with mock.patch(
124+
"google.auth.transport.requests.AuthorizedSession.request", autospec=True
125+
) as auth_session:
126+
data = {'access_token': '123'}
127+
auth_session.return_value = MockResponse(data, http_client.OK)
128+
firetactoe.Game(id='razem', userX=users.get_current_user()).put()
115129

116-
app.post('/opened?g=razem', status=200)
130+
app.post('/opened?g=razem', status=200)
117131

118-
assert mock_http.request_url.startswith(
119-
'http://firebase.com/test-db-url/channels/')
120-
assert mock_http.request_method == 'PATCH'
132+
auth_session.assert_called_once_with(
133+
mock.ANY, # AuthorizedSession object
134+
method="PATCH",
135+
url="http://firebase.com/test-db-url/channels/38razem.json",
136+
body='{"winner": null, "userX": "38", "moveX": null, "winningBoard": null, "board": null, "userO": null}',
137+
data=None,
138+
)
121139

122140

123141
def test_bad_move(app, monkeypatch):
124-
mock_http = MockHttp(200, content=json.dumps({'access_token': '123'}))
125-
monkeypatch.setattr(httplib2, 'Http', mock_http)
126-
firetactoe.Game(
127-
id='razem', userX=users.get_current_user(), board=9*' ',
128-
moveX=True).put()
142+
with mock.patch(
143+
"google.auth.transport.requests.AuthorizedSession.request", autospec=True
144+
) as auth_session:
145+
data = {'access_token': '123'}
146+
auth_session.return_value = MockResponse(data, http_client.OK)
129147

130-
app.post('/move?g=razem', {'i': 10}, status=400)
148+
firetactoe.Game(
149+
id='razem', userX=users.get_current_user(), board=9*' ',
150+
moveX=True).put()
131151

132-
assert mock_http.request_url is None
152+
app.post('/move?g=razem', {'i': 10}, status=400)
133153

154+
assert not auth_session.called
134155

135-
def test_move(app, monkeypatch):
136-
mock_http = MockHttp(200, content=json.dumps({'access_token': '123'}))
137-
monkeypatch.setattr(httplib2, 'Http', mock_http)
138-
firetactoe.Game(
139-
id='razem', userX=users.get_current_user(), board=9*' ',
140-
moveX=True).put()
141156

142-
app.post('/move?g=razem', {'i': 0}, status=200)
157+
def test_move(app, monkeypatch):
158+
with mock.patch(
159+
"google.auth.transport.requests.AuthorizedSession.request", autospec=True
160+
) as auth_session:
161+
data = {'access_token': '123'}
162+
auth_session.return_value = MockResponse(data, http_client.OK)
143163

144-
game = ndb.Key('Game', 'razem').get()
145-
assert game.board == 'X' + (8 * ' ')
164+
firetactoe.Game(
165+
id='razem', userX=users.get_current_user(), board=9*' ',
166+
moveX=True).put()
146167

147-
assert mock_http.request_url.startswith(
148-
'http://firebase.com/test-db-url/channels/')
149-
assert mock_http.request_method == 'PATCH'
168+
app.post('/move?g=razem', {'i': 0}, status=200)
150169

170+
game = ndb.Key('Game', 'razem').get()
171+
assert game.board == 'X' + (8 * ' ')
151172

152-
def test_delete(app, monkeypatch):
153-
mock_http = MockHttp(200, content=json.dumps({'access_token': '123'}))
154-
monkeypatch.setattr(httplib2, 'Http', mock_http)
155-
firetactoe.Game(id='razem', userX=users.get_current_user()).put()
173+
auth_session.assert_called_once_with(
174+
mock.ANY, # AuthorizedSession object
175+
method="PATCH",
176+
url="http://firebase.com/test-db-url/channels/38razem.json",
177+
body='{"winner": null, "userX": "38", "moveX": false, "winningBoard": null, "board": "X ", "userO": null}',
178+
data=None,
179+
)
156180

157-
app.post('/delete?g=razem', status=200)
158181

159-
assert mock_http.request_url.startswith(
160-
'http://firebase.com/test-db-url/channels/')
161-
assert mock_http.request_method == 'DELETE'
182+
def test_delete(app, monkeypatch):
183+
with mock.patch(
184+
"google.auth.transport.requests.AuthorizedSession.request", autospec=True
185+
) as auth_session:
186+
data = {'access_token': '123'}
187+
auth_session.return_value = MockResponse(data, http_client.OK)
188+
firetactoe.Game(id='razem', userX=users.get_current_user()).put()
189+
190+
app.post('/delete?g=razem', status=200)
191+
192+
auth_session.assert_called_once_with(
193+
mock.ANY, # AuthorizedSession object
194+
method="DELETE",
195+
url="http://firebase.com/test-db-url/channels/38razem.json",
196+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pytest==4.6.9
22
WebTest==2.0.34
3+
mock==3.0.5; python_version < "3"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
flask==1.1.2
22
requests==2.23.0
33
requests_toolbelt==0.9.1
4-
oauth2client==4.1.3
4+
google-auth==1.14.2
55
functools32==3.2.3.post2; python_version < "3"

appengine/standard/firebase/firetactoe/rest_api.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,23 @@
2121
# [START rest_writing_data]
2222
import json
2323

24-
import httplib2
25-
from oauth2client.client import GoogleCredentials
24+
from google.auth.transport.requests import AuthorizedSession
25+
import google.auth
2626

2727
_FIREBASE_SCOPES = [
2828
'https://www.googleapis.com/auth/firebase.database',
2929
'https://www.googleapis.com/auth/userinfo.email']
3030

3131

32-
# Memoize the authorized http, to avoid fetching new access tokens
32+
# Memoize the authorized session, to avoid fetching new access tokens
3333
@lru_cache()
34-
def _get_http():
35-
"""Provides an authed http object."""
36-
http = httplib2.Http()
34+
def _get_session():
35+
"""Provides an authed requests session object."""
36+
creds, _ = google.auth.default(scopes=[_FIREBASE_SCOPES])
3737
# Use application default credentials to make the Firebase calls
3838
# https://firebase.google.com/docs/reference/rest/database/user-auth
39-
creds = GoogleCredentials.get_application_default().create_scoped(
40-
_FIREBASE_SCOPES)
41-
creds.authorize(http)
42-
return http
39+
authed_session = AuthorizedSession(creds)
40+
return authed_session
4341

4442

4543
def firebase_put(path, value=None):
@@ -52,7 +50,7 @@ def firebase_put(path, value=None):
5250
path - the url to the Firebase object to write.
5351
value - a json string.
5452
"""
55-
response, content = _get_http().request(path, method='PUT', body=value)
53+
response, content = _get_session().put(path, body=value)
5654
return json.loads(content)
5755

5856

@@ -66,7 +64,7 @@ def firebase_patch(path, value=None):
6664
path - the url to the Firebase object to write.
6765
value - a json string.
6866
"""
69-
response, content = _get_http().request(path, method='PATCH', body=value)
67+
response, content = _get_session().patch(path, body=value)
7068
return json.loads(content)
7169

7270

@@ -82,7 +80,7 @@ def firebase_post(path, value=None):
8280
path - the url to the Firebase list to append to.
8381
value - a json string.
8482
"""
85-
response, content = _get_http().request(path, method='POST', body=value)
83+
response, content = _get_session().post(path, body=value)
8684
return json.loads(content)
8785
# [END rest_writing_data]
8886

@@ -97,7 +95,7 @@ def firebase_get(path):
9795
Args:
9896
path - the url to the Firebase object to read.
9997
"""
100-
response, content = _get_http().request(path, method='GET')
98+
response, content = _get_session().get(path)
10199
return json.loads(content)
102100

103101

@@ -111,4 +109,4 @@ def firebase_delete(path):
111109
Args:
112110
path - the url to the Firebase object to delete.
113111
"""
114-
response, content = _get_http().request(path, method='DELETE')
112+
response, content = _get_session().delete(path)

0 commit comments

Comments
 (0)