From 01ae44c68bdfac1b5993d2d47daa19bb7b726df0 Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 12 Mar 2023 11:09:04 +0100 Subject: [PATCH] CLI - init user creation command --- fittrackee/tests/users/test_users_utils.py | 152 ++++++++++++++++++++- fittrackee/users/exceptions.py | 8 ++ fittrackee/users/utils/admin.py | 70 +++++++++- 3 files changed, 227 insertions(+), 3 deletions(-) diff --git a/fittrackee/tests/users/test_users_utils.py b/fittrackee/tests/users/test_users_utils.py index 68ffda52..8b168daa 100644 --- a/fittrackee/tests/users/test_users_utils.py +++ b/fittrackee/tests/users/test_users_utils.py @@ -13,6 +13,7 @@ from flask import Flask from fittrackee import bcrypt, db from fittrackee.users.exceptions import ( InvalidEmailException, + UserCreationException, UserNotFoundException, ) from fittrackee.users.models import BlacklistedToken, User @@ -32,7 +33,7 @@ from fittrackee.users.utils.token import ( from ..utils import random_email, random_int, random_string -class TestUserManagerService: +class TestUserManagerServiceUserUpdate: def test_it_raises_exception_if_user_does_not_exist( self, app: Flask ) -> None: @@ -190,6 +191,155 @@ class TestUserManagerService: assert user_1.confirmation_token is None +class TestUserManagerServiceUserCreation: + def test_it_raises_exception_if_provided_username_is_invalid( + self, app: Flask + ) -> None: + user_manager_service = UserManagerService(username='.admin') + with pytest.raises( + UserCreationException, + match=( + 'username: only alphanumeric characters and ' + 'the underscore character "_" allowed\n' + ), + ): + user_manager_service.create(email=random_email()) + + def test_it_raises_exception_if_a_user_exists_with_same_username( + self, app: Flask, user_1: User + ) -> None: + user_manager_service = UserManagerService(username=user_1.username) + with pytest.raises( + UserCreationException, + match='sorry, that username is already taken', + ): + user_manager_service.create(email=random_email()) + + def test_it_raises_exception_if_provided_email_is_invalid( + self, app: Flask + ) -> None: + user_manager_service = UserManagerService(username=random_string()) + with pytest.raises( + UserCreationException, match='valid email must be provided' + ): + user_manager_service.create(email=random_string()) + + def test_it_raises_exception_if_a_user_exists_with_same_email( + self, app: Flask, user_1: User + ) -> None: + user_manager_service = UserManagerService(username=random_string()) + with pytest.raises( + UserCreationException, + match='This user already exists. No action done.', + ): + user_manager_service.create(email=user_1.email) + + def test_it_creates_user_with_provided_password(self, app: Flask) -> None: + username = random_string() + email = random_email() + password = random_string() + user_manager_service = UserManagerService(username=username) + + new_user, user_password = user_manager_service.create(email, password) + + assert new_user + assert new_user.username == username + assert new_user.email == email + assert bcrypt.check_password_hash(new_user.password, password) + assert user_password == password + + def test_it_creates_user_when_password_is_not_provided( + self, app: Flask + ) -> None: + username = random_string() + email = random_email() + user_manager_service = UserManagerService(username=username) + + new_user, user_password = user_manager_service.create(email) + + assert new_user + assert new_user.username == username + assert new_user.email == email + assert bcrypt.check_password_hash(new_user.password, user_password) + + def test_it_creates_when_registration_is_not_enabled( + self, + app_with_3_users_max: Flask, + user_1: User, + user_2: User, + user_3: User, + ) -> None: + username = random_string() + email = random_email() + user_manager_service = UserManagerService(username=username) + + new_user, user_password = user_manager_service.create(email) + + assert new_user + assert new_user.username == username + assert new_user.email == email + assert bcrypt.check_password_hash(new_user.password, user_password) + + def test_created_user_is_inactive(self, app: Flask) -> None: + username = random_string() + user_manager_service = UserManagerService(username=username) + + new_user, _ = user_manager_service.create(email=random_email()) + + assert new_user + assert new_user.is_active is False + assert new_user.confirmation_token is not None + + def test_created_user_has_no_admin_rights(self, app: Flask) -> None: + username = random_string() + user_manager_service = UserManagerService(username=username) + + new_user, _ = user_manager_service.create(email=random_email()) + + assert new_user + assert new_user.admin is False + + def test_created_user_does_not_accept_privacy_policy( + self, app: Flask + ) -> None: + username = random_string() + user_manager_service = UserManagerService(username=username) + + new_user, _ = user_manager_service.create(email=random_email()) + + assert new_user + assert new_user.accepted_policy_date is None + + def test_created_user_timezone_is_europe_paris(self, app: Flask) -> None: + username = random_string() + user_manager_service = UserManagerService(username=username) + + new_user, _ = user_manager_service.create(email=random_email()) + + assert new_user + assert new_user.timezone == 'Europe/Paris' + + def test_created_user_date_format_is_MM_dd_yyyy( # noqa + self, app: Flask + ) -> None: + username = random_string() + user_manager_service = UserManagerService(username=username) + + new_user, _ = user_manager_service.create(email=random_email()) + + assert new_user + assert new_user.date_format == 'MM/dd/yyyy' + + def test_created_user_language_is_en(self, app: Flask) -> None: + username = random_string() + user_manager_service = UserManagerService(username=username) + + new_user, _ = user_manager_service.create(email=random_email()) + + assert new_user + assert new_user.language == 'en' + + class TestIsValidEmail: @pytest.mark.parametrize( ('input_email',), diff --git a/fittrackee/users/exceptions.py b/fittrackee/users/exceptions.py index 33733758..2b3d291a 100644 --- a/fittrackee/users/exceptions.py +++ b/fittrackee/users/exceptions.py @@ -2,5 +2,13 @@ class InvalidEmailException(Exception): ... +class UserControlsException(Exception): + ... + + +class UserCreationException(Exception): + ... + + class UserNotFoundException(Exception): ... diff --git a/fittrackee/users/utils/admin.py b/fittrackee/users/utils/admin.py index 0d990ea7..8357e8af 100644 --- a/fittrackee/users/utils/admin.py +++ b/fittrackee/users/utils/admin.py @@ -1,11 +1,18 @@ import secrets from typing import Optional, Tuple +from sqlalchemy import func + from fittrackee import db -from ..exceptions import InvalidEmailException, UserNotFoundException +from ..exceptions import ( + InvalidEmailException, + UserControlsException, + UserCreationException, + UserNotFoundException, +) from ..models import User -from ..utils.controls import is_valid_email +from ..utils.controls import is_valid_email, register_controls class UserManagerService: @@ -80,3 +87,62 @@ class UserManagerService: db.session.commit() return user, user_updated, new_password + + def create_user( + self, + email: str, + password: Optional[str] = None, + check_email: bool = False, + ) -> Tuple[Optional[User], Optional[str]]: + if not password: + password = secrets.token_urlsafe(30) + + ret = register_controls(self.username, email, password) + + if ret != '': + raise UserControlsException(ret) + + user = User.query.filter( + func.lower(User.username) == func.lower(self.username) + ).first() + if user: + raise UserCreationException( + 'sorry, that username is already taken' + ) + + # if a user exists with same email address, no error is returned + # since a user has to confirm his email to activate his account + user = User.query.filter( + func.lower(User.email) == func.lower(email) + ).first() + if user: + if check_email: + raise UserCreationException( + 'This user already exists. No action done.' + ) + return None, None + + new_user = User(username=self.username, email=email, password=password) + new_user.timezone = 'Europe/Paris' + new_user.date_format = 'MM/dd/yyyy' + new_user.confirmation_token = secrets.token_urlsafe(30) + db.session.add(new_user) + db.session.flush() + + return new_user, password + + def create( + self, + email: str, + password: Optional[str] = None, + ) -> Tuple[Optional[User], Optional[str]]: + try: + new_user, password = self.create_user( + email, password, check_email=True + ) + if new_user: + new_user.language = 'en' + db.session.commit() + except UserControlsException as e: + raise UserCreationException(str(e)) + return new_user, password