Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth test utility method #183 #184

Merged
merged 1 commit into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Changelog

### Changed
**Releases 0.0.29 to 0.1.3** -
**Releases 0.0.29 to 0.1.4** -

- README.md references `user` but example table is `users`. [Issue #154](https://github.com/joegasewicz/flask-jwt-router/issues/154)
- Add OAuth 2.0 & compatibility with react-google-oauth2 npm pkg. [Issue #158](https://github.com/joegasewicz/flask-jwt-router/issues/158)
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,33 @@ In your React app directory install react-google-oauth2.0:
npm install react-google-oauth2 --save
```

## Testing

Testing OAuth2.0 in a Flask app is non-trivial, especially if you rely on Flask-JWT-Router
to append your user onto Flask's global context (or `g`). Therefore we have provided a
utility method that returns a headers Dict that you can then use in your test view handler
request. This example is using the Pytest library:

```python
@pytest.fixture()
def client():
# See https://flask.palletsprojects.com/en/1.1.x/testing/ for details


def test_blogs(client):
user_headers = jwt_routes.google.create_test_headers(email="user@gmail.com")
rv = client.get("/blogs", headers=user_headers)
```

If you are not running a db in your tests, then you can use the `entity` kwarg.
For example:

```python
# user is an instantiated SqlAlchemy object
user_headers = jwt_routes.google.create_test_headers(email="user@gmail.com", entity=user)
# user_headers: { "X-Auth-Token": "Bearer <GOOGLE_OAUTH2_TEST>" }
```


## Authors

Expand Down
26 changes: 18 additions & 8 deletions flask_jwt_router/_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,23 +167,33 @@ def _handle_token(self):
:return None:
"""
self.entity.clean_up()
entity = None
try:
if request.args.get("auth"):
token = request.args.get("auth")
elif request.headers.get("X-Auth-Token") and self.google:
bearer = request.headers.get("X-Auth-Token")
token = bearer.split("Bearer ")[1]
try:
# Currently token refreshing is not supported, so pass the current token through
auth_results = self.google.authorize(token)
email = auth_results["email"]
if self.google.test_metadata:
email = self.google.test_metadata["email"]
entity = self.google.test_metadata["entity"]
else:
# Currently token refreshing is not supported, so pass the current token through
auth_results = self.google.authorize(token)
email = auth_results["email"]
self.entity.oauth_entity_key = self.config.oauth_entity
entity = self.entity.get_entity_from_token_or_tablename(
tablename=self.google.tablename,
email_value=email,
)
setattr(g, self.entity.get_entity_from_ext().__tablename__, entity)
if not entity:
entity = self.entity.get_entity_from_token_or_tablename(
tablename=self.google.tablename,
email_value=email,
)
setattr(g, self.entity.get_entity_from_ext().__tablename__, entity)
else:
setattr(g, entity.__tablename__, entity)
setattr(g, "access_token", token)
# Clean up google test util
self.google.test_metadata = None
return None
except InvalidTokenError:
return abort(401)
Expand Down
4 changes: 4 additions & 0 deletions flask_jwt_router/oauth2/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ def init(self, *, client_id, client_secret, redirect_uri, expires_in, email_fiel
@abstractmethod
def oauth_login(self, request: _FlaskRequestType) -> Dict:
pass

@abstractmethod
def create_test_headers(self, *, email: str, entity=None) -> Dict[str, str]:
pass
56 changes: 56 additions & 0 deletions flask_jwt_router/oauth2/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,31 @@ def login():
This will allow the user to gain access to your Flask's app resources until the access token's
expire time has ended. The client should then decide whether to redirect the user to Google's
login screen.

Testing
+++++++

Testing OAuth2.0 in a Flask app is non-trivial, especially if you rely on Flask-JWT-Router
to append your user onto Flask's global context (or `g`). Therefore we have provided a
utility method that returns a headers Dict that you can then use in your test view handler
request. This example is using the Pytest library::

@pytest.fixture()
def client():
# See https://flask.palletsprojects.com/en/1.1.x/testing/ for details


def test_blogs(client):
user_headers = jwt_routes.google.create_test_headers(email="user@gmail.com")
rv = client.get("/blogs", headers=user_headers)

If you are not running a db in your tests, then you can use the `entity` kwarg.
For example::

# user is an instantiated SqlAlchemy object
user_headers = jwt_routes.google.create_test_headers(email="user@gmail.com", entity=user)
# user_headers: { "X-Auth-Token": "Bearer <GOOGLE_OAUTH2_TEST>" }

"""
from typing import Dict
from abc import ABC, abstractmethod
Expand Down Expand Up @@ -131,6 +156,8 @@ class Google(BaseOAuth):

_data: Dict

test_metadata: Dict[str, str] = None

def __init__(self, http):
self.http = http

Expand Down Expand Up @@ -210,3 +237,32 @@ def _set_expires(self):
:return: None
"""
self.expires_in = 3600 * 24 * 7

def create_test_headers(self, *, email: str, entity=None) -> Dict[str, str]:
"""
:key email: SqlAlchemy object will be filtered against the email value.
:key entity: SqlAlchemy object if you prefer not to run a db in your tests.

If you are running your tests against a test db then just pass in the `email` kwarg.
For example::

user_headers = jwt_routes.google.create_test_headers(email="user@gmail.com")
# user_headers: { "X-Auth-Token": "Bearer <GOOGLE_OAUTH2_TEST>" }

If you are not running a db in your tests, then you can use the `entity` kwarg.
For example::

# user is an instantiated SqlAlchemy object
user_headers = jwt_routes.google.create_test_headers(email="user@gmail.com", entity=user)
# user_headers: { "X-Auth-Token": "Bearer <GOOGLE_OAUTH2_TEST>" }


:return: Python Dict containing header key value for OAuth routing with FJR
"""
self.test_metadata = {
"email": email,
"entity": entity,
}
return {
"X-Auth-Token": "Bearer <GOOGLE_OAUTH2_TEST>",
}
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name='flask-jwt-router',
version='0.1.3',
version='0.1.4',
description='Flask JWT Router is a Python library that adds authorised routes to a Flask app',
packages=["flask_jwt_router", "flask_jwt_router.oauth2"],
classifiers=[
Expand Down
1 change: 0 additions & 1 deletion tests/fixtures/app_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def jwt_router_client(request):
ctx.push()

yield client

ctx.pop()


Expand Down
12 changes: 10 additions & 2 deletions tests/fixtures/main_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ def google_exchange():
}


@flask_app.route("/api/v1/test_google_oauth", methods=["GET"])
def request_google_oauth():
oauth_tablename = g.oauth_tablename
return {
"email": oauth_tablename.email,
}, 200


@pytest.fixture(scope='module')
def request_client():
flask_app.config["SECRET_KEY"] = "__TEST_SECRET__"
Expand All @@ -91,8 +99,8 @@ def request_client():
("GET", "/ignore"),
]

from tests.fixtures.models import TeacherModel
flask_app.config["ENTITY_MODELS"] = [TeacherModel]
from tests.fixtures.models import TeacherModel, OAuthUserModel
flask_app.config["ENTITY_MODELS"] = [TeacherModel, OAuthUserModel]

google_oauth = {
"client_id": "<CLIENT_ID>",
Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ class TeacherModel(db.Model):

teacher_id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(300))


class OAuthUserModel(db.Model):
__tablename__ = "oauth_tablename"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(300))
10 changes: 10 additions & 0 deletions tests/fixtures/oauth_fixtures.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest

from tests.fixtures.main_fixture import db
from tests.fixtures.models import OAuthUserModel

TEST_OAUTH_URL = {
"local_flask": "http://localhost:5009/ibanez/api/v1/staffs/login",
Expand All @@ -15,6 +17,14 @@ def oauth_urls():
return TEST_OAUTH_URL


@pytest.fixture
def google_oauth_user():
oauth_user = OAuthUserModel(email="test_one@oauth.com")
db.session.add(oauth_user)
db.session.commit()
return oauth_user


@pytest.fixture
def http_requests():
def inner(_code="<CODE>"):
Expand Down
32 changes: 14 additions & 18 deletions tests/oauth2/test_google.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from flask_jwt_router.oauth2.http_requests import HttpRequests
from flask_jwt_router.oauth2._urls import GOOGLE_OAUTH_URL
from tests.fixtures.oauth_fixtures import TEST_OAUTH_URL, http_requests
from tests.fixtures.model_fixtures import MockAOuthModel

mock_exchange_response = {
"access_token": "<access_token>",
Expand Down Expand Up @@ -97,7 +98,6 @@ def test_oauth_login(self, http_requests):
result = g.oauth_login(_MockFlaskRequest())
assert result["access_token"] == "<access_token>"

@pytest.mark.skip
def test_authorize(self, http_requests):
"""
{
Expand All @@ -117,22 +117,18 @@ def test_authorize(self, http_requests):
result = g.authorize(token)
assert "email" in result

@pytest.mark.skip
def test_local_oauth_login(self):
class _Request(_FlaskRequestType): # TODO make into fixture
base_url = "http://localhost:3000/google_login"

@staticmethod
def get_json() -> Dict:
return {
"code": "",
"scope": "",
}
def test_create_test_headers(self, http_requests, MockAOuthModel):

mock_user = MockAOuthModel(email="test@email.com")
g = Google(http_requests(GOOGLE_OAUTH_URL))
g.init(**{
"client_id": "",
"client_secret": "",
"redirect_uri": "",
})
result = g.oauth_login(_Request())
g.init(**self.mock_options)

result = g.create_test_headers(email="test@email.com")
assert result == {'X-Auth-Token': 'Bearer <GOOGLE_OAUTH2_TEST>'}
assert g.test_metadata["email"] == "test@email.com"
assert g.test_metadata["entity"] is None

result = g.create_test_headers(email="test@email.com", entity=mock_user)
assert result == {'X-Auth-Token': 'Bearer <GOOGLE_OAUTH2_TEST>'}
assert g.test_metadata["email"] == "test@email.com"
assert g.test_metadata["entity"] == mock_user
34 changes: 30 additions & 4 deletions tests/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
from tests.fixtures.token_fixture import mock_token, mock_access_token
from tests.fixtures.model_fixtures import TestMockEntity, MockAOuthModel
from tests.fixtures.app_fixtures import jwt_router_client, test_client_static
from tests.fixtures.main_fixture import request_client
from tests.fixtures.oauth_fixtures import http_requests, oauth_urls
from tests.fixtures.main_fixture import request_client, jwt_routes
from tests.fixtures.oauth_fixtures import http_requests, oauth_urls, google_oauth_user


class MockArgs:
Expand Down Expand Up @@ -106,7 +106,6 @@ def fc_one():
routing.before_middleware()
assert ctx.g.test_entities == [(1, 'joe')]


with ctx:
# token from oauth headers
monkeypatch.setattr("flask.request.headers", MockArgs("<access_token>", "X-Auth-Token"))
Expand All @@ -117,7 +116,6 @@ def fc_one():
routing.before_middleware()
assert ctx.g.oauth_tablename == [(1, "jaco@gmail.com")]


# Fixes bug - "entity key state gets stale between requests #171"
# https://github.com/joegasewicz/flask-jwt-router/issues/171
with ctx:
Expand Down Expand Up @@ -188,3 +186,31 @@ def test_ignored_route_path(self, request_client):
def test_handle_pre_flight_request(self, request_client):
rv = request_client.options("/")
assert "200" in str(rv.status)

def test_routing_with_google_create_test_headers(self, request_client, MockAOuthModel, google_oauth_user):
email = "test_one@oauth.com"
test_user = MockAOuthModel(email="test_one@oauth.com")

assert jwt_routes.google.test_metadata is None
# Pure stateless test with no db
oauth_headers = jwt_routes.google.create_test_headers(email=email, entity=test_user)

assert jwt_routes.google.test_metadata == {"email": email, "entity": test_user}
assert oauth_headers == {'X-Auth-Token': 'Bearer <GOOGLE_OAUTH2_TEST>'}

rv = request_client.get("/api/v1/test_google_oauth", headers=oauth_headers)
assert "200" in str(rv.status)
assert email == rv.get_json()["email"]
assert jwt_routes.google.test_metadata is None

# Tests with side effects to db
oauth_headers = jwt_routes.google.create_test_headers(email=google_oauth_user.email)

assert jwt_routes.google.test_metadata == {"email": email, "entity": None}
assert oauth_headers == {'X-Auth-Token': 'Bearer <GOOGLE_OAUTH2_TEST>'}

rv = request_client.get("/api/v1/test_google_oauth", headers=oauth_headers)
assert "200" in str(rv.status)
assert email == rv.get_json()["email"]
assert jwt_routes.google.test_metadata is None