Browse Source

Implement account activation services

Alec Nikolas Reiter 7 years ago
parent
commit
1de148dbe8

+ 83 - 60
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,23 +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"))
 
 
 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"))
 
@@ -93,13 +118,21 @@ 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'))
 
@@ -107,30 +140,20 @@ class ResetPasswordForm(FlaskBBForm):
 
 
 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"))

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

@@ -0,0 +1,47 @@
+# -*- 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 ...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

+ 75 - 90
flaskbb/auth/views.py

@@ -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
+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,17 @@ 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 .services.password import ResetPasswordService
-from ..core.auth.registration import (RegistrationService, UserRegistrationInfo)
-from ..core.exceptions import ValidationError, StopValidation
+from ..core.auth.registration import RegistrationService, 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 registration
+from .services.activation import AccountActivator
+from .services.password import ResetPasswordService
 
 logger = logging.getLogger(__name__)
 
@@ -147,10 +145,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"
                     )
 
@@ -179,7 +178,8 @@ class ForgotPassword(MethodView):
         if form.validate_on_submit():
 
             try:
-                self.password_reset_service_factory().initiate_password_reset(form.email.data)
+                self.password_reset_service_factory(
+                ).initiate_password_reset(form.email.data)
             except ValidationError:
                 flash(
                     _(
@@ -212,9 +212,7 @@ 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:
@@ -240,6 +238,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()
@@ -248,17 +249,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
@@ -266,67 +269,40 @@ 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 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)
@@ -406,16 +382,11 @@ def flaskbb_load_blueprints(app):
 
     def reset_service_factory():
         token_serializer = FlaskBBTokenSerializer(
-            app.config['SECRET_KEY'],
-            expiry=timedelta(hours=1)
+            app.config['SECRET_KEY'], expiry=timedelta(hours=1)
         )
-        verifiers = [
-            EmailMatchesUserToken(User)
-        ]
+        verifiers = [EmailMatchesUserToken(User)]
         return ResetPasswordService(
-            token_serializer,
-            User,
-            token_verifiers=verifiers
+            token_serializer, User, token_verifiers=verifiers
         )
 
     register_view(
@@ -435,15 +406,29 @@ def flaskbb_load_blueprints(app):
             password_reset_service_factory=reset_service_factory
         )
     )
+
+    def account_activator_factory():
+        token_serializer = FlaskBBTokenSerializer(
+            app.config['SECRET_KEY'], expiry=timedelta(hours=1)
+        )
+        return AccountActivator(token_serializer, User)
+
     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

+ 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

+ 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

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

@@ -0,0 +1,83 @@
+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
+
+
+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

+ 21 - 38
tests/unit/auth/test_password.py

@@ -1,5 +1,3 @@
-import json
-
 import pytest
 from werkzeug.security import check_password_hash
 
@@ -9,25 +7,13 @@ from flaskbb.core.tokens import Token, TokenActions, TokenError
 from flaskbb.user.models import User
 
 
-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(
+    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)
         )
 
@@ -38,8 +24,8 @@ class TestPasswordReset(object):
 
         assert "invalid" in str(excinfo.value)
 
-    def test_raises_StopValidation_if_verifiers_fail(self):
-        token = SimpleTokenSerializer.dumps(
+    def test_raises_StopValidation_if_verifiers_fail(self, token_serializer):
+        token = token_serializer.dumps(
             Token(user_id=1, operation=TokenActions.RESET_PASSWORD)
         )
 
@@ -47,30 +33,30 @@ class TestPasswordReset(object):
             raise ValidationError('attr', 'no')
 
         service = password.ResetPasswordService(
-            SimpleTokenSerializer, User, [verifier]
+            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, Fred):
-        token = SimpleTokenSerializer.dumps(
+    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(
-            SimpleTokenSerializer, User, []
-        )
+        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, Fred):
-        service = password.ResetPasswordService(
-            SimpleTokenSerializer, User, []
-        )
+    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')
 
@@ -78,19 +64,16 @@ class TestPasswordReset(object):
         assert excinfo.value.reason == 'Invalid email'
 
     def test_calls_send_reset_token_successfully_if_user_exists(
-            self, Fred, mocker
+            self, Fred, mocker, token_serializer
     ):
-        service = password.ResetPasswordService(
-            SimpleTokenSerializer, User, []
-        )
+        service = password.ResetPasswordService(token_serializer, User, [])
         mock = mocker.MagicMock()
 
         with mocker.patch(
-            'flaskbb.auth.services.password.send_reset_token.delay', mock
-        ):
+                'flaskbb.auth.services.password.send_reset_token.delay', mock):
             service.initiate_password_reset(Fred.email)
 
-        token = SimpleTokenSerializer.dumps(
+        token = token_serializer.dumps(
             Token(user_id=Fred.id, operation=TokenActions.RESET_PASSWORD)
         )
         mock.assert_called_once_with(