Skip to content

Commit

Permalink
Remove oauth2client from AppEngine Standard Firebase (#3718)
Browse files Browse the repository at this point in the history
  • Loading branch information
busunkim96 authored May 13, 2020
1 parent 67d4aeb commit 41d7204
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 113 deletions.
24 changes: 10 additions & 14 deletions appengine/standard/firebase/firetactoe/firetactoe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
201 changes: 118 additions & 83 deletions appengine/standard/firebase/firetactoe/firetactoe_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
)
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pytest==4.6.9
WebTest==2.0.34
mock==3.0.5; python_version < "3"
2 changes: 1 addition & 1 deletion appengine/standard/firebase/firetactoe/requirements.txt
Original file line number Diff line number Diff line change
@@ -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"
28 changes: 13 additions & 15 deletions appengine/standard/firebase/firetactoe/rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)


Expand All @@ -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)


Expand All @@ -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]

Expand All @@ -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)


Expand All @@ -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)

0 comments on commit 41d7204

Please sign in to comment.