Просмотр исходного кода

Merge pull request #424 from justanr/more-auth-services

More auth services
Peter Justin 7 лет назад
Родитель
Сommit
972388a08b

+ 0 - 1
.coveragerc

@@ -36,7 +36,6 @@ exclude_lines =
 ignore_errors = True
 precision = 2
 show_missing = true
-skip_covered = true
 
 [html]
 directory = tests/htmlcov

+ 83 - 74
flaskbb/auth/forms.py

@@ -14,16 +14,13 @@ from flask_babelplus import lazy_gettext as _
 from wtforms import (BooleanField, HiddenField, PasswordField, SelectField,
                      StringField, SubmitField)
 from wtforms.validators import (DataRequired, Email, EqualTo, InputRequired,
-                                ValidationError, regexp)
+                                regexp)
 
-from flaskbb.user.models import User
 from flaskbb.utils.fields import RecaptchaField
 from flaskbb.utils.forms import FlaskBBForm
-from flaskbb.utils.helpers import time_utcnow
 
 logger = logging.getLogger(__name__)
 
-
 USERNAME_RE = r'^[\w.+-]+$'
 is_valid_username = regexp(
     USERNAME_RE, message=_("You can only use letters, numbers or dashes.")
@@ -31,12 +28,19 @@ is_valid_username = regexp(
 
 
 class LoginForm(FlaskBBForm):
-    login = StringField(_("Username or Email address"), validators=[
-        DataRequired(message=_("Please enter your username or email address."))
-    ])
-
-    password = PasswordField(_("Password"), validators=[
-        DataRequired(message=_("Please enter your password."))])
+    login = StringField(
+        _("Username or Email address"),
+        validators=[
+            DataRequired(
+                message=_("Please enter your username or email address.")
+            )
+        ]
+    )
+
+    password = PasswordField(
+        _("Password"),
+        validators=[DataRequired(message=_("Please enter your password."))]
+    )
 
     remember_me = BooleanField(_("Remember me"), default=False)
 
@@ -49,17 +53,29 @@ class LoginRecaptchaForm(LoginForm):
 
 
 class RegisterForm(FlaskBBForm):
-    username = StringField(_("Username"), validators=[
-        DataRequired(message=_("A valid username is required")),
-        is_valid_username])
-
-    email = StringField(_("Email address"), validators=[
-        DataRequired(message=_("A valid email address is required.")),
-        Email(message=_("Invalid email address."))])
-
-    password = PasswordField(_('Password'), validators=[
-        InputRequired(),
-        EqualTo('confirm_password', message=_('Passwords must match.'))])
+    username = StringField(
+        _("Username"),
+        validators=[
+            DataRequired(message=_("A valid username is required")),
+            is_valid_username
+        ]
+    )
+
+    email = StringField(
+        _("Email address"),
+        validators=[
+            DataRequired(message=_("A valid email address is required.")),
+            Email(message=_("Invalid email address."))
+        ]
+    )
+
+    password = PasswordField(
+        _('Password'),
+        validators=[
+            InputRequired(),
+            EqualTo('confirm_password', message=_('Passwords must match.'))
+        ]
+    )
 
     confirm_password = PasswordField(_('Confirm password'))
 
@@ -67,32 +83,32 @@ class RegisterForm(FlaskBBForm):
 
     language = SelectField(_('Language'))
 
-    accept_tos = BooleanField(_("I accept the Terms of Service"), validators=[
-        DataRequired(message=_("Please accept the TOS."))], default=True)
+    accept_tos = BooleanField(
+        _("I accept the Terms of Service"),
+        validators=[DataRequired(message=_("Please accept the TOS."))],
+        default=True
+    )
 
     submit = SubmitField(_("Register"))
 
-    def save(self):
-        user = User(username=self.username.data,
-                    email=self.email.data,
-                    password=self.password.data,
-                    date_joined=time_utcnow(),
-                    primary_group_id=4,
-                    language=self.language.data)
-        return user.save()
-
 
 class ReauthForm(FlaskBBForm):
-    password = PasswordField(_('Password'), validators=[
-        DataRequired(message=_("Please enter your password."))])
+    password = PasswordField(
+        _('Password'),
+        validators=[DataRequired(message=_("Please enter your password."))]
+    )
 
     submit = SubmitField(_("Refresh Login"))
 
 
 class ForgotPasswordForm(FlaskBBForm):
-    email = StringField(_('Email address'), validators=[
-        DataRequired(message=_("A valid email address is required.")),
-        Email()])
+    email = StringField(
+        _('Email address'),
+        validators=[
+            DataRequired(message=_("A valid email address is required.")),
+            Email()
+        ]
+    )
 
     recaptcha = RecaptchaField(_("Captcha"))
 
@@ -102,49 +118,42 @@ class ForgotPasswordForm(FlaskBBForm):
 class ResetPasswordForm(FlaskBBForm):
     token = HiddenField('Token')
 
-    email = StringField(_('Email address'), validators=[
-        DataRequired(message=_("A valid email address is required.")),
-        Email()])
-
-    password = PasswordField(_('Password'), validators=[
-        InputRequired(),
-        EqualTo('confirm_password', message=_('Passwords must match.'))])
+    email = StringField(
+        _('Email address'),
+        validators=[
+            DataRequired(message=_("A valid email address is required.")),
+            Email()
+        ]
+    )
+
+    password = PasswordField(
+        _('Password'),
+        validators=[
+            InputRequired(),
+            EqualTo('confirm_password', message=_('Passwords must match.'))
+        ]
+    )
 
     confirm_password = PasswordField(_('Confirm password'))
 
     submit = SubmitField(_("Reset password"))
 
-    def validate_email(self, field):
-        email = User.query.filter_by(email=field.data).first()
-        if not email:
-            raise ValidationError(_("Wrong email address."))
-
 
 class RequestActivationForm(FlaskBBForm):
-    username = StringField(_("Username"), validators=[
-        DataRequired(message=_("A valid username is required.")),
-        is_valid_username])
-
-    email = StringField(_("Email address"), validators=[
-        DataRequired(message=_("A valid email address is required.")),
-        Email(message=_("Invalid email address."))])
+    username = StringField(
+        _("Username"),
+        validators=[
+            DataRequired(message=_("A valid username is required.")),
+            is_valid_username
+        ]
+    )
+
+    email = StringField(
+        _("Email address"),
+        validators=[
+            DataRequired(message=_("A valid email address is required.")),
+            Email(message=_("Invalid email address."))
+        ]
+    )
 
     submit = SubmitField(_("Send Confirmation Mail"))
-
-    def validate_email(self, field):
-        self.user = User.query.filter_by(email=field.data).first()
-        # check if the username matches the one found in the database
-        if not self.user.username == self.username.data:
-            raise ValidationError(_("User does not exist."))
-
-        if self.user.activated is True:
-            raise ValidationError(_("User is already active."))
-
-
-class AccountActivationForm(FlaskBBForm):
-    token = StringField(_("Email confirmation token"), validators=[
-        DataRequired(message=_("Please enter the token that we have sent to "
-                               "you."))
-    ])
-
-    submit = SubmitField(_("Confirm Email"))

+ 12 - 4
flaskbb/auth/plugins.py

@@ -1,11 +1,20 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.auth.plugins
+    ~~~~~~~~~~~~~~~~~~~~
+    Plugin implementations for FlaskBB auth hooks
+
+    :copyright: (c) 2014-2018 the FlaskBB Team.
+    :license: BSD, see LICENSE for more details
+"""
 from flask import flash
 from flask_babelplus import gettext as _
 from flask_login import login_user
 
 from . import impl
-from ..email import send_activation_token
 from ..user.models import User
 from ..utils.settings import flaskbb_config
+from .services.factories import account_activator_factory
 
 
 @impl
@@ -13,9 +22,8 @@ def flaskbb_event_user_registered(username):
     user = User.query.filter_by(username=username).first()
 
     if flaskbb_config["ACTIVATE_ACCOUNT"]:
-        send_activation_token.delay(
-            user_id=user.id, username=user.username, email=user.email
-        )
+        service = account_activator_factory()
+        service.initiate_account_activation(user.email)
         flash(
             _(
                 "An account activation email has been sent to "

+ 19 - 0
flaskbb/auth/services/__init__.py

@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.auth.services
+    ~~~~~~~~~~~~~~~~~~~~~
+    Public module of implemenations of auth related services
+    in FlaskBB. If you are developing a plugin or extending
+    FlaskBB, you should import from this module rather than
+    submodules.
+
+    :copyright: (c) 2014-2018 the FlaskBB Team.
+    :license: BSD, see LICENSE for more details
+"""
+
+from .activation import AccountActivator
+from .factories import (account_activator_factory,
+                        registration_service_factory, reset_service_factory)
+from .password import ResetPasswordService
+from .registration import (EmailUniquenessValidator, UsernameRequirements,
+                           UsernameUniquenessValidator, UsernameValidator)

+ 51 - 0
flaskbb/auth/services/activation.py

@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.auth.services.activation
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    Handlers for activating accounts in FlaskBB
+
+    :copyright: (c) 2014-2018 the FlaskBB Team
+    :license: BSD, see LICENSE for more details
+"""
+
+from flask_babelplus import gettext as _
+
+from ...core.auth.activation import AccountActivator as _AccountActivator
+from ...core.exceptions import ValidationError
+from ...core.tokens import Token, TokenActions, TokenError
+from ...email import send_activation_token
+
+
+class AccountActivator(_AccountActivator):
+
+    def __init__(self, token_serializer, users):
+        self.token_serializer = token_serializer
+        self.users = users
+
+    def initiate_account_activation(self, email):
+        user = self.users.query.filter_by(email=email).first()
+
+        if user is None:
+            raise ValidationError('email', _("Entered email doesn't exist"))
+
+        if user.activated:
+            raise ValidationError('email', _('Account is already activated'))
+
+        token = self.token_serializer.dumps(
+            Token(user_id=user.id, operation=TokenActions.ACTIVATE_ACCOUNT)
+        )
+
+        send_activation_token.delay(
+            token=token, username=user.username, email=user.email
+        )
+
+    def activate_account(self, token):
+        token = self.token_serializer.loads(token)
+        if token.operation != TokenActions.ACTIVATE_ACCOUNT:
+            raise TokenError.invalid()
+        user = self.users.query.get(token.user_id)
+        if user.activated:
+            raise ValidationError(
+                'activated', _('Account is already activated')
+            )
+        user.activated = True

+ 64 - 0
flaskbb/auth/services/factories.py

@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.auth.services.factories
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    Factory functions for various FlaskBB auth services
+
+    These factories are provisional.
+
+    :copyright: 2014-2018 the FlaskBB Team.
+    :license: BSD, see LICENSE for more details
+"""
+from datetime import timedelta
+
+from flask import current_app
+
+from ...extensions import db
+from ...tokens import FlaskBBTokenSerializer
+from ...tokens.verifiers import EmailMatchesUserToken
+from ...user.models import User
+from ...user.repo import UserRepository
+from ...utils.settings import flaskbb_config
+from .activation import AccountActivator
+from .password import ResetPasswordService
+from .registration import (EmailUniquenessValidator, RegistrationService,
+                           UsernameRequirements, UsernameUniquenessValidator,
+                           UsernameValidator)
+
+
+def registration_service_factory():
+    blacklist = [
+        w.strip()
+        for w in flaskbb_config["AUTH_USERNAME_BLACKLIST"].split(",")
+    ]
+
+    requirements = UsernameRequirements(
+        min=flaskbb_config["AUTH_USERNAME_MIN_LENGTH"],
+        max=flaskbb_config["AUTH_USERNAME_MAX_LENGTH"],
+        blacklist=blacklist
+    )
+
+    validators = [
+        EmailUniquenessValidator(User),
+        UsernameUniquenessValidator(User),
+        UsernameValidator(requirements)
+    ]
+
+    return RegistrationService(validators, UserRepository(db))
+
+
+def reset_service_factory():
+    token_serializer = FlaskBBTokenSerializer(
+        current_app.config['SECRET_KEY'], expiry=timedelta(hours=1)
+    )
+    verifiers = [EmailMatchesUserToken(User)]
+    return ResetPasswordService(
+        token_serializer, User, token_verifiers=verifiers
+    )
+
+
+def account_activator_factory():
+    token_serializer = FlaskBBTokenSerializer(
+        current_app.config['SECRET_KEY'], expiry=timedelta(hours=1)
+    )
+    return AccountActivator(token_serializer, User)

+ 58 - 0
flaskbb/auth/services/password.py

@@ -0,0 +1,58 @@
+"""
+    flaskbb.auth.password
+    ~~~~~~~~~~~~~~~~~~~~~
+
+    Password reset manager
+
+    :copyright: (c) 2014-2018 the FlaskBB Team.
+    :license: BSD, see LICENSE for more details
+"""
+
+from flask_babelplus import gettext as _
+
+from ...core.auth.password import ResetPasswordService as _ResetPasswordService
+from ...core.exceptions import StopValidation, ValidationError
+from ...core.tokens import Token, TokenActions, TokenError
+from ...email import send_reset_token
+
+
+class ResetPasswordService(_ResetPasswordService):
+
+    def __init__(self, token_serializer, users, token_verifiers):
+        self.token_serializer = token_serializer
+        self.users = users
+        self.token_verifiers = token_verifiers
+
+    def initiate_password_reset(self, email):
+        user = self.users.query.filter_by(email=email).first()
+
+        if user is None:
+            raise ValidationError('email', _('Invalid email'))
+
+        token = self.token_serializer.dumps(
+            Token(user_id=user.id, operation=TokenActions.RESET_PASSWORD)
+        )
+
+        send_reset_token.delay(
+            token=token, username=user.username, email=user.email
+        )
+
+    def reset_password(self, token, email, new_password):
+        token = self.token_serializer.loads(token)
+        if token.operation != TokenActions.RESET_PASSWORD:
+            raise TokenError.invalid()
+        self._verify_token(token, email)
+        user = self.users.query.get(token.user_id)
+        user.password = new_password
+
+    def _verify_token(self, token, email):
+        errors = []
+
+        for verifier in self.token_verifiers:
+            try:
+                verifier(token, email=email)
+            except ValidationError as e:
+                errors.append((e.attribute, e.reason))
+
+        if errors:
+            raise StopValidation(errors)

+ 36 - 7
flaskbb/auth/services/registration.py

@@ -11,10 +11,11 @@
 
 from collections import namedtuple
 
+from flask_babelplus import gettext as _
 from sqlalchemy import func
 
-from ...core.auth.registration import UserValidator
-from ...core.exceptions import ValidationError
+from ...core.auth.registration import UserRegistrationService, UserValidator
+from ...core.exceptions import StopValidation, ValidationError
 
 __all__ = (
     "UsernameRequirements", "UsernameValidator", "EmailUniquenessValidator",
@@ -36,8 +37,10 @@ class UsernameValidator(UserValidator):
                 self._requirements.max):
             raise ValidationError(
                 'username',
-                'Username must be between {} and {} characters long'.format(
-                    self._requirements.min, self._requirements.max
+                _(
+                    'Username must be between %(min)s and %(max)s characters long',
+                    min=self._requirements.min,
+                    max=self._requirements.max
                 )
             )
 
@@ -45,7 +48,10 @@ class UsernameValidator(UserValidator):
         if is_blacklisted:  # pragma: no branch
             raise ValidationError(
                 'username',
-                '{} is a forbidden username'.format(user_info.username)
+                _(
+                    '%(username)s is a forbidden username',
+                    username=user_info.username
+                )
             )
 
 
@@ -61,7 +67,10 @@ class UsernameUniquenessValidator(UserValidator):
         if count != 0:  # pragma: no branch
             raise ValidationError(
                 'username',
-                '{} is already registered'.format(user_info.username)
+                _(
+                    '%(username)s is already registered',
+                    username=user_info.username
+                )
             )
 
 
@@ -76,5 +85,25 @@ class EmailUniquenessValidator(UserValidator):
         ).count()
         if count != 0:  # pragma: no branch
             raise ValidationError(
-                'email', '{} is already registered'.format(user_info.email)
+                'email',
+                _('%(email)s is already registered', email=user_info.email)
             )
+
+
+class RegistrationService(UserRegistrationService):
+
+    def __init__(self, validators, user_repo):
+        self.validators = validators
+        self.user_repo = user_repo
+
+    def register(self, user_info):
+        failures = []
+
+        for v in self.validators:
+            try:
+                v(user_info)
+            except ValidationError as e:
+                failures.append((e.attribute, e.reason))
+        if failures:
+            raise StopValidation(failures)
+        self.user_repo.add(user_info)

+ 82 - 131
flaskbb/auth/views.py

@@ -10,7 +10,7 @@
     :license: BSD, see LICENSE for more details.
 """
 import logging
-from datetime import datetime, timedelta
+from datetime import datetime
 
 from flask import Blueprint, current_app, flash, g, redirect, request, url_for
 from flask.views import MethodView
@@ -18,11 +18,9 @@ from flask_babelplus import gettext as _
 from flask_login import (confirm_login, current_user, login_fresh,
                          login_required, login_user, logout_user)
 
-from flaskbb.auth.forms import (AccountActivationForm, ForgotPasswordForm,
-                                LoginForm, LoginRecaptchaForm, ReauthForm,
-                                RegisterForm, RequestActivationForm,
-                                ResetPasswordForm)
-from flaskbb.email import send_activation_token, send_reset_token
+from flaskbb.auth.forms import (ForgotPasswordForm, LoginForm,
+                                LoginRecaptchaForm, ReauthForm, RegisterForm,
+                                RequestActivationForm, ResetPasswordForm)
 from flaskbb.exceptions import AuthenticationError
 from flaskbb.extensions import db, limiter
 from flaskbb.user.models import User
@@ -32,17 +30,13 @@ from flaskbb.utils.helpers import (anonymous_required, enforce_recaptcha,
                                    registration_enabled, render_template,
                                    requires_unactivated)
 from flaskbb.utils.settings import flaskbb_config
-from flaskbb.utils.tokens import get_token_status
 
-from .services import registration
-from ..core.auth.password import ResetPasswordService
-from ..core.auth.registration import (RegistrationService, UserRegistrationInfo)
-from ..core.exceptions import ValidationError, StopValidation
+from ..core.auth.registration import UserRegistrationInfo
+from ..core.exceptions import StopValidation, ValidationError
 from ..core.tokens import TokenError
-from ..tokens import FlaskBBTokenSerializer
-from ..tokens.verifiers import EmailMatchesUserToken
-from ..user.repo import UserRepository
 from .plugins import impl
+from .services import (account_activator_factory, registration_service_factory,
+                       reset_service_factory)
 
 logger = logging.getLogger(__name__)
 
@@ -147,10 +141,11 @@ class Register(MethodView):
                 try:
                     db.session.commit()
                 except Exception:  # noqa
-                    logger.exception("Uh that looks bad...")
+                    logger.exception("Database error while resetting password")
                     flash(
                         _(
-                            "Could not process registration due to an unrecoverable error"
+                            "Could not process registration due"
+                            "to an unrecoverable error"
                         ), "danger"
                     )
 
@@ -168,27 +163,30 @@ class ForgotPassword(MethodView):
     decorators = [anonymous_required]
     form = ForgotPasswordForm
 
+    def __init__(self, password_reset_service_factory):
+        self.password_reset_service_factory = password_reset_service_factory
+
     def get(self):
         return render_template("auth/forgot_password.html", form=self.form())
 
     def post(self):
         form = self.form()
         if form.validate_on_submit():
-            user = User.query.filter_by(email=form.email.data).first()
 
-            if user:
-                send_reset_token.delay(
-                    user_id=user.id, username=user.username, email=user.email
-                )
-                flash(_("Email sent! Please check your inbox."), "info")
-                return redirect(url_for("auth.forgot_password"))
-            else:
+            try:
+                self.password_reset_service_factory(
+                ).initiate_password_reset(form.email.data)
+            except ValidationError:
                 flash(
                     _(
                         "You have entered an username or email address that "
                         "is not linked with your account."
                     ), "danger"
                 )
+            else:
+                flash(_("Email sent! Please check your inbox."), "info")
+                return redirect(url_for("auth.forgot_password"))
+
         return render_template("auth/forgot_password.html", form=form)
 
 
@@ -210,13 +208,11 @@ class ResetPassword(MethodView):
             try:
                 service = self.password_reset_service_factory()
                 service.reset_password(
-                    token,
-                    form.email.data,
-                    form.password.data
+                    token, form.email.data, form.password.data
                 )
                 db.session.commit()
             except TokenError as e:
-                flash(_(e.reason), 'danger')
+                flash(e.reason, 'danger')
                 return redirect(url_for('auth.forgot_password'))
             except StopValidation as e:
                 form.populate_errors(e.reasons)
@@ -238,6 +234,9 @@ class RequestActivationToken(MethodView):
     decorators = [requires_unactivated]
     form = RequestActivationForm
 
+    def __init__(self, account_activator_factory):
+        self.account_activator_factory = account_activator_factory
+
     def get(self):
         return render_template(
             "auth/request_account_activation.html", form=self.form()
@@ -246,17 +245,19 @@ class RequestActivationToken(MethodView):
     def post(self):
         form = self.form()
         if form.validate_on_submit():
-            user = User.query.filter_by(email=form.email.data).first()
-            send_activation_token.delay(
-                user_id=user.id, username=user.username, email=user.email
-            )
-            flash(
-                _(
-                    "A new account activation token has been sent to "
-                    "your email address."
-                ), "success"
-            )
-            return redirect(url_for("auth.activate_account"))
+            activator = self.account_activator_factory()
+            try:
+                activator.initiate_account_activation(form.email.data)
+            except ValidationError as e:
+                form.populate_errors([(e.attribute, e.reason)])
+            else:
+                flash(
+                    _(
+                        "A new account activation token has been sent to "
+                        "your email address."
+                    ), "success"
+                )
+                return redirect(url_for("auth.activate_account"))
 
         return render_template(
             "auth/request_account_activation.html", form=form
@@ -264,67 +265,41 @@ class RequestActivationToken(MethodView):
 
 
 class ActivateAccount(MethodView):
-    form = AccountActivationForm
     decorators = [requires_unactivated]
 
-    def get(self, token=None):
-        expired = invalid = user = None
-        if token is not None:
-            expired, invalid, user = get_token_status(token, "activate_account")
-
-        if invalid:
-            flash(_("Your account activation token is invalid."), "danger")
-            return redirect(url_for("auth.request_activation_token"))
-
-        if expired:
-            flash(_("Your account activation token is expired."), "danger")
-            return redirect(url_for("auth.request_activation_token"))
-
-        if user:
-            user.activated = True
-            user.save()
-
-            if current_user != user:
-                logout_user()
-                login_user(user)
-
-            flash(_("Your account has been activated."), "success")
-            return redirect(url_for("forum.index"))
-
-        return render_template("auth/account_activation.html", form=self.form())
+    def __init__(self, account_activator_factory):
+        self.account_activator_factory = account_activator_factory
 
-    def post(self, token=None):
-        expired = invalid = user = None
-        form = self.form()
+    def get(self, token=None):
+        activator = self.account_activator_factory()
+        try:
+            activator.activate_account(token)
+        except TokenError as e:
+            flash(e.reason, 'danger')
+        except ValidationError as e:
+            flash(e.reason, 'danger')
+            return redirect('forum.index')
+
+        else:
+            try:
+                db.session.commit()
+            except Exception:  # noqa
+                logger.exception("Database error while activating account")
+                flash(
+                    _(
+                        "Could not activate account due to an unrecoverable error"
+                    ), "danger"
+                )
 
-        if token is not None:
-            expired, invalid, user = get_token_status(token, "activate_account")
+                return redirect('auth.request_activation_token')
 
-        elif form.validate_on_submit():
-            expired, invalid, user = get_token_status(
-                form.token.data, "activate_account"
+            flash(
+                _("Your account has been activated and you can now login."),
+                "success"
             )
-
-        if invalid:
-            flash(_("Your account activation token is invalid."), "danger")
-            return redirect(url_for("auth.request_activation_token"))
-
-        if expired:
-            flash(_("Your account activation token is expired."), "danger")
-            return redirect(url_for("auth.request_activation_token"))
-
-        if user:
-            user.activated = True
-            user.save()
-
-            if current_user != user:
-                logout_user()
-                login_user(user)
-
-            flash(_("Your account has been activated."), "success")
             return redirect(url_for("forum.index"))
 
-        return render_template("auth/account_activation.html", form=form)
+        return render_template("auth/account_activation.html")
 
 
 @impl(tryfirst=True)
@@ -364,27 +339,6 @@ def flaskbb_load_blueprints(app):
             "errors/too_many_logins.html", timeout=error.description
         )
 
-    def registration_service_factory():
-        with app.app_context():
-            blacklist = [
-                w.strip()
-                for w in flaskbb_config["AUTH_USERNAME_BLACKLIST"].split(",")
-            ]
-
-            requirements = registration.UsernameRequirements(
-                min=flaskbb_config["AUTH_USERNAME_MIN_LENGTH"],
-                max=flaskbb_config["AUTH_USERNAME_MAX_LENGTH"],
-                blacklist=blacklist
-            )
-
-        validators = [
-            registration.EmailUniquenessValidator(User),
-            registration.UsernameUniquenessValidator(User),
-            registration.UsernameValidator(requirements)
-        ]
-
-        return RegistrationService(validators, UserRepository(db))
-
     # Activate rate limiting on the whole blueprint
     limiter.limit(
         login_rate_limit, error_message=login_rate_limit_message
@@ -401,25 +355,15 @@ def flaskbb_load_blueprints(app):
             registration_service_factory=registration_service_factory
         )
     )
+
     register_view(
         auth,
         routes=['/reset-password'],
-        view_func=ForgotPassword.as_view('forgot_password')
-    )
-
-    def reset_service_factory():
-        token_serializer = FlaskBBTokenSerializer(
-            app.config['SECRET_KEY'],
-            expiry=timedelta(hours=1)
-        )
-        verifiers = [
-            EmailMatchesUserToken(User)
-        ]
-        return ResetPasswordService(
-            token_serializer,
-            User,
-            token_verifiers=verifiers
+        view_func=ForgotPassword.as_view(
+            'forgot_password',
+            password_reset_service_factory=reset_service_factory
         )
+    )
 
     register_view(
         auth,
@@ -429,15 +373,22 @@ def flaskbb_load_blueprints(app):
             password_reset_service_factory=reset_service_factory
         )
     )
+
     register_view(
         auth,
         routes=['/activate'],
-        view_func=RequestActivationToken.as_view('request_activation_token')
+        view_func=RequestActivationToken.as_view(
+            'request_activation_token',
+            account_activator_factory=account_activator_factory
+        )
     )
+
     register_view(
         auth,
         routes=['/activate/confirm', '/activate/confirm/<token>'],
-        view_func=ActivateAccount.as_view('activate_account')
+        view_func=ActivateAccount.as_view(
+            'activate_account',
+            account_activator_factory=account_activator_factory
+        )
     )
-
     app.register_blueprint(auth, url_prefix=app.config['AUTH_URL_PREFIX'])

+ 24 - 0
flaskbb/core/auth/activation.py

@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.core.auth.activation
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+    Interfaces for handling account activation
+    in FlaskBB
+
+    :copyright: (c) 2014-2018 the FlaskBB Team
+    :license: BSD, see LICENSE for more details
+"""
+
+from abc import abstractmethod
+
+from ..._compat import ABC
+
+
+class AccountActivator(ABC):
+    @abstractmethod
+    def initiate_account_activation(self, user):
+        pass
+
+    @abstractmethod
+    def activate_account(self, token):
+        pass

+ 8 - 24
flaskbb/core/auth/password.py

@@ -9,33 +9,17 @@
     :license: BSD, see LICENSE for more details
 """
 
-from ..exceptions import StopValidation, ValidationError
-from ..tokens import TokenActions, TokenError
+from abc import abstractmethod
 
+from ..._compat import ABC
 
-class ResetPasswordService(object):
 
-    def __init__(self, token_serializer, users, token_verifiers):
-        self.token_serializer = token_serializer
-        self.users = users
-        self.token_verifiers = token_verifiers
+class ResetPasswordService(ABC):
 
-    def verify_token(self, token, email):
-        errors = []
-
-        for verifier in self.token_verifiers:
-            try:
-                verifier(token, email=email)
-            except ValidationError as e:
-                errors.append((e.attribute, e.reason))
-
-        if errors:
-            raise StopValidation(errors)
+    @abstractmethod
+    def initiate_password_reset(self, email):
+        pass
 
+    @abstractmethod
     def reset_password(self, token, email, new_password):
-        token = self.token_serializer.loads(token)
-        if token.operation != TokenActions.RESET_PASSWORD:
-            raise TokenError.invalid()
-        self.verify_token(token, email)
-        user = self.users.query.get(token.user_id)
-        user.password = new_password
+        pass

+ 3 - 17
flaskbb/core/auth/registration.py

@@ -41,21 +41,7 @@ class UserValidator(ABC):
         return self.validate(user_info)
 
 
-class RegistrationService(object):
-    def __init__(self, validators, user_repo):
-        self.validators = validators
-        self.user_repo = user_repo
-
+class UserRegistrationService(ABC):
+    @abstractmethod
     def register(self, user_info):
-        failures = []
-
-        for v in self.validators:
-            try:
-                v(user_info)
-            except ValidationError as e:
-                failures.append((e.attribute, e.reason))
-
-        if failures:
-            raise StopValidation(failures)
-
-        self.user_repo.add(user_info)
+        pass

+ 8 - 3
flaskbb/core/tokens.py

@@ -12,34 +12,37 @@
 from abc import abstractmethod
 
 import attr
+from flask_babelplus import gettext as _
 
 from .._compat import ABC
 from .exceptions import BaseFlaskBBError
 
+
 class TokenError(BaseFlaskBBError):
     """
     Raised when there is an issue with deserializing
     a token. Has helper classmethods to ensure
     consistent verbiage.
     """
+
     def __init__(self, reason):
         self.reason = reason
         super(TokenError, self).__init__(reason)
 
     @classmethod
     def invalid(cls):
-        return cls('Token is invalid')
+        return cls(_('Token is invalid'))
 
     @classmethod
     def expired(cls):
-        return cls('Token is expired')
+        return cls(_('Token is expired'))
 
     # in theory this would never be raised
     # but it's provided for a generic catchall
     # when processing goes horribly wrong
     @classmethod  # pragma: no cover
     def bad(cls):
-        return cls('Token cannot be processed')
+        return cls(_('Token cannot be processed'))
 
 
 # holder for token actions
@@ -65,6 +68,7 @@ class TokenSerializer(ABC):
     loads must accept a string representation of
     a JWT and produce a token instance
     """
+
     @abstractmethod
     def dumps(self, token):
         pass
@@ -83,6 +87,7 @@ class TokenVerifier(ABC):
     Should raise a flaskbb.core.exceptions.ValidationError
     if verification fails.
     """
+
     @abstractmethod
     def verify_token(self, token, **kwargs):
         pass

+ 4 - 7
flaskbb/email.py

@@ -14,21 +14,19 @@ from flask_mail import Message
 from flask_babelplus import lazy_gettext as _
 
 from flaskbb.extensions import mail, celery
-from flaskbb.utils.tokens import make_token
 
 
 logger = logging.getLogger(__name__)
 
 
 @celery.task
-def send_reset_token(user_id, username, email):
+def send_reset_token(token, username, email):
     """Sends the reset token to the user's email address.
 
-    :param user_id: The user id. Used to generate the reset token.
+    :param token: The token to send to the user
     :param username: The username to whom the email should be sent.
     :param email:  The email address of the user
     """
-    token = make_token(user_id=user_id, operation="reset_password")
     send_email(
         subject=_("Password Recovery Confirmation"),
         recipients=[email],
@@ -46,14 +44,13 @@ def send_reset_token(user_id, username, email):
 
 
 @celery.task
-def send_activation_token(user_id, username, email):
+def send_activation_token(token, username, email):
     """Sends the activation token to the user's email address.
 
-    :param user_id: The user id. Used to generate the reset token.
+    :param token: The token to send to the user
     :param username: The username to whom the email should be sent.
     :param email:  The email address of the user
     """
-    token = make_token(user_id=user_id, operation="activate_account")
     send_email(
         subject=_("Account Activation"),
         recipients=[email],

+ 0 - 75
flaskbb/utils/tokens.py

@@ -1,75 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    flaskbb.utils.tokens
-    ~~~~~~~~~~~~~~~~~~~~
-
-    A module that helps to create and verify various tokens that
-    are used by FlaskBB.
-
-    :copyright: (c) 2014 by the FlaskBB Team.
-    :license: BSD, see LICENSE for more details.
-"""
-import logging
-from flask import current_app
-from itsdangerous import (TimedJSONWebSignatureSerializer, SignatureExpired,
-                          BadSignature)
-
-from flaskbb.user.models import User
-
-
-logger = logging.getLogger(__name__)
-
-
-def make_token(user_id, operation, expire=3600):
-    """Generates a JSON Web Signature (JWS).
-    See `RFC 7515 <https://tools.ietf.org/html/rfc7515>` if you want to know
-    more about JWS.
-
-    :param user_id: The user_id for which the token should be generated.
-    :param operation: The function of the token. For example, you might want
-                      to generate two different tokens. One for a
-                      password reset link, which you hypothetically want
-                      to name 'reset' and the second one, for the generation
-                      of a token for a E-Mail confirmation link, which you
-                      name 'email'.
-    :param expire: The time, in seconds, after which the token should be
-                   invalid. Defaults to 3600.
-    """
-    s = TimedJSONWebSignatureSerializer(
-        current_app.config['SECRET_KEY'], expire
-    )
-    data = {"id": user_id, "op": operation}
-    return s.dumps(data)
-
-
-def get_token_status(token, operation, return_data=False):
-    """Returns the expired status, invalid status, the user and optionally
-    the content of the JSON Web Signature token.
-
-    :param token: A valid JSON Web Signature token.
-    :param operation: The function of the token.
-    :param return_data: If set to ``True``, it will also return the content
-                        of the token.
-    """
-    s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
-    user, data = None, None
-    expired, invalid = False, False
-
-    try:
-        data = s.loads(token)
-    except SignatureExpired:
-        expired = True
-    except (BadSignature, TypeError, ValueError):
-        invalid = True
-
-    if data is not None:
-        # check if the operation matches the one from the token
-        if operation == data.get("op", None):
-            user = User.query.filter_by(id=data.get('id')).first()
-        else:
-            invalid = True
-
-    if return_data:
-        return expired, invalid, user, data
-
-    return expired, invalid, user

+ 3 - 2
tests/conftest.py

@@ -1,5 +1,6 @@
 from tests.fixtures.app import *  # noqa
+from tests.fixtures.auth import *  # noqa
 from tests.fixtures.forum import *  # noqa
-from tests.fixtures.user import *  # noqa
-from tests.fixtures.settings_fixture import *  # noqa
 from tests.fixtures.plugin import *  # noqa
+from tests.fixtures.settings_fixture import *  # noqa
+from tests.fixtures.user import *  # noqa

+ 0 - 66
tests/core/auth/test_password.py

@@ -1,66 +0,0 @@
-import json
-
-import pytest
-from flaskbb.core.auth import password
-from flaskbb.core.exceptions import StopValidation, ValidationError
-from flaskbb.core.tokens import Token, TokenActions, TokenError
-from flaskbb.user.models import User
-from werkzeug.security import check_password_hash
-
-
-class SimpleTokenSerializer:
-
-    @staticmethod
-    def dumps(token):
-        return json.dumps({'user_id': token.user_id, 'op': token.operation})
-
-    @staticmethod
-    def loads(raw_token):
-        loaded = json.loads(raw_token)
-        return Token(user_id=loaded['user_id'], operation=loaded['op'])
-
-
-class TestPasswordReset(object):
-
-    def test_raises_token_error_if_not_a_password_reset(self):
-        service = password.ResetPasswordService(
-            SimpleTokenSerializer, User, []
-        )
-        raw_token = SimpleTokenSerializer.dumps(
-            Token(user_id=1, operation=TokenActions.ACTIVATE_ACCOUNT)
-        )
-
-        with pytest.raises(TokenError) as excinfo:
-            service.reset_password(
-                raw_token, "some@e.mail", "a great password!"
-            )
-
-        assert "invalid" in str(excinfo.value)
-
-    def test_raises_StopValidation_if_verifiers_fail(self):
-        token = SimpleTokenSerializer.dumps(
-            Token(user_id=1, operation=TokenActions.RESET_PASSWORD)
-        )
-
-        def verifier(*a, **k):
-            raise ValidationError('attr', 'no')
-
-        service = password.ResetPasswordService(
-            SimpleTokenSerializer, User, [verifier]
-        )
-
-        with pytest.raises(StopValidation) as excinfo:
-            service.reset_password(token, "an@e.mail", "great password!")
-        assert ("attr", "no") in excinfo.value.reasons
-
-    def test_sets_user_password_to_provided_if_verifiers_pass(self, Fred):
-        token = SimpleTokenSerializer.dumps(
-            Token(user_id=Fred.id, operation=TokenActions.RESET_PASSWORD)
-        )
-
-        service = password.ResetPasswordService(
-            SimpleTokenSerializer, User, []
-        )
-
-        service.reset_password(token, Fred.email, "newpasswordwhodis")
-        assert check_password_hash(Fred.password, "newpasswordwhodis")

+ 22 - 0
tests/fixtures/auth.py

@@ -0,0 +1,22 @@
+import json
+
+import pytest
+
+from flaskbb.core.tokens import Token
+
+
+class SimpleTokenSerializer:
+
+    @staticmethod
+    def dumps(token):
+        return json.dumps({'user_id': token.user_id, 'op': token.operation})
+
+    @staticmethod
+    def loads(raw_token):
+        loaded = json.loads(raw_token)
+        return Token(user_id=loaded['user_id'], operation=loaded['op'])
+
+
+@pytest.fixture(scope='session')
+def token_serializer():
+    return SimpleTokenSerializer

+ 12 - 0
tests/fixtures/user.py

@@ -65,3 +65,15 @@ def Fred(default_groups):
                 activated=True)
     fred.save()
     return fred
+
+
+@pytest.fixture
+def unactivated_user(default_groups):
+    """
+    Creates an unactivated user in the default user group
+    """
+    user = User(username='notactive', email='not@active.com',
+                password='password', primary_group=default_groups[3],
+                activated=False)
+    user.save()
+    return user

+ 85 - 0
tests/unit/auth/test_activation.py

@@ -0,0 +1,85 @@
+import pytest
+
+from flaskbb.auth.services import activation
+from flaskbb.core.exceptions import ValidationError
+from flaskbb.core.tokens import Token, TokenActions, TokenError
+from flaskbb.user.models import User
+
+pytestmark = pytest.mark.usefixtures('default_settings')
+
+
+class TestAccountActivationInitiateActivation(object):
+
+    def test_raises_if_user_doesnt_exist(self, Fred, token_serializer):
+        service = activation.AccountActivator(token_serializer, User)
+
+        with pytest.raises(ValidationError) as excinfo:
+            service.initiate_account_activation('does@not.exist')
+
+        assert excinfo.value.reason == "Entered email doesn't exist"
+
+    def test_raises_if_user_is_already_active(self, Fred, token_serializer):
+        service = activation.AccountActivator(token_serializer, User)
+
+        with pytest.raises(ValidationError) as excinfo:
+            service.initiate_account_activation(Fred.email)
+
+        assert excinfo.value.reason == "Account is already activated"
+
+    def test_calls_send_activation_token_successfully_if_user_exists(
+            self, mocker, unactivated_user, token_serializer
+    ):
+        service = activation.AccountActivator(token_serializer, User)
+        mock = mocker.MagicMock()
+        with mocker.patch(
+                'flaskbb.auth.services.activation.send_activation_token.delay',
+                mock):
+            service.initiate_account_activation(unactivated_user.email)
+
+        token = token_serializer.dumps(
+            Token(
+                user_id=unactivated_user.id,
+                operation=TokenActions.ACTIVATE_ACCOUNT
+            )
+        )
+        mock.assert_called_once_with(
+            token=token,
+            username=unactivated_user.username,
+            email=unactivated_user.email
+        )
+
+
+class TestAccountActivationActivateAccount(object):
+
+    def test_raises_if_token_operation_isnt_activate(self, token_serializer):
+        service = activation.AccountActivator(token_serializer, User)
+        token = token_serializer.dumps(
+            Token(user_id=1, operation=TokenActions.RESET_PASSWORD)
+        )
+
+        with pytest.raises(TokenError):
+            service.activate_account(token)
+
+    def test_raises_if_user_is_already_active(self, Fred, token_serializer):
+        service = activation.AccountActivator(token_serializer, User)
+        token = token_serializer.dumps(
+            Token(user_id=Fred.id, operation=TokenActions.ACTIVATE_ACCOUNT)
+        )
+
+        with pytest.raises(ValidationError) as excinfo:
+            service.activate_account(token)
+
+        assert excinfo.value.reason == 'Account is already activated'
+
+    def test_activates_user_successfully(
+            self, unactivated_user, token_serializer
+    ):
+        service = activation.AccountActivator(token_serializer, User)
+        token = token_serializer.dumps(
+            Token(
+                user_id=unactivated_user.id,
+                operation=TokenActions.ACTIVATE_ACCOUNT
+            )
+        )
+        service.activate_account(token)
+        assert unactivated_user.activated

+ 83 - 0
tests/unit/auth/test_password.py

@@ -0,0 +1,83 @@
+import pytest
+from werkzeug.security import check_password_hash
+
+from flaskbb.auth.services import password
+from flaskbb.core.exceptions import StopValidation, ValidationError
+from flaskbb.core.tokens import Token, TokenActions, TokenError
+from flaskbb.user.models import User
+
+pytestmark = pytest.mark.usefixtures('default_settings')
+
+
+class TestPasswordReset(object):
+
+    def test_raises_token_error_if_not_a_password_reset(
+            self, token_serializer
+    ):
+        service = password.ResetPasswordService(token_serializer, User, [])
+        raw_token = token_serializer.dumps(
+            Token(user_id=1, operation=TokenActions.ACTIVATE_ACCOUNT)
+        )
+
+        with pytest.raises(TokenError) as excinfo:
+            service.reset_password(
+                raw_token, "some@e.mail", "a great password!"
+            )
+
+        assert "invalid" in str(excinfo.value)
+
+    def test_raises_StopValidation_if_verifiers_fail(self, token_serializer):
+        token = token_serializer.dumps(
+            Token(user_id=1, operation=TokenActions.RESET_PASSWORD)
+        )
+
+        def verifier(*a, **k):
+            raise ValidationError('attr', 'no')
+
+        service = password.ResetPasswordService(
+            token_serializer, User, [verifier]
+        )
+
+        with pytest.raises(StopValidation) as excinfo:
+            service.reset_password(token, "an@e.mail", "great password!")
+        assert ("attr", "no") in excinfo.value.reasons
+
+    def test_sets_user_password_to_provided_if_verifiers_pass(
+            self, token_serializer, Fred
+    ):
+        token = token_serializer.dumps(
+            Token(user_id=Fred.id, operation=TokenActions.RESET_PASSWORD)
+        )
+
+        service = password.ResetPasswordService(token_serializer, User, [])
+
+        service.reset_password(token, Fred.email, "newpasswordwhodis")
+        assert check_password_hash(Fred.password, "newpasswordwhodis")
+
+    # need fred to initiate Users
+    def test_initiate_raises_if_user_doesnt_exist(
+            self, token_serializer, Fred
+    ):
+        service = password.ResetPasswordService(token_serializer, User, [])
+        with pytest.raises(ValidationError) as excinfo:
+            service.initiate_password_reset('lol@doesnt.exist')
+
+        assert excinfo.value.attribute == 'email'
+        assert excinfo.value.reason == 'Invalid email'
+
+    def test_calls_send_reset_token_successfully_if_user_exists(
+            self, Fred, mocker, token_serializer
+    ):
+        service = password.ResetPasswordService(token_serializer, User, [])
+        mock = mocker.MagicMock()
+
+        with mocker.patch(
+                'flaskbb.auth.services.password.send_reset_token.delay', mock):
+            service.initiate_password_reset(Fred.email)
+
+        token = token_serializer.dumps(
+            Token(user_id=Fred.id, operation=TokenActions.RESET_PASSWORD)
+        )
+        mock.assert_called_once_with(
+            token=token, username=Fred.username, email=Fred.email
+        )

+ 10 - 10
tests/core/auth/test_registration.py → tests/unit/auth/test_registration.py

@@ -1,26 +1,26 @@
 import pytest
-from flaskbb.core.auth import registration
+
+from flaskbb.auth.services import registration
+from flaskbb.core.auth.registration import UserRegistrationInfo
 from flaskbb.core.exceptions import StopValidation, ValidationError
 from flaskbb.core.user.repo import UserRepository
 
+pytestmark = pytest.mark.usefixtures('default_settings')
+
 
 class RaisingValidator(registration.UserValidator):
 
     def validate(self, user_info):
-        raise ValidationError(
-            'test', 'just a little whoopsie-diddle'
-        )
+        raise ValidationError('test', 'just a little whoopsie-diddle')
 
 
-def test_doesnt_register_user_if_validator_fails_with_ValidationError(
-        mocker
-):
+def test_doesnt_register_user_if_validator_fails_with_ValidationError(mocker):
     repo = mocker.Mock(UserRepository)
     service = registration.RegistrationService([RaisingValidator()], repo)
 
     with pytest.raises(StopValidation):
         service.register(
-            registration.UserRegistrationInfo(
+            UserRegistrationInfo(
                 username='fred',
                 password='lol',
                 email='fred@fred.fred',
@@ -40,7 +40,7 @@ def test_gathers_up_all_errors_during_registration(mocker):
 
     with pytest.raises(StopValidation) as excinfo:
         service.register(
-            registration.UserRegistrationInfo(
+            UserRegistrationInfo(
                 username='fred',
                 password='lol',
                 email='fred@fred.fred',
@@ -58,7 +58,7 @@ def test_gathers_up_all_errors_during_registration(mocker):
 def test_registers_user_if_no_errors_occurs(mocker):
     repo = mocker.Mock(UserRepository)
     service = registration.RegistrationService([], repo)
-    user_info = registration.UserRegistrationInfo(
+    user_info = UserRegistrationInfo(
         username='fred',
         password='lol',
         email='fred@fred.fred',

+ 3 - 0
tests/unit/auth/test_validators.py

@@ -1,4 +1,5 @@
 import pytest
+
 from flaskbb.auth.services.registration import (EmailUniquenessValidator,
                                                 UsernameRequirements,
                                                 UsernameUniquenessValidator,
@@ -7,6 +8,8 @@ from flaskbb.core.auth.registration import UserRegistrationInfo
 from flaskbb.core.exceptions import ValidationError
 from flaskbb.user.models import User
 
+pytestmark = pytest.mark.usefixtures('default_settings')
+
 
 def test_raises_if_username_too_short():
     requirements = UsernameRequirements(min=4, max=100, blacklist=set())

+ 2 - 0
tests/unit/tokens/test_serializer.py

@@ -6,6 +6,8 @@ from freezegun import freeze_time
 from flaskbb import tokens
 from flaskbb.core.tokens import Token, TokenActions, TokenError
 
+pytestmark = pytest.mark.usefixtures('default_settings')
+
 
 def test_can_round_trip_token():
     serializer = tokens.FlaskBBTokenSerializer(

+ 3 - 1
tests/unit/tokens/test_verifiers.py

@@ -1,10 +1,12 @@
 import pytest
 
-from flaskbb.core.tokens import Token, TokenActions
 from flaskbb.core.exceptions import ValidationError
+from flaskbb.core.tokens import Token, TokenActions
 from flaskbb.tokens import verifiers
 from flaskbb.user.models import User
 
+pytestmark = pytest.mark.usefixtures('default_settings')
+
 
 def test_raises_if_email_doesnt_match_token_user(Fred):
     verifier = verifiers.EmailMatchesUserToken(User)

+ 0 - 55
tests/unit/utils/test_tokens.py

@@ -1,55 +0,0 @@
-from flask import current_app
-from itsdangerous import TimedJSONWebSignatureSerializer
-from flaskbb.utils.tokens import make_token, get_token_status
-
-
-def test_make_token(user):
-    token = make_token(user.id, "test")
-    s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
-    unpacked_token = s.loads(token)
-    assert user.id == unpacked_token["id"]
-    assert "test" == unpacked_token["op"]
-
-
-def test_valid_token_status(user):
-    token = make_token(user.id, "valid_test")
-    expired, invalid, token_user = get_token_status(token, "valid_test")
-
-    assert not expired
-    assert not invalid
-    assert token_user == user
-
-
-def test_token_status_with_data(user):
-    token = make_token(user.id, "test_data")
-    expired, invalid, token_user, data = \
-        get_token_status(token, "test_data", return_data=True)
-    assert user.id == data["id"]
-    assert "test_data" == data["op"]
-
-
-def test_token_operation(user):
-    token = make_token(user.id, "operation_test")
-    expired, invalid, token_user = get_token_status(token, "invalid_op")
-    assert invalid
-    assert not expired
-    assert not token_user
-
-
-def test_invalid_token_status(user):
-    token = "this-is-not-a-token"
-    expired, invalid, token_user, data = \
-        get_token_status(token, "invalid_test", return_data=True)
-
-    assert invalid
-    assert not expired
-    assert not token_user
-    assert data is None
-
-
-def test_expired_token_status(user):
-    token = make_token(user.id, "expired_test", -1)
-    expired, invalid, token_user = get_token_status(token, "expired_test")
-    assert expired
-    assert not invalid
-    assert not token_user