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,
 from wtforms import (BooleanField, HiddenField, PasswordField, SelectField,
                      StringField, SubmitField)
                      StringField, SubmitField)
 from wtforms.validators import (DataRequired, Email, EqualTo, InputRequired,
 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.fields import RecaptchaField
 from flaskbb.utils.forms import FlaskBBForm
 from flaskbb.utils.forms import FlaskBBForm
-from flaskbb.utils.helpers import time_utcnow
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-
 USERNAME_RE = r'^[\w.+-]+$'
 USERNAME_RE = r'^[\w.+-]+$'
 is_valid_username = regexp(
 is_valid_username = regexp(
     USERNAME_RE, message=_("You can only use letters, numbers or dashes.")
     USERNAME_RE, message=_("You can only use letters, numbers or dashes.")
@@ -31,12 +28,19 @@ is_valid_username = regexp(
 
 
 
 
 class LoginForm(FlaskBBForm):
 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)
     remember_me = BooleanField(_("Remember me"), default=False)
 
 
@@ -49,17 +53,29 @@ class LoginRecaptchaForm(LoginForm):
 
 
 
 
 class RegisterForm(FlaskBBForm):
 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'))
     confirm_password = PasswordField(_('Confirm password'))
 
 
@@ -67,23 +83,32 @@ class RegisterForm(FlaskBBForm):
 
 
     language = SelectField(_('Language'))
     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"))
     submit = SubmitField(_("Register"))
 
 
 
 
 class ReauthForm(FlaskBBForm):
 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"))
     submit = SubmitField(_("Refresh Login"))
 
 
 
 
 class ForgotPasswordForm(FlaskBBForm):
 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"))
     recaptcha = RecaptchaField(_("Captcha"))
 
 
@@ -93,13 +118,21 @@ class ForgotPasswordForm(FlaskBBForm):
 class ResetPasswordForm(FlaskBBForm):
 class ResetPasswordForm(FlaskBBForm):
     token = HiddenField('Token')
     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'))
     confirm_password = PasswordField(_('Confirm password'))
 
 
@@ -107,30 +140,20 @@ class ResetPasswordForm(FlaskBBForm):
 
 
 
 
 class RequestActivationForm(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"))
     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,
 from flask_login import (confirm_login, current_user, login_fresh,
                          login_required, login_user, logout_user)
                          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.exceptions import AuthenticationError
 from flaskbb.extensions import db, limiter
 from flaskbb.extensions import db, limiter
 from flaskbb.user.models import User
 from flaskbb.user.models import User
@@ -32,17 +30,17 @@ from flaskbb.utils.helpers import (anonymous_required, enforce_recaptcha,
                                    registration_enabled, render_template,
                                    registration_enabled, render_template,
                                    requires_unactivated)
                                    requires_unactivated)
 from flaskbb.utils.settings import flaskbb_config
 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 ..core.tokens import TokenError
 from ..tokens import FlaskBBTokenSerializer
 from ..tokens import FlaskBBTokenSerializer
 from ..tokens.verifiers import EmailMatchesUserToken
 from ..tokens.verifiers import EmailMatchesUserToken
 from ..user.repo import UserRepository
 from ..user.repo import UserRepository
 from .plugins import impl
 from .plugins import impl
+from .services import registration
+from .services.activation import AccountActivator
+from .services.password import ResetPasswordService
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -147,10 +145,11 @@ class Register(MethodView):
                 try:
                 try:
                     db.session.commit()
                     db.session.commit()
                 except Exception:  # noqa
                 except Exception:  # noqa
-                    logger.exception("Uh that looks bad...")
+                    logger.exception("Database error while resetting password")
                     flash(
                     flash(
                         _(
                         _(
-                            "Could not process registration due to an unrecoverable error"
+                            "Could not process registration due"
+                            "to an unrecoverable error"
                         ), "danger"
                         ), "danger"
                     )
                     )
 
 
@@ -179,7 +178,8 @@ class ForgotPassword(MethodView):
         if form.validate_on_submit():
         if form.validate_on_submit():
 
 
             try:
             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:
             except ValidationError:
                 flash(
                 flash(
                     _(
                     _(
@@ -212,9 +212,7 @@ class ResetPassword(MethodView):
             try:
             try:
                 service = self.password_reset_service_factory()
                 service = self.password_reset_service_factory()
                 service.reset_password(
                 service.reset_password(
-                    token,
-                    form.email.data,
-                    form.password.data
+                    token, form.email.data, form.password.data
                 )
                 )
                 db.session.commit()
                 db.session.commit()
             except TokenError as e:
             except TokenError as e:
@@ -240,6 +238,9 @@ class RequestActivationToken(MethodView):
     decorators = [requires_unactivated]
     decorators = [requires_unactivated]
     form = RequestActivationForm
     form = RequestActivationForm
 
 
+    def __init__(self, account_activator_factory):
+        self.account_activator_factory = account_activator_factory
+
     def get(self):
     def get(self):
         return render_template(
         return render_template(
             "auth/request_account_activation.html", form=self.form()
             "auth/request_account_activation.html", form=self.form()
@@ -248,17 +249,19 @@ class RequestActivationToken(MethodView):
     def post(self):
     def post(self):
         form = self.form()
         form = self.form()
         if form.validate_on_submit():
         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(
         return render_template(
             "auth/request_account_activation.html", form=form
             "auth/request_account_activation.html", form=form
@@ -266,67 +269,40 @@ class RequestActivationToken(MethodView):
 
 
 
 
 class ActivateAccount(MethodView):
 class ActivateAccount(MethodView):
-    form = AccountActivationForm
     decorators = [requires_unactivated]
     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 redirect(url_for("forum.index"))
 
 
-        return render_template("auth/account_activation.html", form=form)
+        return render_template("auth/account_activation.html")
 
 
 
 
 @impl(tryfirst=True)
 @impl(tryfirst=True)
@@ -406,16 +382,11 @@ def flaskbb_load_blueprints(app):
 
 
     def reset_service_factory():
     def reset_service_factory():
         token_serializer = FlaskBBTokenSerializer(
         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(
         return ResetPasswordService(
-            token_serializer,
-            User,
-            token_verifiers=verifiers
+            token_serializer, User, token_verifiers=verifiers
         )
         )
 
 
     register_view(
     register_view(
@@ -435,15 +406,29 @@ def flaskbb_load_blueprints(app):
             password_reset_service_factory=reset_service_factory
             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(
     register_view(
         auth,
         auth,
         routes=['/activate'],
         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(
     register_view(
         auth,
         auth,
         routes=['/activate/confirm', '/activate/confirm/<token>'],
         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'])
     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.app import *  # noqa
+from tests.fixtures.auth import *  # noqa
 from tests.fixtures.forum 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.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)
                 activated=True)
     fred.save()
     fred.save()
     return fred
     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
 import pytest
 from werkzeug.security import check_password_hash
 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
 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):
 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)
             Token(user_id=1, operation=TokenActions.ACTIVATE_ACCOUNT)
         )
         )
 
 
@@ -38,8 +24,8 @@ class TestPasswordReset(object):
 
 
         assert "invalid" in str(excinfo.value)
         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)
             Token(user_id=1, operation=TokenActions.RESET_PASSWORD)
         )
         )
 
 
@@ -47,30 +33,30 @@ class TestPasswordReset(object):
             raise ValidationError('attr', 'no')
             raise ValidationError('attr', 'no')
 
 
         service = password.ResetPasswordService(
         service = password.ResetPasswordService(
-            SimpleTokenSerializer, User, [verifier]
+            token_serializer, User, [verifier]
         )
         )
 
 
         with pytest.raises(StopValidation) as excinfo:
         with pytest.raises(StopValidation) as excinfo:
             service.reset_password(token, "an@e.mail", "great password!")
             service.reset_password(token, "an@e.mail", "great password!")
         assert ("attr", "no") in excinfo.value.reasons
         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)
             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")
         service.reset_password(token, Fred.email, "newpasswordwhodis")
         assert check_password_hash(Fred.password, "newpasswordwhodis")
         assert check_password_hash(Fred.password, "newpasswordwhodis")
 
 
     # need fred to initiate Users
     # 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:
         with pytest.raises(ValidationError) as excinfo:
             service.initiate_password_reset('lol@doesnt.exist')
             service.initiate_password_reset('lol@doesnt.exist')
 
 
@@ -78,19 +64,16 @@ class TestPasswordReset(object):
         assert excinfo.value.reason == 'Invalid email'
         assert excinfo.value.reason == 'Invalid email'
 
 
     def test_calls_send_reset_token_successfully_if_user_exists(
     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()
         mock = mocker.MagicMock()
 
 
         with mocker.patch(
         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)
             service.initiate_password_reset(Fred.email)
 
 
-        token = SimpleTokenSerializer.dumps(
+        token = token_serializer.dumps(
             Token(user_id=Fred.id, operation=TokenActions.RESET_PASSWORD)
             Token(user_id=Fred.id, operation=TokenActions.RESET_PASSWORD)
         )
         )
         mock.assert_called_once_with(
         mock.assert_called_once_with(