diff --git a/docs/examplesapp.rst b/docs/examplesapp.rst index b3a34838..fb74075e 100644 --- a/docs/examplesapp.rst +++ b/docs/examplesapp.rst @@ -45,3 +45,10 @@ CERN .. include:: ../examples/cern_app.py :start-after: SPHINX-START :end-before: SPHINX-END + +Globus +------ + +.. include:: ../examples/globus_app.py + :start-after: SPHINX-START + :end-before: SPHINX-END diff --git a/docs/usage.rst b/docs/usage.rst index 44e8b059..4b29f4b1 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -40,6 +40,10 @@ CERN .. automodule:: invenio_oauthclient.contrib.cern +Globus +------ +.. automodule:: invenio_oauthclient.contrib.globus + Advanced -------- diff --git a/examples/globus_app.py b/examples/globus_app.py new file mode 100644 index 00000000..03144bbf --- /dev/null +++ b/examples/globus_app.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2015, 2016, 2017 CERN. +# +# Invenio is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Invenio is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Invenio; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + +r"""Minimal Flask application example for development with globus handler. + +SPHINX-START + +1. Register a Globus application at `https://developers.globus.org/` with the + `Redirect URL` as `http://localhost:5000/oauth/authorized/globus/`. See + here for more documentation: + `https://docs.globus.org/api/auth/developer-guide/#register-app` + + +2. Grab the *Client ID* and *Client Secret* after registering the application + and add them to your instance configuration as `consumer_key` and + `consumer_secret`. + + .. code-block:: console + + $ export GLOBUS_APP_CREDENTIALS_KEY=my_globus_client_id + $ export GLOBUS_APP_CREDENTIALS_SECRET=my_globus_client_secret + +3. Create database and tables: + + .. code-block:: console + + $ cdvirtualenv src/invenio-oauthclient + $ pip install -e .[all] + $ cd examples + $ export FLASK_APP=globus_app.py + $ ./app-setup.py + +You can find the database in `examples/globus_app.db`. + +4. Run the development server: + + .. code-block:: console + + $ flask run -p 5000 -h '0.0.0.0' + +5. Open in a browser the page `http://localhost:5000/globus`. + + You will be redirected to globus to authorize the application. + + Click on `Allow` and you will be redirected back to + `http://localhost:5000/oauth/signup/globus/`, where you will be able to + finalize the local user registration. + +6. To clean up and drop tables: + + .. code-block:: console + + $ ./app-teardown.sh + +SPHINX-END + +""" + +from __future__ import absolute_import, print_function + +import os + +from flask import Flask, redirect, url_for +from flask_babelex import Babel +from flask_login import current_user +from flask_menu import Menu as FlaskMenu +from flask_oauthlib.client import OAuth as FlaskOAuth +from invenio_accounts import InvenioAccounts +from invenio_accounts.views import blueprint as blueprint_user +from invenio_db import InvenioDB +from invenio_mail import InvenioMail +from invenio_userprofiles import InvenioUserProfiles +from invenio_userprofiles.views import \ + blueprint_ui_init as blueprint_userprofile_init + +from invenio_oauthclient import InvenioOAuthClient +from invenio_oauthclient.contrib import globus +from invenio_oauthclient.views.client import blueprint as blueprint_client +from invenio_oauthclient.views.settings import blueprint as blueprint_settings + +# [ Configure application credentials ] +GLOBUS_APP_CREDENTIALS = dict( + consumer_key=os.environ.get('GLOBUS_APP_CREDENTIALS_KEY'), + consumer_secret=os.environ.get('GLOBUS_APP_CREDENTIALS_SECRET'), +) + +# Create Flask application +app = Flask(__name__) + +app.config.update( + SQLALCHEMY_DATABASE_URI=os.environ.get( + 'SQLALCHEMY_DATABASE_URI', 'sqlite:///globus_app.db' + ), + OAUTHCLIENT_REMOTE_APPS=dict( + globus=globus.REMOTE_APP, + ), + GLOBUS_APP_CREDENTIALS=GLOBUS_APP_CREDENTIALS, + DEBUG=True, + SECRET_KEY='TEST', + SQLALCHEMY_ECHO=False, + SECURITY_PASSWORD_SALT='security-password-salt', + MAIL_SUPPRESS_SEND=True, + TESTING=True, + USERPROFILES_EXTEND_SECURITY_FORMS=True, +) + +Babel(app) +FlaskMenu(app) +InvenioDB(app) +InvenioAccounts(app) +InvenioUserProfiles(app) +FlaskOAuth(app) +InvenioOAuthClient(app) +InvenioMail(app) + +app.register_blueprint(blueprint_user) +app.register_blueprint(blueprint_client) +app.register_blueprint(blueprint_settings) +app.register_blueprint(blueprint_userprofile_init) + + +@app.route('/') +def index(): + """Homepage.""" + return 'Home page (without any restrictions)' + + +@app.route('/globus') +def globus(): + """Try to print user email or redirect to login with globus.""" + if not current_user.is_authenticated: + return redirect(url_for('invenio_oauthclient.login', + remote_app='globus')) + return 'hello {}'.format(current_user.email) diff --git a/invenio_oauthclient/contrib/globus.py b/invenio_oauthclient/contrib/globus.py new file mode 100644 index 00000000..e59a0a51 --- /dev/null +++ b/invenio_oauthclient/contrib/globus.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2014, 2015, 2016, 2017 CERN. +# +# Invenio is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Invenio is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Invenio; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + +"""Pre-configured remote application for enabling sign in/up with Globus. + +1. Edit your configuration and add: + + .. code-block:: python + + from invenio_oauthclient.contrib import globus + + OAUTHCLIENT_REMOTE_APPS = dict( + globus=globus.REMOTE_APP, + ) + + GLOBUS_APP_CREDENTIALS = dict( + consumer_key='changeme', + consumer_secret='changeme', + ) + +2. Register a Globus application at `https://developers.globus.org/` with the + `Redirect URL` as `http://localhost:5000/oauth/authorized/globus/`. For + full documentation on all app fields, see: + `https://docs.globus.org/api/auth/developer-guide/#register-app` + +4. Grab the *Client ID* and *Client Secret* after registering the application + and add them to your instance configuration (``invenio.cfg``): + + .. code-block:: python + + GLOBUS_APP_CREDENTIALS = dict( + consumer_key='', + consumer_secret='', + ) + +5. Now go to your site: http://localhost:5000/oauth/authorized/globus/ + +6. You should see Globus listed under Linked accounts: + http://localhost:5000/account/settings/linkedaccounts/ + +""" + +from flask import current_app, redirect, url_for +from flask_login import current_user +from invenio_db import db + +from invenio_oauthclient.errors import OAuthResponseError +from invenio_oauthclient.models import RemoteAccount +from invenio_oauthclient.utils import oauth_link_external_id, \ + oauth_unlink_external_id + + +REMOTE_APP = dict( + title='Globus', + description='Research data management simplified.', + icon='', + authorized_handler='invenio_oauthclient.handlers' + ':authorized_signup_handler', + disconnect_handler='invenio_oauthclient.contrib.globus' + ':disconnect_handler', + signup_handler=dict( + info='invenio_oauthclient.contrib.globus:account_info', + setup='invenio_oauthclient.contrib.globus:account_setup', + view='invenio_oauthclient.handlers:signup_handler', + ), + params=dict( + request_token_params={'scope': 'openid email profile'}, + base_url='https://auth.globus.org/', + request_token_url=None, + access_token_url='https://auth.globus.org/v2/oauth2/token', + access_token_method='POST', + authorize_url='https://auth.globus.org/v2/oauth2/authorize', + app_key='GLOBUS_APP_CREDENTIALS', + ) +) +"""Globus remote application configuration.""" + +GLOBUS_USER_INFO_URL = 'https://auth.globus.org/v2/oauth2/userinfo' +GLOBUS_USER_ID_URL = 'https://auth.globus.org/v2/api/identities' +GLOBUS_EXTERNAL_METHOD = 'globus' + + +def get_dict_from_response(response): + """Check for errors in the response and return the resulting JSON""" + if getattr(response, '_resp') and response._resp.code > 400: + raise OAuthResponseError( + 'Application mis-configuration in Globus', None, response + ) + + return response.data + + +def get_user_info(remote): + """Get user information from Globus. + + See the docs here for v2/oauth/userinfo: + https://docs.globus.org/api/auth/reference/ + """ + response = remote.get(GLOBUS_USER_INFO_URL) + user_info = get_dict_from_response(response) + response.data['username'] = response.data['preferred_username'] + if '@' in response.data['username']: + user_info['username'], _ = response.data['username'].split('@') + return user_info + + +def get_user_id(remote, email): + """Get the Globus identity for a users given email. A Globus ID is a UUID + that can uniquely identify a Globus user + + See the docs here for v2/api/identities + https://docs.globus.org/api/auth/reference/""" + try: + url = '{}?usernames={}'.format(GLOBUS_USER_ID_URL, email) + user_id = get_dict_from_response(remote.get(url)) + return user_id['identities'][0]['id'] + except KeyError: + # If we got here the response was successful but the data was invalid. + # It's likely the URL is wrong but possible the API has changed. + raise OAuthResponseError('Failed to fetch user id, likely server ' + 'mis-configuration', None, remote) + + +def account_info(remote, resp): + """Retrieve remote account information used to find local user. + + It returns a dictionary with the following structure: + + .. code-block:: python + + { + 'user': { + 'email': '...', + 'profile': { + 'username': '...', + 'full_name': '...', + } + }, + 'external_id': 'globus-unique-identifier', + 'external_method': 'globus', + } + + Information inside the user dictionary are available for other modules. + For example, they are used from the module invenio-userprofiles to fill + the user profile. + + :param remote: The remote application. + :param resp: The response. + :returns: A dictionary with the user information. + """ + info = get_user_info(remote) + + return { + 'user': { + 'email': info['email'], + 'profile': { + 'username': info['username'], + 'full_name': info['name'] + }, + }, + 'external_id': get_user_id(remote, info['preferred_username']), + 'external_method': GLOBUS_EXTERNAL_METHOD + } + + +def account_setup(remote, token, resp): + """Perform additional setup after user have been logged in. + + :param remote: The remote application. + :param token: The token value. + :param resp: The response. + """ + info = get_user_info(remote) + user_id = get_user_id(remote, info['preferred_username']) + with db.session.begin_nested(): + + token.remote_account.extra_data = { + 'login': info['username'], + 'id': user_id} + + # Create user <-> external id link. + oauth_link_external_id( + token.remote_account.user, dict( + id=user_id, + method=GLOBUS_EXTERNAL_METHOD) + ) + + +def disconnect_handler(remote, *args, **kwargs): + """Handle unlinking of remote account. + + :param remote: The remote application. + :returns: The HTML response. + """ + if not current_user.is_authenticated: + return current_app.login_manager.unauthorized() + + remote_account = RemoteAccount.get(user_id=current_user.get_id(), + client_id=remote.consumer_key) + external_ids = [i.id for i in current_user.external_identifiers + if i.method == GLOBUS_EXTERNAL_METHOD] + + if external_ids: + oauth_unlink_external_id(dict(id=external_ids[0], + method=GLOBUS_EXTERNAL_METHOD)) + + if remote_account: + with db.session.begin_nested(): + remote_account.delete() + + return redirect(url_for('invenio_oauthclient_settings.index')) diff --git a/tests/conftest.py b/tests/conftest.py index e9da38c1..41fe56c3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,6 +49,7 @@ from invenio_oauthclient.contrib.cern import REMOTE_APP as CERN_REMOTE_APP from invenio_oauthclient.contrib.github import REMOTE_APP as GITHUB_REMOTE_APP from invenio_oauthclient.contrib.orcid import REMOTE_APP as ORCID_REMOTE_APP +from invenio_oauthclient.contrib.globus import REMOTE_APP as GLOBUS_REMOTE_APP from invenio_oauthclient.views.client import blueprint as blueprint_client from invenio_oauthclient.views.settings import blueprint as blueprint_settings @@ -67,6 +68,7 @@ def base_app(request): cern=CERN_REMOTE_APP, orcid=ORCID_REMOTE_APP, github=GITHUB_REMOTE_APP, + globus=GLOBUS_REMOTE_APP, ), GITHUB_APP_CREDENTIALS=dict( consumer_key='github_key_changeme', @@ -80,6 +82,10 @@ def base_app(request): consumer_key='cern_key_changeme', consumer_secret='cern_secret_changeme', ), + GLOBUS_APP_CREDENTIALS=dict( + consumer_key='globus_key_changeme', + consumer_secret='globus_secret_changeme', + ), # use local memory mailbox EMAIL_BACKEND='flask_email.backends.locmem.Mail', SQLALCHEMY_DATABASE_URI=os.getenv('SQLALCHEMY_DATABASE_URI', @@ -88,6 +94,7 @@ def base_app(request): DEBUG=False, SECRET_KEY='TEST', SECURITY_DEPRECATED_PASSWORD_SCHEMES=[], + SQLALCHEMY_TRACK_MODIFICATIONS=False, SECURITY_PASSWORD_HASH='plaintext', SECURITY_PASSWORD_SCHEMES=['plaintext'], ) @@ -289,6 +296,42 @@ def example_github(request): } +@pytest.fixture +def example_globus(request): + """Globus example data""" + return { + 'identity_provider_display_name': 'Globus ID', + 'sub': '1142af3a-fea4-4df9-afe2-865ccd68bfdb', + 'preferred_username': 'carberry@inveniosoftware.org', + 'identity_provider': '41143743-f3c8-4d60-bbdb-eeecaba85bd9', + 'organization': 'Globus', + 'email': 'carberry@inveniosoftware.org', + 'name': 'Josiah Carberry' + }, { + 'expires_in': 3599, + 'resource_server': 'auth.globus.org', + 'state': 'test_state', + 'access_token': 'test_access_token', + 'id_token': 'header.test-oidc-token.pub-key', + 'other_tokens': [], + 'scope': 'profile openid email', + 'token_type': 'Bearer', + }, { + 'identities': [ + { + 'username': 'carberry@inveniosoftware.org', + 'status': 'used', + 'name': 'Josiah Carberry', + 'email': 'carberry@inveniosoftware.org', + 'identity_provider': + '927d7238-f917-4eb2-9ace-c523fa9ba34e', + 'organization': 'Globus', + 'id': '3b843349-4d4d-4ef3-916d-2a465f9740a9' + } + ] + } + + @pytest.fixture def example_orcid(request): """ORCID example data.""" diff --git a/tests/test_contrib_globus.py b/tests/test_contrib_globus.py new file mode 100644 index 00000000..181eb7fc --- /dev/null +++ b/tests/test_contrib_globus.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2016, 2017 CERN. +# +# Invenio is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Invenio is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Invenio; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + +"""Test case for globus oauth remote app.""" + +from __future__ import absolute_import + +from collections import namedtuple +import json +import pytest +from mock import MagicMock + +from flask import session, url_for +from flask_login import current_user +from flask_security import login_user +from flask_oauthlib.client import OAuthResponse +from helpers import check_redirect_location, mock_response, mock_remote_get +from invenio_accounts.models import User +from six.moves.urllib_parse import parse_qs, urlparse + +from invenio_oauthclient.errors import OAuthResponseError +from invenio_oauthclient._compat import _create_identifier +from invenio_oauthclient.models import RemoteAccount, RemoteToken, UserIdentity +from invenio_oauthclient.views.client import serializer + + +def _get_state(): + return serializer.dumps({'app': 'globus', 'sid': _create_identifier(), + 'next': None, }) + + +def test_login(app): + """Test globus login.""" + client = app.test_client() + + resp = client.get( + url_for('invenio_oauthclient.login', remote_app='globus', + next='/someurl/') + ) + assert resp.status_code == 302 + + params = parse_qs(urlparse(resp.location).query) + assert params['response_type'], ['code'] + assert params['scope'] == ['openid email profile'] + assert params['redirect_uri'] + assert params['client_id'] + assert params['state'] + + +def test_authorized_signup_valid_user(app, example_globus): + """Test authorized callback with sign-up.""" + + with app.test_client() as c: + # User login with email 'info' + ioc = app.extensions['oauthlib.client'] + + # Ensure remote apps have been loaded (due to before first request) + resp = c.get(url_for('invenio_oauthclient.login', + remote_app='globus')) + assert resp.status_code == 302 + + example_info, example_token, example_account_id = example_globus + mock_response(app.extensions['oauthlib.client'], 'globus', + example_token) + example_info.update(example_account_id) + oauth_resp = OAuthResponse(resp=None, content=json.dumps(example_info), + content_type='application/json') + mock_remote_get(ioc, 'globus', oauth_resp) + + # User authorized the requests and is redirect back + resp = c.get( + url_for('invenio_oauthclient.authorized', + remote_app='globus', code='test', + state=_get_state())) + assert resp.status_code == 302 + assert resp.location == ('http://localhost/account/settings/' + + 'linkedaccounts/') + + # Assert database state (Sign-up complete) + user = User.query.filter_by(email='carberry@inveniosoftware.org').one() + remote = RemoteAccount.query.filter_by(user_id=user.id).one() + RemoteToken.query.filter_by(id_remote_account=remote.id).one() + assert user.active + + # Disconnect link + resp = c.get( + url_for('invenio_oauthclient.disconnect', remote_app='globus')) + assert resp.status_code == 302 + + # User exists + user = User.query.filter_by(email='carberry@inveniosoftware.org').one() + assert 0 == UserIdentity.query.filter_by( + method='orcid', id_user=user.id, + id='globususer' + ).count() + assert RemoteAccount.query.filter_by(user_id=user.id).count() == 0 + assert RemoteToken.query.count() == 0 + + # User authorized the requests and is redirect back + resp = c.get( + url_for('invenio_oauthclient.authorized', + remote_app='globus', code='test', + state=_get_state())) + assert resp.status_code == 302 + assert resp.location == ( + 'http://localhost/' + + 'account/settings/linkedaccounts/' + ) + + # check that exist only one account + user = User.query.filter_by(email='carberry@inveniosoftware.org').one() + assert User.query.count() == 1 + + +def test_authorized_reject(app): + """Test a rejected request.""" + with app.test_client() as c: + c.get(url_for('invenio_oauthclient.login', remote_app='globus')) + resp = c.get( + url_for('invenio_oauthclient.authorized', + remote_app='globus', error='access_denied', + error_description='User denied access', + state=_get_state())) + assert resp.status_code in (301, 302) + assert resp.location == ( + 'http://localhost/' + ) + # Check message flash + assert session['_flashes'][0][0] == 'info' + + +def test_authorized_already_authenticated(models_fixture, example_globus): + """Test authorized callback with sign-up.""" + app = models_fixture + + datastore = app.extensions['invenio-accounts'].datastore + login_manager = app.login_manager + + existing_email = 'existing@inveniosoftware.org' + user = datastore.find_user(email=existing_email) + + @login_manager.user_loader + def load_user(user_id): + return user + + @app.route('/foo_login') + def login(): + login_user(user) + return 'Logged In' + + with app.test_client() as client: + + # make a fake login (using my login function) + client.get('/foo_login', follow_redirects=True) + # Ensure remote apps have been loaded (due to before first request) + client.get(url_for('invenio_oauthclient.login', remote_app='globus')) + + ioc = app.extensions['oauthlib.client'] + example_info, example_token, example_account_id = example_globus + mock_response(app.extensions['oauthlib.client'], 'globus', + example_token) + example_info.update(example_account_id) + oauth_resp = OAuthResponse(resp=None, content=json.dumps(example_info), + content_type='application/json') + mock_remote_get(ioc, 'globus', oauth_resp) + + # User then goes to 'Linked accounts' and clicks 'Connect' + resp = client.get( + url_for('invenio_oauthclient.login', remote_app='globus', + next='/someurl/') + ) + assert resp.status_code == 302 + + # User authorized the requests and is redirected back + resp = client.get( + url_for('invenio_oauthclient.authorized', + remote_app='globus', code='test', + state=_get_state())) + + # Assert database state (Sign-up complete) + u = User.query.filter_by(email=existing_email).one() + remote = RemoteAccount.query.filter_by(user_id=u.id).one() + RemoteToken.query.filter_by(id_remote_account=remote.id).one() + + # Disconnect link + resp = client.get( + url_for('invenio_oauthclient.disconnect', remote_app='globus')) + assert resp.status_code == 302 + + # User exists + u = User.query.filter_by(email=existing_email).one() + assert 0 == UserIdentity.query.filter_by( + method='globus', id_user=u.id, + id='globususer' + ).count() + assert RemoteAccount.query.filter_by(user_id=u.id).count() == 0 + assert RemoteToken.query.count() == 0 + + +def test_not_authenticated(app): + """Test disconnect when user is not authenticated.""" + with app.test_client() as client: + assert not current_user.is_authenticated + resp = client.get( + url_for('invenio_oauthclient.disconnect', remote_app='globus')) + assert resp.status_code == 302 + + +def test_bad_provider_response(app, example_globus): + with app.test_client() as c: + + class MockResponse: + code = 403 + + # User login with email 'info' + ioc = app.extensions['oauthlib.client'] + + # Ensure remote apps have been loaded (due to before first request) + resp = c.get(url_for('invenio_oauthclient.login', + remote_app='globus')) + assert resp.status_code == 302 + + _, example_token, _ = example_globus + mock_response(app.extensions['oauthlib.client'], 'globus', + example_token) + oauth_resp = OAuthResponse(resp=MockResponse(), content=None, + content_type='application/json') + mock_remote_get(ioc, 'globus', oauth_resp) + + with pytest.raises(OAuthResponseError): + c.get( + url_for('invenio_oauthclient.authorized', + remote_app='globus', code='test', + state=_get_state())) + + +def test_invalid_user_id_response(app, example_globus): + with app.test_client() as c: + + # User login with email 'info' + ioc = app.extensions['oauthlib.client'] + + # Ensure remote apps have been loaded (due to before first request) + resp = c.get(url_for('invenio_oauthclient.login', + remote_app='globus')) + assert resp.status_code == 302 + + example_info, example_token, _ = example_globus + mock_response(app.extensions['oauthlib.client'], 'globus', + example_token) + oauth_resp = OAuthResponse(resp=None, content=json.dumps(example_info), + content_type='application/json') + mock_remote_get(ioc, 'globus', oauth_resp) + + with pytest.raises(OAuthResponseError): + c.get( + url_for('invenio_oauthclient.authorized', + remote_app='globus', code='test', + state=_get_state())) diff --git a/tests/test_examples_app.py b/tests/test_examples_app.py index a32e7526..611b5c2e 100644 --- a/tests/test_examples_app.py +++ b/tests/test_examples_app.py @@ -62,7 +62,7 @@ def _create_example_app(app_name): time.sleep(2) -@pytest.mark.parametrize('service', ['orcid', 'github', 'cern']) +@pytest.mark.parametrize('service', ['orcid', 'github', 'cern', 'globus']) def test_app(service): """Test example app for given service.""" with _create_example_app('{0}_app.py'.format(service)):