diff --git a/appengine/standard/firebase/firetactoe/firetactoe.py b/appengine/standard/firebase/firetactoe/firetactoe.py index ec84abf90c84..c46203b33f8c 100644 --- a/appengine/standard/firebase/firetactoe/firetactoe.py +++ b/appengine/standard/firebase/firetactoe/firetactoe.py @@ -31,8 +31,8 @@ from google.appengine.api import app_identity from google.appengine.api import users from google.appengine.ext import ndb -import httplib2 -from oauth2client.client import GoogleCredentials +from google.auth.transport.requests import AuthorizedSession +import google.auth _FIREBASE_CONFIG = '_firebase_config.html' @@ -73,17 +73,13 @@ def _get_firebase_db_url(): return url.group(1) -# Memoize the authorized http, to avoid fetching new access tokens +# Memoize the authorized session, to avoid fetching new access tokens @lru_cache() -def _get_http(): - """Provides an authed http object.""" - http = httplib2.Http() - # Use application default credentials to make the Firebase calls - # https://firebase.google.com/docs/reference/rest/database/user-auth - creds = GoogleCredentials.get_application_default().create_scoped( - _FIREBASE_SCOPES) - creds.authorize(http) - return http +def _get_session(): + """Provides an authed requests session object.""" + creds, _ = google.auth.default(scopes=[_FIREBASE_SCOPES]) + authed_session = AuthorizedSession(creds) + return authed_session def _send_firebase_message(u_id, message=None): @@ -95,9 +91,9 @@ def _send_firebase_message(u_id, message=None): url = '{}/channels/{}.json'.format(_get_firebase_db_url(), u_id) if message: - return _get_http().request(url, 'PATCH', body=message) + return _get_session().patch(url, body=message) else: - return _get_http().request(url, 'DELETE') + return _get_session().delete(url) def create_custom_token(uid, valid_minutes=60): diff --git a/appengine/standard/firebase/firetactoe/firetactoe_test.py b/appengine/standard/firebase/firetactoe/firetactoe_test.py index f5ec1ca9573d..8e12092cb184 100644 --- a/appengine/standard/firebase/firetactoe/firetactoe_test.py +++ b/appengine/standard/firebase/firetactoe/firetactoe_test.py @@ -12,39 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json +import mock import re from google.appengine.api import users from google.appengine.ext import ndb -import httplib2 +from six.moves import http_client import pytest import webtest import firetactoe -class MockHttp(object): - """Mock the Http object, so we can set what the response will be.""" - def __init__(self, status, content=''): - self.content = content - self.status = status - self.request_url = None +class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code - def __call__(self, *args, **kwargs): - return self - - def request(self, url, method, content='', *args, **kwargs): - self.request_url = url - self.request_method = method - self.request_content = content - return self, self.content + def json(self): + return self.json_data @pytest.fixture def app(testbed, monkeypatch, login): # Don't let the _get_http function memoize its value - firetactoe._get_http.cache_clear() + firetactoe._get_session.cache_clear() # Provide a test firebase config. The following will set the databaseURL # databaseURL: "http://firebase.com/test-db-url" @@ -58,104 +50,147 @@ def app(testbed, monkeypatch, login): def test_index_new_game(app, monkeypatch): - mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) - monkeypatch.setattr(httplib2, 'Http', mock_http) + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.request", autospec=True + ) as auth_session: + data = {'access_token': '123'} + auth_session.return_value = MockResponse(data, http_client.OK) - response = app.get('/') + response = app.get('/') - assert 'g=' in response.body - # Look for the unique game token - assert re.search( - r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body) + assert 'g=' in response.body + # Look for the unique game token + assert re.search( + r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body) - assert firetactoe.Game.query().count() == 1 + assert firetactoe.Game.query().count() == 1 - assert mock_http.request_url.startswith( - 'http://firebase.com/test-db-url/channels/') - assert mock_http.request_method == 'PATCH' + auth_session.assert_called_once_with( + mock.ANY, # AuthorizedSession object + method="PATCH", + url="http://firebase.com/test-db-url/channels/3838.json", + body='{"winner": null, "userX": "38", "moveX": true, "winningBoard": null, "board": " ", "userO": null}', + data=None, + ) def test_index_existing_game(app, monkeypatch): - mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) - monkeypatch.setattr(httplib2, 'Http', mock_http) - userX = users.User('x@example.com', _user_id='123') - firetactoe.Game(id='razem', userX=userX).put() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.request", autospec=True + ) as auth_session: + data = {'access_token': '123'} + auth_session.return_value = MockResponse(data, http_client.OK) + + userX = users.User('x@example.com', _user_id='123') + firetactoe.Game(id='razem', userX=userX).put() - response = app.get('/?g=razem') + response = app.get('/?g=razem') - assert 'g=' in response.body - # Look for the unique game token - assert re.search( - r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body) + assert 'g=' in response.body + # Look for the unique game token + assert re.search( + r'initGame[^\n]+\'[\w+/=]+\.[\w+/=]+\.[\w+/=]+\'', response.body) - assert firetactoe.Game.query().count() == 1 - game = ndb.Key('Game', 'razem').get() - assert game is not None - assert game.userO.user_id() == '38' + assert firetactoe.Game.query().count() == 1 + game = ndb.Key('Game', 'razem').get() + assert game is not None + assert game.userO.user_id() == '38' - assert mock_http.request_url.startswith( - 'http://firebase.com/test-db-url/channels/') - assert mock_http.request_method == 'PATCH' + auth_session.assert_called_once_with( + mock.ANY, # AuthorizedSession object + method="PATCH", + url="http://firebase.com/test-db-url/channels/38razem.json", + body='{"winner": null, "userX": "123", "moveX": null, "winningBoard": null, "board": null, "userO": "38"}', + data=None, + ) def test_index_nonexisting_game(app, monkeypatch): - mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) - monkeypatch.setattr(httplib2, 'Http', mock_http) - firetactoe.Game(id='razem', userX=users.get_current_user()).put() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.request", autospec=True + ) as auth_session: + data = {'access_token': '123'} + auth_session.return_value = MockResponse(data, http_client.OK) - app.get('/?g=razemfrazem', status=404) + firetactoe.Game(id='razem', userX=users.get_current_user()).put() - assert mock_http.request_url is None + app.get('/?g=razemfrazem', status=404) + + assert not auth_session.called def test_opened(app, monkeypatch): - mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) - monkeypatch.setattr(httplib2, 'Http', mock_http) - firetactoe.Game(id='razem', userX=users.get_current_user()).put() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.request", autospec=True + ) as auth_session: + data = {'access_token': '123'} + auth_session.return_value = MockResponse(data, http_client.OK) + firetactoe.Game(id='razem', userX=users.get_current_user()).put() - app.post('/opened?g=razem', status=200) + app.post('/opened?g=razem', status=200) - assert mock_http.request_url.startswith( - 'http://firebase.com/test-db-url/channels/') - assert mock_http.request_method == 'PATCH' + auth_session.assert_called_once_with( + mock.ANY, # AuthorizedSession object + method="PATCH", + url="http://firebase.com/test-db-url/channels/38razem.json", + body='{"winner": null, "userX": "38", "moveX": null, "winningBoard": null, "board": null, "userO": null}', + data=None, + ) def test_bad_move(app, monkeypatch): - mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) - monkeypatch.setattr(httplib2, 'Http', mock_http) - firetactoe.Game( - id='razem', userX=users.get_current_user(), board=9*' ', - moveX=True).put() + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.request", autospec=True + ) as auth_session: + data = {'access_token': '123'} + auth_session.return_value = MockResponse(data, http_client.OK) - app.post('/move?g=razem', {'i': 10}, status=400) + firetactoe.Game( + id='razem', userX=users.get_current_user(), board=9*' ', + moveX=True).put() - assert mock_http.request_url is None + app.post('/move?g=razem', {'i': 10}, status=400) + assert not auth_session.called -def test_move(app, monkeypatch): - mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) - monkeypatch.setattr(httplib2, 'Http', mock_http) - firetactoe.Game( - id='razem', userX=users.get_current_user(), board=9*' ', - moveX=True).put() - app.post('/move?g=razem', {'i': 0}, status=200) +def test_move(app, monkeypatch): + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.request", autospec=True + ) as auth_session: + data = {'access_token': '123'} + auth_session.return_value = MockResponse(data, http_client.OK) - game = ndb.Key('Game', 'razem').get() - assert game.board == 'X' + (8 * ' ') + firetactoe.Game( + id='razem', userX=users.get_current_user(), board=9*' ', + moveX=True).put() - assert mock_http.request_url.startswith( - 'http://firebase.com/test-db-url/channels/') - assert mock_http.request_method == 'PATCH' + app.post('/move?g=razem', {'i': 0}, status=200) + game = ndb.Key('Game', 'razem').get() + assert game.board == 'X' + (8 * ' ') -def test_delete(app, monkeypatch): - mock_http = MockHttp(200, content=json.dumps({'access_token': '123'})) - monkeypatch.setattr(httplib2, 'Http', mock_http) - firetactoe.Game(id='razem', userX=users.get_current_user()).put() + auth_session.assert_called_once_with( + mock.ANY, # AuthorizedSession object + method="PATCH", + url="http://firebase.com/test-db-url/channels/38razem.json", + body='{"winner": null, "userX": "38", "moveX": false, "winningBoard": null, "board": "X ", "userO": null}', + data=None, + ) - app.post('/delete?g=razem', status=200) - assert mock_http.request_url.startswith( - 'http://firebase.com/test-db-url/channels/') - assert mock_http.request_method == 'DELETE' +def test_delete(app, monkeypatch): + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.request", autospec=True + ) as auth_session: + data = {'access_token': '123'} + auth_session.return_value = MockResponse(data, http_client.OK) + firetactoe.Game(id='razem', userX=users.get_current_user()).put() + + app.post('/delete?g=razem', status=200) + + auth_session.assert_called_once_with( + mock.ANY, # AuthorizedSession object + method="DELETE", + url="http://firebase.com/test-db-url/channels/38razem.json", + ) diff --git a/appengine/standard/firebase/firetactoe/requirements-test.txt b/appengine/standard/firebase/firetactoe/requirements-test.txt index 93474254bab8..6fc424d9cd4f 100644 --- a/appengine/standard/firebase/firetactoe/requirements-test.txt +++ b/appengine/standard/firebase/firetactoe/requirements-test.txt @@ -1,2 +1,3 @@ pytest==4.6.9 WebTest==2.0.34 +mock==3.0.5; python_version < "3" \ No newline at end of file diff --git a/appengine/standard/firebase/firetactoe/requirements.txt b/appengine/standard/firebase/firetactoe/requirements.txt index b354f6bc6824..4c781efd19b5 100644 --- a/appengine/standard/firebase/firetactoe/requirements.txt +++ b/appengine/standard/firebase/firetactoe/requirements.txt @@ -1,5 +1,5 @@ flask==1.1.2 requests==2.23.0 requests_toolbelt==0.9.1 -oauth2client==4.1.3 +google-auth==1.14.2 functools32==3.2.3.post2; python_version < "3" diff --git a/appengine/standard/firebase/firetactoe/rest_api.py b/appengine/standard/firebase/firetactoe/rest_api.py index a067953a8f13..7c8a2ee59fae 100644 --- a/appengine/standard/firebase/firetactoe/rest_api.py +++ b/appengine/standard/firebase/firetactoe/rest_api.py @@ -21,25 +21,23 @@ # [START rest_writing_data] import json -import httplib2 -from oauth2client.client import GoogleCredentials +from google.auth.transport.requests import AuthorizedSession +import google.auth _FIREBASE_SCOPES = [ 'https://www.googleapis.com/auth/firebase.database', 'https://www.googleapis.com/auth/userinfo.email'] -# Memoize the authorized http, to avoid fetching new access tokens +# Memoize the authorized session, to avoid fetching new access tokens @lru_cache() -def _get_http(): - """Provides an authed http object.""" - http = httplib2.Http() +def _get_session(): + """Provides an authed requests session object.""" + creds, _ = google.auth.default(scopes=[_FIREBASE_SCOPES]) # Use application default credentials to make the Firebase calls # https://firebase.google.com/docs/reference/rest/database/user-auth - creds = GoogleCredentials.get_application_default().create_scoped( - _FIREBASE_SCOPES) - creds.authorize(http) - return http + authed_session = AuthorizedSession(creds) + return authed_session def firebase_put(path, value=None): @@ -52,7 +50,7 @@ def firebase_put(path, value=None): path - the url to the Firebase object to write. value - a json string. """ - response, content = _get_http().request(path, method='PUT', body=value) + response, content = _get_session().put(path, body=value) return json.loads(content) @@ -66,7 +64,7 @@ def firebase_patch(path, value=None): path - the url to the Firebase object to write. value - a json string. """ - response, content = _get_http().request(path, method='PATCH', body=value) + response, content = _get_session().patch(path, body=value) return json.loads(content) @@ -82,7 +80,7 @@ def firebase_post(path, value=None): path - the url to the Firebase list to append to. value - a json string. """ - response, content = _get_http().request(path, method='POST', body=value) + response, content = _get_session().post(path, body=value) return json.loads(content) # [END rest_writing_data] @@ -97,7 +95,7 @@ def firebase_get(path): Args: path - the url to the Firebase object to read. """ - response, content = _get_http().request(path, method='GET') + response, content = _get_session().get(path) return json.loads(content) @@ -111,4 +109,4 @@ def firebase_delete(path): Args: path - the url to the Firebase object to delete. """ - response, content = _get_http().request(path, method='DELETE') + response, content = _get_session().delete(path)