import json from datetime import datetime, timedelta from io import BytesIO from unittest.mock import MagicMock, Mock, patch import pytest from flask import Flask from freezegun import freeze_time from fittrackee.users.models import User, UserSportPreference from fittrackee.users.utils.token import get_user_token from fittrackee.workouts.models import Sport from ..mixins import ApiTestCaseMixin from ..utils import jsonify_dict USER_AGENT = ( 'Mozilla/5.0 (X11; Linux x86_64; rv:98.0) Gecko/20100101 Firefox/98.0' ) class TestUserRegistration(ApiTestCaseMixin): def test_it_returns_error_if_payload_is_empty(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps(dict()), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_if_username_is_missing(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( email=self.random_email(), password=self.random_string(), ) ), content_type='application/json', ) self.assert_400(response) @pytest.mark.parametrize( 'input_username_length', [1, 31], ) def test_it_returns_error_if_username_length_is_invalid( self, app: Flask, input_username_length: int ) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(length=input_username_length), email=self.random_email(), password=self.random_string(), ) ), content_type='application/json', ) self.assert_400(response, 'username: 3 to 30 characters required\n') @pytest.mark.parametrize( 'input_description,input_username', [ ('account_handle', '@sam@example.com'), ('with special characters', 'sam*'), ], ) def test_it_returns_error_if_username_is_invalid( self, app: Flask, input_description: str, input_username: str ) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=input_username, email=self.random_email(), password=self.random_email(), ) ), content_type='application/json', ) self.assert_400( response, 'username: only alphanumeric characters and ' 'the underscore character "_" allowed\n', ) @pytest.mark.parametrize( 'text_transformation', ['upper', 'lower'], ) def test_it_returns_error_if_user_already_exists_with_same_username( self, app: Flask, user_1: User, text_transformation: str ) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=( user_1.username.upper() if text_transformation == 'upper' else user_1.username.lower() ), email=self.random_email(), password=self.random_string(), ) ), content_type='application/json', ) self.assert_400(response, 'sorry, that username is already taken') def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_email(), ) ), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_if_password_is_too_short( self, app: Flask ) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_email(), password=self.random_string(length=7), ) ), content_type='application/json', ) self.assert_400(response, 'password: 8 characters required\n') def test_it_returns_error_if_email_is_missing(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), password=self.random_string(), ) ), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_if_email_is_invalid(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_string(), password=self.random_string(), ) ), content_type='application/json', ) self.assert_400(response, 'email: valid email must be provided\n') def test_it_does_not_send_email_after_error( self, app: Flask, account_confirmation_email_mock: Mock ) -> None: client = app.test_client() client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_string(), ) ), content_type='application/json', ) account_confirmation_email_mock.send.assert_not_called() def test_it_returns_success_if_payload_is_valid(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_email(), password=self.random_string(), ) ), content_type='application/json', ) assert response.status_code == 200 assert response.content_type == 'application/json' data = json.loads(response.data.decode()) assert data['status'] == 'success' assert 'auth_token' not in data def test_it_creates_user_with_inactive_account(self, app: Flask) -> None: client = app.test_client() username = self.random_string() email = self.random_email() client.post( '/api/auth/register', data=json.dumps( dict( username=username, email=email, password=self.random_string(), ) ), content_type='application/json', ) new_user = User.query.filter_by(username=username).first() assert new_user.email == email assert new_user.password is not None assert new_user.is_active is False def test_it_calls_account_confirmation_email_if_payload_is_valid( self, app: Flask, account_confirmation_email_mock: Mock ) -> None: client = app.test_client() email = self.random_email() username = self.random_string() expected_token = self.random_string() with patch('secrets.token_urlsafe', return_value=expected_token): client.post( '/api/auth/register', data=json.dumps( dict( username=username, email=email, password='12345678', ) ), content_type='application/json', environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) account_confirmation_email_mock.send.assert_called_once_with( { 'language': 'en', 'email': email, }, { 'username': username, 'fittrackee_url': 'http://0.0.0.0:5000', 'operating_system': 'Linux', 'browser_name': 'Firefox', 'account_confirmation_url': ( 'http://0.0.0.0:5000/account-confirmation' f'?token={expected_token}' ), }, ) def test_it_does_not_call_account_confirmation_email_when_email_sending_is_disabled( # noqa self, app_wo_email_activation: Flask, account_confirmation_email_mock: Mock, ) -> None: client = app_wo_email_activation.test_client() email = self.random_email() username = self.random_string() response = client.post( '/api/auth/register', data=json.dumps( dict( username=username, email=email, password='12345678', ) ), content_type='application/json', environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) assert response.status_code == 200 account_confirmation_email_mock.send.assert_not_called() @pytest.mark.parametrize( 'text_transformation', ['upper', 'lower'], ) def test_it_does_not_return_error_if_a_user_already_exists_with_same_email( self, app: Flask, user_1: User, text_transformation: str ) -> None: client = app.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=( user_1.email.upper() if text_transformation == 'upper' else user_1.email.lower() ), password=self.random_string(), ) ), content_type='application/json', ) assert response.status_code == 200 assert response.content_type == 'application/json' data = json.loads(response.data.decode()) assert data['status'] == 'success' assert 'auth_token' not in data def test_it_does_not_call_account_confirmation_email_if_user_already_exists( # noqa self, app: Flask, user_1: User, account_confirmation_email_mock: Mock ) -> None: client = app.test_client() client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=user_1.email, password=self.random_string(), ) ), content_type='application/json', ) account_confirmation_email_mock.send.assert_not_called() class TestUserLogin(ApiTestCaseMixin): def test_it_returns_error_if_payload_is_empty(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/login', data=json.dumps(dict()), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_if_user_does_not_exists( self, app: Flask ) -> None: client = app.test_client() response = client.post( '/api/auth/login', data=json.dumps( dict(email=self.random_email(), password=self.random_string()) ), content_type='application/json', ) self.assert_401(response, 'invalid credentials') def test_it_returns_error_if_user_account_is_inactive( self, app: Flask, inactive_user: User ) -> None: client = app.test_client() response = client.post( '/api/auth/login', data=json.dumps( dict(email=inactive_user.email, password='12345678') ), content_type='application/json', ) self.assert_401(response, 'invalid credentials') def test_it_returns_error_if_password_is_invalid( self, app: Flask, user_1: User ) -> None: client = app.test_client() response = client.post( '/api/auth/login', data=json.dumps( dict(email=user_1.email, password=self.random_email()) ), content_type='application/json', ) self.assert_401(response, 'invalid credentials') @pytest.mark.parametrize( 'text_transformation', ['upper', 'lower'], ) def test_user_can_login_regardless_username_case( self, app: Flask, user_1: User, text_transformation: str ) -> None: client = app.test_client() response = client.post( '/api/auth/login', data=json.dumps( dict( email=( user_1.email.upper() if text_transformation == 'upper' else user_1.email.lower() ), password='12345678', ) ), content_type='application/json', ) assert response.status_code == 200 assert response.content_type == 'application/json' data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'successfully logged in' assert data['auth_token'] class TestUserProfile(ApiTestCaseMixin): def test_it_returns_error_if_auth_token_is_missing( self, app: Flask ) -> None: client = app.test_client() response = client.get('/api/auth/profile') self.assert_401(response, 'provide a valid auth token') def test_it_returns_error_if_auth_token_is_invalid( self, app: Flask ) -> None: client = app.test_client() response = client.get( '/api/auth/profile', headers=dict(Authorization='Bearer invalid') ) self.assert_invalid_token(response) def test_it_returns_user(self, app: Flask, user_1: User) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.get( '/api/auth/profile', headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['data'] == jsonify_dict(user_1.serialize(user_1)) @pytest.mark.parametrize( 'client_scope, can_access', [ ('application:write', False), ('profile:read', True), ('profile:write', False), ('users:read', False), ('users:write', False), ('workouts:read', False), ('workouts:write', False), ], ) def test_expected_scopes_are_defined( self, app: Flask, user_1: User, client_scope: str, can_access: bool ) -> None: ( client, oauth_client, access_token, _, ) = self.create_oauth2_client_and_issue_token( app, user_1, scope=client_scope ) response = client.get( '/api/auth/profile', content_type='application/json', headers=dict(Authorization=f'Bearer {access_token}'), ) self.assert_response_scope(response, can_access) class TestUserProfileUpdate(ApiTestCaseMixin): def test_it_returns_error_if_payload_is_empty( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit', content_type='application/json', data=json.dumps(dict()), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response) def test_it_returns_error_if_fields_are_missing( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit', content_type='application/json', data=json.dumps(dict(first_name=self.random_string())), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response) def test_it_updates_user_profile(self, app: Flask, user_1: User) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) first_name = self.random_string() last_name = self.random_string() location = self.random_string() bio = self.random_string() birth_date = '1980-01-01' response = client.post( '/api/auth/profile/edit', content_type='application/json', data=json.dumps( dict( first_name=first_name, last_name=last_name, location=location, bio=bio, birth_date=birth_date, ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user profile updated' assert data['data'] == jsonify_dict(user_1.serialize(user_1)) @pytest.mark.parametrize( 'client_scope, can_access', [ ('application:write', False), ('profile:read', False), ('profile:write', True), ('users:read', False), ('users:write', False), ('workouts:read', False), ('workouts:write', False), ], ) def test_expected_scopes_are_defined( self, app: Flask, user_1: User, client_scope: str, can_access: bool, ) -> None: ( client, oauth_client, access_token, _, ) = self.create_oauth2_client_and_issue_token( app, user_1, scope=client_scope ) response = client.post( '/api/auth/profile/edit', content_type='application/json', headers=dict(Authorization=f'Bearer {access_token}'), ) self.assert_response_scope(response, can_access) class TestUserAccountUpdate(ApiTestCaseMixin): @staticmethod def assert_no_emails_sent( email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: email_updated_to_current_address_mock.send.assert_not_called() email_updated_to_new_address_mock.send.assert_not_called() password_change_email_mock.send.assert_not_called() def test_it_returns_error_if_payload_is_empty( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps(dict()), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response) def test_it_returns_error_if_current_password_is_missing( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response, error_message='current password is missing') def test_it_returns_error_if_email_is_missing( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( password='12345678', new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response, 'email is missing') def test_it_returns_error_if_current_password_is_invalid( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, password=self.random_string(), new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_401(response, error_message='invalid credentials') def test_it_does_not_send_emails_when_error_occurs( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, password=self.random_string(), new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_no_emails_sent( email_updated_to_current_address_mock, email_updated_to_new_address_mock, password_change_email_mock, ) def test_it_does_not_returns_error_if_no_new_password_provided( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, password='12345678', ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user account updated' def test_it_does_not_send_emails_if_no_change( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, password='12345678', ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_no_emails_sent( email_updated_to_current_address_mock, email_updated_to_new_address_mock, password_change_email_mock, ) def test_it_returns_error_if_new_email_is_invalid( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=self.random_string(), password='12345678', ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response, 'email: valid email must be provided\n') def test_it_only_updates_email_to_confirm_if_new_email_provided( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) current_email = user_1.email new_email = 'new.email@example.com' response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=new_email, password='12345678', ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 200 assert current_email == user_1.email assert new_email == user_1.email_to_confirm assert user_1.confirmation_token is not None def test_it_updates_email_when_email_sending_is_disabled( self, app_wo_email_activation: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app_wo_email_activation, user_1.email ) new_email = 'new.email@example.com' response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=new_email, password='12345678', ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 200 assert user_1.email == new_email assert user_1.email_to_confirm is None assert user_1.confirmation_token is None def test_it_calls_email_updated_to_current_email_send_when_new_email_provided( # noqa self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) new_email = 'new.email@example.com' client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=new_email, password='12345678', ) ), headers=dict(Authorization=f'Bearer {auth_token}'), environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) email_updated_to_current_address_mock.send.assert_called_once_with( { 'language': 'en', 'email': user_1.email, }, { 'username': user_1.username, 'fittrackee_url': 'http://0.0.0.0:5000', 'operating_system': 'Linux', 'browser_name': 'Firefox', 'new_email_address': new_email, }, ) def test_it_calls_email_updated_to_new_email_send_when_new_email_provided( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) new_email = 'new.email@example.com' expected_token = self.random_string() with patch('secrets.token_urlsafe', return_value=expected_token): client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=new_email, password='12345678', ) ), headers=dict(Authorization=f'Bearer {auth_token}'), environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) email_updated_to_new_address_mock.send.assert_called_once_with( { 'language': 'en', 'email': user_1.email_to_confirm, }, { 'username': user_1.username, 'fittrackee_url': 'http://0.0.0.0:5000', 'operating_system': 'Linux', 'browser_name': 'Firefox', 'email_confirmation_url': ( f'http://0.0.0.0:5000/email-update?token={expected_token}' ), }, ) def test_it_does_not_calls_password_change_email_send_when_new_email_provided( # noqa self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) new_email = 'new.email@example.com' expected_token = self.random_string() with patch('secrets.token_urlsafe', return_value=expected_token): client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=new_email, password='12345678', ) ), headers=dict(Authorization=f'Bearer {auth_token}'), environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) password_change_email_mock.send.assert_not_called() def test_it_returns_error_if_controls_fail_on_new_password( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, password='12345678', new_password=self.random_string(length=3), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response, 'password: 8 characters required') def test_it_updates_auth_user_password_when_new_password_provided( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) current_hashed_password = user_1.password response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, password='12345678', new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user account updated' assert current_hashed_password != user_1.password def test_new_password_is_hashed( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) new_password = self.random_string() response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, password='12345678', new_password=new_password, ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 200 assert new_password != user_1.password def test_it_calls_password_change_email_when_new_password_provided( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, password='12345678', new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) password_change_email_mock.send.assert_called_once_with( { 'language': 'en', 'email': user_1.email, }, { 'username': user_1.username, 'fittrackee_url': 'http://0.0.0.0:5000', 'operating_system': 'Linux', 'browser_name': 'Firefox', }, ) def test_it_does_not_call_email_updated_emails_send_when_new_password_provided( # noqa self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=user_1.email, password='12345678', new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) email_updated_to_current_address_mock.send.assert_not_called() email_updated_to_new_address_mock.send.assert_not_called() def test_it_updates_email_to_confirm_and_password_when_new_email_and_password_provided( # noqa self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) current_email = user_1.email current_hashed_password = user_1.password new_email = 'new.email@example.com' response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email=new_email, password='12345678', new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user account updated' assert user_1.email == current_email assert user_1.email_to_confirm == new_email assert user_1.password != current_hashed_password def test_it_calls_all_email_send_when_new_email_and_password_provided( self, app: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email='new.email@example.com', password='12345678', new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) email_updated_to_current_address_mock.send.assert_called_once() email_updated_to_new_address_mock.send.assert_called_once() password_change_email_mock.send.assert_called_once() def test_it_does_not_calls_all_email_send_when_email_sending_is_disabled( self, app_wo_email_activation: Flask, user_1: User, email_updated_to_current_address_mock: MagicMock, email_updated_to_new_address_mock: MagicMock, password_change_email_mock: MagicMock, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app_wo_email_activation, user_1.email ) client.patch( '/api/auth/profile/edit/account', content_type='application/json', data=json.dumps( dict( email='new.email@example.com', password='12345678', new_password=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_no_emails_sent( email_updated_to_current_address_mock, email_updated_to_new_address_mock, password_change_email_mock, ) @pytest.mark.parametrize( 'client_scope, can_access', [ ('application:write', False), ('profile:read', False), ('profile:write', True), ('users:read', False), ('users:write', False), ('workouts:read', False), ('workouts:write', False), ], ) def test_expected_scopes_are_defined( self, app: Flask, user_1: User, client_scope: str, can_access: bool, ) -> None: ( client, oauth_client, access_token, _, ) = self.create_oauth2_client_and_issue_token( app, user_1, scope=client_scope ) response = client.patch( '/api/auth/profile/edit/account', content_type='application/json', headers=dict(Authorization=f'Bearer {access_token}'), ) self.assert_response_scope(response, can_access) class TestUserPreferencesUpdate(ApiTestCaseMixin): def test_it_returns_error_if_payload_is_empty( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/preferences', content_type='application/json', data=json.dumps(dict()), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response) def test_it_returns_error_if_fields_are_missing( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/preferences', content_type='application/json', data=json.dumps(dict(weekm=True)), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response) def test_it_updates_user_preferences( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/preferences', content_type='application/json', data=json.dumps( dict( timezone='America/New_York', weekm=True, language='fr', imperial_units=True, ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user preferences updated' assert data['data'] == jsonify_dict(user_1.serialize(user_1)) @pytest.mark.parametrize( 'client_scope, can_access', [ ('application:write', False), ('profile:read', False), ('profile:write', True), ('users:read', False), ('users:write', False), ('workouts:read', False), ('workouts:write', False), ], ) def test_expected_scopes_are_defined( self, app: Flask, user_1: User, client_scope: str, can_access: bool, ) -> None: ( client, oauth_client, access_token, _, ) = self.create_oauth2_client_and_issue_token( app, user_1, scope=client_scope ) response = client.post( '/api/auth/profile/edit/preferences', content_type='application/json', headers=dict(Authorization=f'Bearer {access_token}'), ) self.assert_response_scope(response, can_access) class TestUserSportPreferencesUpdate(ApiTestCaseMixin): def test_it_returns_error_if_payload_is_empty( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/sports', content_type='application/json', data=json.dumps(dict()), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response) def test_it_returns_error_if_sport_id_is_missing( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/sports', content_type='application/json', data=json.dumps(dict(is_active=True)), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response) def test_it_returns_error_if_sport_not_found( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/sports', content_type='application/json', data=json.dumps(dict(sport_id=1, is_active=True)), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_404_with_entity(response, 'sport') def test_it_returns_error_if_payload_contains_only_sport_id( self, app: Flask, user_1: User, sport_1_cycling: Sport ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/sports', content_type='application/json', data=json.dumps(dict(sport_id=1)), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response) def test_it_returns_error_if_color_is_invalid( self, app: Flask, user_1: User, sport_1_cycling: Sport ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/sports', content_type='application/json', data=json.dumps( dict( sport_id=sport_1_cycling.id, color=self.random_string(), ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_400(response, 'invalid hexadecimal color') @pytest.mark.parametrize( 'input_color', ['#000000', '#FFF'], ) def test_it_updates_sport_color_for_auth_user( self, app: Flask, user_1: User, sport_2_running: Sport, input_color: str, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/sports', content_type='application/json', data=json.dumps( dict( sport_id=sport_2_running.id, color=input_color, ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user sport preferences updated' assert response.status_code == 200 assert data['data']['user_id'] == user_1.id assert data['data']['sport_id'] == sport_2_running.id assert data['data']['color'] == input_color assert data['data']['is_active'] is True assert data['data']['stopped_speed_threshold'] == 0.1 def test_it_disables_sport_for_auth_user( self, app: Flask, user_1: User, sport_1_cycling: Sport ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/sports', content_type='application/json', data=json.dumps( dict( sport_id=sport_1_cycling.id, is_active=False, ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user sport preferences updated' assert response.status_code == 200 assert data['data']['user_id'] == user_1.id assert data['data']['sport_id'] == sport_1_cycling.id assert data['data']['color'] is None assert data['data']['is_active'] is False assert data['data']['stopped_speed_threshold'] == 1 def test_it_updates_stopped_speed_threshold_for_auth_user( self, app: Flask, user_1: User, sport_1_cycling: Sport ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/profile/edit/sports', content_type='application/json', data=json.dumps( dict( sport_id=sport_1_cycling.id, stopped_speed_threshold=0.5, ) ), headers=dict(Authorization=f'Bearer {auth_token}'), ) data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user sport preferences updated' assert response.status_code == 200 assert data['data']['user_id'] == user_1.id assert data['data']['sport_id'] == sport_1_cycling.id assert data['data']['color'] is None assert data['data']['is_active'] assert data['data']['stopped_speed_threshold'] == 0.5 @pytest.mark.parametrize( 'client_scope, can_access', [ ('application:write', False), ('profile:read', False), ('profile:write', True), ('users:read', False), ('users:write', False), ('workouts:read', False), ('workouts:write', False), ], ) def test_expected_scopes_are_defined( self, app: Flask, user_1: User, client_scope: str, can_access: bool, ) -> None: ( client, oauth_client, access_token, _, ) = self.create_oauth2_client_and_issue_token( app, user_1, scope=client_scope ) response = client.post( '/api/auth/profile/edit/sports', content_type='application/json', headers=dict(Authorization=f'Bearer {access_token}'), ) self.assert_response_scope(response, can_access) class TestUserSportPreferencesReset(ApiTestCaseMixin): def test_it_returns_error_if_sport_does_not_exist( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.delete( '/api/auth/profile/reset/sports/1', headers=dict(Authorization=f'Bearer {auth_token}'), ) self.assert_404_with_entity(response, 'sport') def test_it_resets_sport_preferences( self, app: Flask, user_1: User, sport_1_cycling: Sport, user_sport_1_preference: UserSportPreference, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.delete( f'/api/auth/profile/reset/sports/{sport_1_cycling.id}', headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 204 assert ( UserSportPreference.query.filter_by( user_id=user_1.id, sport_id=sport_1_cycling.id, ).first() is None ) def test_it_does_not_raise_error_if_sport_preferences_do_not_exist( self, app: Flask, user_1: User, sport_1_cycling: Sport ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.delete( f'/api/auth/profile/reset/sports/{sport_1_cycling.id}', headers=dict(Authorization=f'Bearer {auth_token}'), ) assert response.status_code == 204 @pytest.mark.parametrize( 'client_scope, can_access', [ ('application:write', False), ('profile:read', False), ('profile:write', True), ('users:read', False), ('users:write', False), ('workouts:read', False), ('workouts:write', False), ], ) def test_expected_scopes_are_defined( self, app: Flask, user_1: User, client_scope: str, can_access: bool, sport_1_cycling: Sport, user_sport_1_preference: UserSportPreference, ) -> None: ( client, oauth_client, access_token, _, ) = self.create_oauth2_client_and_issue_token( app, user_1, scope=client_scope ) response = client.delete( f'/api/auth/profile/reset/sports/{sport_1_cycling.id}', content_type='application/json', headers=dict(Authorization=f'Bearer {access_token}'), ) self.assert_response_scope(response, can_access) class TestUserPicture(ApiTestCaseMixin): def test_it_returns_error_if_file_is_missing( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/picture', headers=dict( content_type='multipart/form-data', Authorization=f'Bearer {auth_token}', ), ) self.assert_400(response, 'no file part', 'fail') def test_it_returns_error_if_file_is_invalid( self, app: Flask, user_1: User ) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/picture', data=dict(file=(BytesIO(b'avatar'), 'avatar.bmp')), headers=dict( content_type='multipart/form-data', Authorization=f'Bearer {auth_token}', ), ) self.assert_400(response, 'file extension not allowed', 'fail') def test_it_returns_error_if_image_size_exceeds_file_limit( self, app_with_max_file_size: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app_with_max_file_size, user_1.email ) response = client.post( '/api/auth/picture', data=dict( file=(BytesIO(b'test_file_for_avatar' * 50), 'avatar.jpg') ), headers=dict( content_type='multipart/form-data', Authorization=f'Bearer {auth_token}', ), ) data = self.assert_413( response, 'Error during picture upload, file size (1.2KB) exceeds 1.0KB.', ) assert 'data' not in data def test_it_returns_error_if_image_size_exceeds_archive_limit( self, app_with_max_zip_file_size: Flask, user_1: User, sport_1_cycling: Sport, gpx_file: str, ) -> None: client, auth_token = self.get_test_client_and_auth_token( app_with_max_zip_file_size, user_1.email ) response = client.post( '/api/auth/picture', data=dict( file=(BytesIO(b'test_file_for_avatar' * 50), 'avatar.jpg') ), headers=dict( content_type='multipart/form-data', Authorization=f'Bearer {auth_token}', ), ) data = self.assert_413( response, 'Error during picture upload, file size (1.2KB) exceeds 1.0KB.', ) assert 'data' not in data def test_it_updates_user_picture(self, app: Flask, user_1: User) -> None: client, auth_token = self.get_test_client_and_auth_token( app, user_1.email ) response = client.post( '/api/auth/picture', data=dict(file=(BytesIO(b'avatar'), 'avatar.png')), headers=dict( content_type='multipart/form-data', Authorization=f'Bearer {auth_token}', ), ) data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user picture updated' assert response.status_code == 200 assert 'avatar.png' in user_1.picture response = client.post( '/api/auth/picture', data=dict(file=(BytesIO(b'avatar2'), 'avatar2.png')), headers=dict( content_type='multipart/form-data', Authorization=f'Bearer {auth_token}', ), ) data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'user picture updated' assert response.status_code == 200 assert 'avatar.png' not in user_1.picture assert 'avatar2.png' in user_1.picture @pytest.mark.parametrize( 'client_scope, can_access', [ ('application:write', False), ('profile:read', False), ('profile:write', True), ('users:read', False), ('users:write', False), ('workouts:read', False), ('workouts:write', False), ], ) def test_expected_scopes_are_defined( self, app: Flask, user_1: User, client_scope: str, can_access: bool, ) -> None: ( client, oauth_client, access_token, _, ) = self.create_oauth2_client_and_issue_token( app, user_1, scope=client_scope ) response = client.post( '/api/auth/picture', content_type='application/json', headers=dict(Authorization=f'Bearer {access_token}'), ) self.assert_response_scope(response, can_access) class TestRegistrationConfiguration(ApiTestCaseMixin): def test_it_returns_error_if_it_exceeds_max_users( self, app_with_3_users_max: Flask, user_1_admin: User, user_2: User, user_3: User, ) -> None: client = app_with_3_users_max.test_client() response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_email(), password=self.random_string(), ) ), content_type='application/json', ) self.assert_403(response, 'error, registration is disabled') def test_it_disables_registration_on_user_registration( self, app_with_3_users_max: Flask, user_1_admin: User, user_2: User, ) -> None: client = app_with_3_users_max.test_client() client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_email(), password=self.random_string(), ) ), content_type='application/json', ) response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_email(), password=self.random_string(), ) ), content_type='application/json', ) self.assert_403(response, 'error, registration is disabled') def test_it_does_not_disable_registration_if_users_count_below_limit( self, app_with_3_users_max: Flask, user_1: User, ) -> None: client = app_with_3_users_max.test_client() client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_email(), password=self.random_string(), ) ), content_type='application/json', ) response = client.post( '/api/auth/register', data=json.dumps( dict( username=self.random_string(), email=self.random_email(), password=self.random_string(), ) ), content_type='application/json', ) assert response.status_code == 200 class TestPasswordResetRequest(ApiTestCaseMixin): def test_it_returns_error_on_empty_payload(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/password/reset-request', data=json.dumps(dict()), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_on_invalid_payload(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/password/reset-request', data=json.dumps(dict(username=self.random_string())), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_when_email_sending_is_disabled( self, app_wo_email_activation: Flask ) -> None: client = app_wo_email_activation.test_client() response = client.post( '/api/auth/password/reset-request', data=json.dumps(dict(email='test@test.com')), content_type='application/json', ) self.assert_404_with_message( response, 'the requested URL was not found on the server' ) def test_it_requests_password_reset_when_user_exists( self, app: Flask, user_1: User, user_reset_password_email: Mock ) -> None: client = app.test_client() response = client.post( '/api/auth/password/reset-request', data=json.dumps(dict(email='test@test.com')), content_type='application/json', ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'password reset request processed' def test_it_calls_reset_password_email_when_user_exists( self, app: Flask, user_1: User, reset_password_email: Mock ) -> None: client = app.test_client() token = self.random_string() with patch('jwt.encode', return_value=token): client.post( '/api/auth/password/reset-request', data=json.dumps(dict(email='test@test.com')), content_type='application/json', environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) reset_password_email.send.assert_called_once_with( { 'language': 'en', 'email': user_1.email, }, { 'expiration_delay': '3 seconds', 'username': user_1.username, 'password_reset_url': ( f'http://0.0.0.0:5000/password-reset?token={token}' ), 'fittrackee_url': 'http://0.0.0.0:5000', 'operating_system': 'Linux', 'browser_name': 'Firefox', }, ) def test_it_does_not_return_error_when_user_does_not_exist( self, app: Flask ) -> None: client = app.test_client() response = client.post( '/api/auth/password/reset-request', data=json.dumps(dict(email='test@test.com')), content_type='application/json', ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'password reset request processed' def test_it_does_not_call_reset_password_email_when_user_does_not_exist( self, app: Flask, reset_password_email: Mock ) -> None: client = app.test_client() client.post( '/api/auth/password/reset-request', data=json.dumps(dict(email='test@test.com')), content_type='application/json', ) reset_password_email.assert_not_called() class TestPasswordUpdate(ApiTestCaseMixin): def test_it_returns_error_if_payload_is_empty(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/password/update', data=json.dumps(dict()), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/password/update', data=json.dumps( dict( password=self.random_string(), ) ), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_if_password_is_missing(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/password/update', data=json.dumps( dict( token=self.random_string(), ) ), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_if_token_is_invalid(self, app: Flask) -> None: token = get_user_token(1) client = app.test_client() response = client.post( '/api/auth/password/update', data=json.dumps( dict( token=token, password=self.random_string(), ) ), content_type='application/json', ) self.assert_401(response, 'invalid token, please request a new token') def test_it_returns_error_if_token_is_expired( self, app: Flask, user_1: User ) -> None: now = datetime.utcnow() token = get_user_token(user_1.id, password_reset=True) client = app.test_client() with freeze_time(now + timedelta(seconds=4)): response = client.post( '/api/auth/password/update', data=json.dumps( dict( token=token, password=self.random_string(), ) ), content_type='application/json', ) self.assert_401( response, 'invalid token, please request a new token' ) def test_it_returns_error_if_password_is_invalid( self, app: Flask, user_1: User ) -> None: token = get_user_token(user_1.id, password_reset=True) client = app.test_client() response = client.post( '/api/auth/password/update', data=json.dumps( dict( token=token, password=self.random_string(length=7), ) ), content_type='application/json', ) self.assert_400(response, 'password: 8 characters required\n') def test_it_does_not_send_email_after_error( self, app: Flask, user_1: User, password_change_email_mock: MagicMock, ) -> None: token = get_user_token(user_1.id, password_reset=True) client = app.test_client() client.post( '/api/auth/password/update', data=json.dumps( dict( token=token, password=self.random_string(length=7), ) ), content_type='application/json', ) password_change_email_mock.assert_not_called() def test_it_updates_password( self, app: Flask, user_1: User, password_change_email_mock: MagicMock, ) -> None: token = get_user_token(user_1.id, password_reset=True) client = app.test_client() response = client.post( '/api/auth/password/update', data=json.dumps( dict( token=token, password=self.random_string(), ) ), content_type='application/json', ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'password updated' def test_it_sends_email_after_successful_update( self, app: Flask, user_1: User, password_change_email_mock: MagicMock, ) -> None: token = get_user_token(user_1.id, password_reset=True) client = app.test_client() response = client.post( '/api/auth/password/update', data=json.dumps( dict( token=token, password=self.random_string(), ) ), content_type='application/json', environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) assert response.status_code == 200 password_change_email_mock.send.assert_called_once_with( { 'language': 'en', 'email': user_1.email, }, { 'username': user_1.username, 'fittrackee_url': 'http://0.0.0.0:5000', 'operating_system': 'Linux', 'browser_name': 'Firefox', }, ) def test_it_does_not_send_email_when_email_sending_is_disabled( self, app_wo_email_activation: Flask, user_1: User, password_change_email_mock: MagicMock, ) -> None: token = get_user_token(user_1.id, password_reset=True) client = app_wo_email_activation.test_client() client.post( '/api/auth/password/update', data=json.dumps( dict( token=token, password=self.random_string(), ) ), content_type='application/json', environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) password_change_email_mock.send.assert_not_called() class TestEmailUpdateWitUnauthenticatedUser(ApiTestCaseMixin): def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/email/update', data=json.dumps(dict()), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_if_token_is_invalid(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/email/update', data=json.dumps(dict(token=self.random_string())), content_type='application/json', ) self.assert_400(response) def test_it_does_not_update_email_if_token_mismatches( self, app: Flask, user_1: User ) -> None: user_1.confirmation_token = self.random_string() new_email = 'new.email@example.com' user_1.email_to_confirm = new_email client = app.test_client() response = client.post( '/api/auth/email/update', data=json.dumps(dict(token=self.random_string())), content_type='application/json', ) self.assert_400(response) def test_it_updates_email(self, app: Flask, user_1: User) -> None: token = self.random_string() user_1.confirmation_token = token new_email = 'new.email@example.com' user_1.email_to_confirm = new_email client = app.test_client() response = client.post( '/api/auth/email/update', data=json.dumps(dict(token=token)), content_type='application/json', ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'email updated' assert user_1.email == new_email assert user_1.email_to_confirm is None assert user_1.confirmation_token is None class TestConfirmationAccount(ApiTestCaseMixin): def test_it_returns_error_if_token_is_missing(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/account/confirm', data=json.dumps(dict()), content_type='application/json', ) self.assert_400(response) def test_it_returns_error_if_token_is_invalid(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/account/confirm', data=json.dumps(dict(token=self.random_string())), content_type='application/json', ) self.assert_400(response) def test_it_activates_user_account( self, app: Flask, inactive_user: User ) -> None: token = self.random_string() inactive_user.confirmation_token = token client = app.test_client() response = client.post( '/api/auth/account/confirm', data=json.dumps(dict(token=token)), content_type='application/json', ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'account confirmation successful' assert inactive_user.is_active is True assert inactive_user.confirmation_token is None class TestResendAccountConfirmationEmail(ApiTestCaseMixin): def test_it_returns_error_if_email_is_missing(self, app: Flask) -> None: client = app.test_client() response = client.post( '/api/auth/account/resend-confirmation', data=json.dumps(dict()), content_type='application/json', ) self.assert_400(response) def test_it_does_not_return_error_if_account_does_not_exist( self, app: Flask ) -> None: client = app.test_client() response = client.post( '/api/auth/account/resend-confirmation', data=json.dumps(dict(email=self.random_email())), content_type='application/json', ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'confirmation email resent' def test_it_does_not_return_error_if_account_already_active( self, app: Flask, user_1: User ) -> None: client = app.test_client() response = client.post( '/api/auth/account/resend-confirmation', data=json.dumps(dict(email=user_1.email)), content_type='application/json', ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'confirmation email resent' def test_it_does_not_call_account_confirmation_email_if_user_is_active( self, app: Flask, user_1: User, account_confirmation_email_mock: Mock, ) -> None: client = app.test_client() client.post( '/api/auth/account/resend-confirmation', data=json.dumps(dict(email=user_1.email)), content_type='application/json', environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) account_confirmation_email_mock.send.assert_not_called() def test_it_returns_success_if_user_is_inactive( self, app: Flask, inactive_user: User ) -> None: client = app.test_client() response = client.post( '/api/auth/account/resend-confirmation', data=json.dumps(dict(email=inactive_user.email)), content_type='application/json', ) assert response.status_code == 200 data = json.loads(response.data.decode()) assert data['status'] == 'success' assert data['message'] == 'confirmation email resent' def test_it_updates_token_if_user_is_inactive( self, app: Flask, inactive_user: User ) -> None: client = app.test_client() previous_token = inactive_user.confirmation_token client.post( '/api/auth/account/resend-confirmation', data=json.dumps(dict(email=inactive_user.email)), content_type='application/json', ) assert inactive_user.confirmation_token != previous_token def test_it_calls_account_confirmation_email_if_user_is_inactive( self, app: Flask, inactive_user: User, account_confirmation_email_mock: Mock, ) -> None: client = app.test_client() expected_token = self.random_string() with patch('secrets.token_urlsafe', return_value=expected_token): client.post( '/api/auth/account/resend-confirmation', data=json.dumps(dict(email=inactive_user.email)), content_type='application/json', environ_base={'HTTP_USER_AGENT': USER_AGENT}, ) account_confirmation_email_mock.send.assert_called_once_with( { 'language': 'en', 'email': inactive_user.email, }, { 'username': inactive_user.username, 'fittrackee_url': 'http://0.0.0.0:5000', 'operating_system': 'Linux', 'browser_name': 'Firefox', 'account_confirmation_url': ( 'http://0.0.0.0:5000/account-confirmation' f'?token={expected_token}' ), }, ) def test_it_returns_error_if_email_sending_is_disabled( self, app_wo_email_activation: Flask, inactive_user: User ) -> None: client = app_wo_email_activation.test_client() response = client.post( '/api/auth/account/resend-confirmation', data=json.dumps(dict(email=inactive_user.email)), content_type='application/json', ) self.assert_404_with_message( response, 'the requested URL was not found on the server' )