diff --git a/invenio_accounts_rest/ext.py b/invenio_accounts_rest/ext.py index 35e54b0..7ca0c4c 100644 --- a/invenio_accounts_rest/ext.py +++ b/invenio_accounts_rest/ext.py @@ -26,6 +26,8 @@ from __future__ import absolute_import, print_function +from .views import create_blueprint + class InvenioAccountsREST(object): """Invenio-Accounts-REST extension.""" @@ -38,6 +40,7 @@ def __init__(self, app=None): def init_app(self, app): """Flask application initialization.""" self.init_config(app) + app.register_blueprint(create_blueprint()) app.extensions['invenio-accounts-rest'] = self def init_config(self, app): diff --git a/invenio_accounts_rest/views.py b/invenio_accounts_rest/views.py index f32bdda..85b44fa 100644 --- a/invenio_accounts_rest/views.py +++ b/invenio_accounts_rest/views.py @@ -26,9 +26,55 @@ from __future__ import absolute_import, print_function -from flask import Blueprint +import json -blueprint = Blueprint( - 'invenio_accounts_rest', - __name__, -) +from flask import Blueprint, request +from invenio_accounts.models import User +from invenio_oauth2server import require_api_auth +from invenio_rest import ContentNegotiatedMethodView +from sqlalchemy import String, cast + + +def accounts_serializer(*args, **kwargs): + """Basic serializer for invenio_accounts.models.User data.""" + return json.dumps([{'id': u.id, 'email': u.email} for u in args]) + + +def create_blueprint(): + """Create invenio-accounts REST blueprint.""" + blueprint = Blueprint( + 'invenio_accounts_rest', + __name__, + ) + + accounts_resource = AccountsResource.as_view( + 'accounts_resource', + serializers={'application/json': accounts_serializer}, + default_media_type='application/json' + ) + + blueprint.add_url_rule( + '/users/', + view_func=accounts_resource, + methods=['GET'], + ) + + return blueprint + + +class AccountsResource(ContentNegotiatedMethodView): + """MethodView implementation.""" + + def __init__(self, serializers, default_media_type): + """Constructor.""" + super(AccountsResource, self).__init__( + serializers, default_media_type=default_media_type) + + @require_api_auth() + def get(self): + """Get accounts/users/?q=.""" + query = request.args.get('q') + return User.query.filter( + (User.email == query) | + (cast(User.id, String) == query) + ).all() diff --git a/setup.py b/setup.py index c3b5895..ef21b4a 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,10 @@ install_requires = [ 'Flask-BabelEx>=0.9.2', + 'cryptography<=1.5', + 'invenio-db>=1.0.0b1', + 'invenio-oauth2server>=1.0.0a9', + 'invenio-rest>=1.0.0a9', ] packages = find_packages() diff --git a/tests/conftest.py b/tests/conftest.py index e101946..71bef67 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,15 +27,73 @@ from __future__ import absolute_import, print_function +import tempfile + import pytest from flask import Flask +from flask_security.utils import encrypt_password +from invenio_accounts import InvenioAccounts +from invenio_db import db as db_ +from invenio_db import InvenioDB +from invenio_oauth2server import InvenioOAuth2Server +from invenio_oauth2server.models import Token +from sqlalchemy_utils.functions import create_database, database_exists +from werkzeug.local import LocalProxy + +from invenio_accounts_rest import InvenioAccountsREST -@pytest.fixture() +@pytest.yield_fixture() def app(): """Flask application fixture.""" - app = Flask('testapp') + instance_path = tempfile.mkdtemp() + + app = Flask(__name__, instance_path=instance_path) app.config.update( - TESTING=True + OAUTH2SERVER_CLIENT_ID_SALT_LEN=40, + OAUTH2SERVER_CLIENT_SECRET_SALT_LEN=60, + OAUTH2SERVER_TOKEN_PERSONAL_SALT_LEN=60, + SECRET_KEY='changeme', + TESTING=True, ) - return app + + InvenioOAuth2Server(app) + InvenioAccounts(app) + InvenioAccountsREST(app) + InvenioDB(app) + + with app.app_context(): + yield app + + +@pytest.yield_fixture() +def db(app): + """Database fixture.""" + if not database_exists(str(db_.engine.url)): + create_database(str(db_.engine.url)) + + db_.create_all() + yield db_ + db_.session.remove() + db_.drop_all() + + +@pytest.yield_fixture() +def access_token(app, db): + """Access token fixture.""" + _datastore = LocalProxy(lambda: app.extensions['security'].datastore) + kwargs = dict(email='admin@inveniosoftware.org', password='123456', + active=True) + kwargs['password'] = encrypt_password(kwargs['password']) + user = _datastore.create_user(**kwargs) + + db.session.commit() + token = Token.create_personal( + 'test-personal-{0}'.format(user.id), + user.id, + scopes=[], + is_internal=True, + ).access_token + db.session.commit() + + yield token diff --git a/tests/test_invenio_accounts_rest.py b/tests/test_invenio_accounts_rest.py index c38dc31..ec2e98f 100644 --- a/tests/test_invenio_accounts_rest.py +++ b/tests/test_invenio_accounts_rest.py @@ -31,7 +31,6 @@ from flask_babelex import Babel from invenio_accounts_rest import InvenioAccountsREST -from invenio_accounts_rest.views import blueprint def test_version(): @@ -51,9 +50,3 @@ def test_init(): assert 'invenio-accounts-rest' not in app.extensions ext.init_app(app) assert 'invenio-accounts-rest' in app.extensions - - -def test_view(app): - """Test view.""" - InvenioAccountsREST(app) - app.register_blueprint(blueprint) diff --git a/tests/test_user_search.py b/tests/test_user_search.py new file mode 100644 index 0000000..2f48be2 --- /dev/null +++ b/tests/test_user_search.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2016 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. + +"""Module user search tests.""" + +import json + +from flask import url_for +from invenio_accounts.models import User + + +def test_user_search(app, db, access_token): + """Test REST API for circulation specific user search.""" + # The access_token fixture generates a user + user = User.query.all()[0] + with app.test_request_context(): + with app.test_client() as client: + # Search while not being authorized + url = url_for('invenio_accounts_rest.accounts_resource') + url += '?q={0}'.format(user.id + 1) + url += '&access_token=foo' + res = client.get(url) + + assert res.status_code == 401 + + # Search for non existing user + url = url_for('invenio_accounts_rest.accounts_resource') + url += '?q={0}'.format(user.id + 1) + url += '&access_token=' + access_token + res = client.get(url) + + assert res.status_code == 200 + assert len(json.loads(res.data.decode('utf-8'))) == 0 + + # Search for existing user + url = url_for('invenio_accounts_rest.accounts_resource') + url += '?q={0}'.format(user.id) + url += '&access_token=' + access_token + res = client.get(url) + + assert res.status_code == 200 + assert len(json.loads(res.data.decode('utf-8'))) == 1