Skip to content

Commit

Permalink
fix: update profile request data validation (anitab-org#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
isabelcosta authored and Harish Gupta committed Mar 2, 2021
1 parent 5f2f4b2 commit eadfd3e
Show file tree
Hide file tree
Showing 11 changed files with 396 additions and 48 deletions.
9 changes: 7 additions & 2 deletions app/api/resources/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from flask_jwt_extended import jwt_required, create_access_token, get_jwt_identity
from flask_restplus import Resource, marshal, Namespace

from app.api.validations.user import (validate_user_registration_request_data, validate_resend_email_request_data,
validate_new_password)
from app.api.validations.user import *
from app.api.email_utils import send_email_verification_message
from app.api.models.user import *
from app.api.dao.user import UserDAO
Expand Down Expand Up @@ -91,6 +90,12 @@ def put(cls):
"""

data = request.json

is_valid = validate_update_profile_request_data(data)

if is_valid != {}:
return is_valid, 400

user_id = get_jwt_identity()
return DAO.update_user_profile(user_id, data)

Expand Down
122 changes: 103 additions & 19 deletions app/api/validations/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from app.utils.validation_utils import is_email_valid, is_username_valid
from app.utils.validation_utils import is_email_valid, is_username_valid, validate_length, get_stripped_string

# Field character limit

Expand All @@ -9,6 +9,15 @@
PASSWORD_MAX_LENGTH = 25
PASSWORD_MIN_LENGTH = 8

BIO_MAX_LENGTH = 450
LOCATION_MAX_LENGTH = 60
OCCUPATION_MAX_LENGTH = 60
ORGANIZATION_MAX_LENGTH = 60
SLACK_USERNAME_MAX_LENGTH = 60
SKILLS_MAX_LENGTH = 450
INTERESTS_MAX_LENGTH = 150
SOCIALS_MAX_LENGTH = 400


def validate_user_registration_request_data(data):
# Verify if request body has required fields
Expand All @@ -32,20 +41,17 @@ def validate_user_registration_request_data(data):
if not (isinstance(name, str) and isinstance(username, str) and isinstance(password, str)):
return {"message": "Name, username and password must be in string format."}

if not (NAME_MIN_LENGTH <= len(name) <= NAME_MAX_LENGTH):
return {"message": "The name field has to longer than {min_limit} "
"characters and shorter than {max_limit} characters.".format(min_limit=NAME_MIN_LENGTH-1,
max_limit=NAME_MAX_LENGTH+1)}
is_valid = validate_length(len(get_stripped_string(name)), NAME_MIN_LENGTH, NAME_MAX_LENGTH, 'name')
if not is_valid[0]:
return is_valid[1]

if not (USERNAME_MIN_LENGTH <= len(username) <= USERNAME_MAX_LENGTH):
return {"message": "The username field has to longer than {min_limit} "
"characters and shorter than {max_limit} characters.".format(min_limit=USERNAME_MIN_LENGTH-1,
max_limit=USERNAME_MAX_LENGTH+1)}
is_valid = validate_length(len(get_stripped_string(username)), USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, 'username')
if not is_valid[0]:
return is_valid[1]

if not (PASSWORD_MIN_LENGTH <= len(password) <= PASSWORD_MAX_LENGTH):
return {"message": "The password field has to longer than {min_limit} "
"characters and shorter than {max_limit} characters.".format(min_limit=PASSWORD_MIN_LENGTH-1,
max_limit=PASSWORD_MAX_LENGTH+1)}
is_valid = validate_length(len(get_stripped_string(password)), PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH, 'password')
if not is_valid[0]:
return is_valid[1]

# Verify business logic of request body
if terms_and_conditions_checked is False:
Expand All @@ -61,7 +67,6 @@ def validate_user_registration_request_data(data):


def validate_resend_email_request_data(data):

# Verify if request body has required fields
if 'email' not in data:
return {"message": "Email field is missing."}
Expand All @@ -73,16 +78,95 @@ def validate_resend_email_request_data(data):
return {}


def validate_update_profile_request_data(data):
# todo this does not check if non expected fields are being sent

if not data:
return {"message": "No data for updating profile was sent."}

username = data.get('username', None)
if username:
is_valid = validate_length(len(get_stripped_string(username)), USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH,
'username')
if not is_valid[0]:
return is_valid[1]

if not is_username_valid(username):
return {"message": "Your new username is invalid."}

name = data.get('name', None)
if name:
is_valid = validate_length(len(get_stripped_string(name)), NAME_MIN_LENGTH, NAME_MAX_LENGTH, 'name')
if not is_valid[0]:
return is_valid[1]

bio = data.get('bio', None)
if bio:
is_valid = validate_length(len(get_stripped_string(bio)), 0, BIO_MAX_LENGTH, 'bio')
if not is_valid[0]:
return is_valid[1]

location = data.get('location', None)
if location:
is_valid = validate_length(len(get_stripped_string(location)), 0, LOCATION_MAX_LENGTH, 'location')
if not is_valid[0]:
return is_valid[1]

occupation = data.get('occupation', None)
if occupation:
is_valid = validate_length(len(get_stripped_string(occupation)), 0, OCCUPATION_MAX_LENGTH, 'occupation')
if not is_valid[0]:
return is_valid[1]

organization = data.get('organization', None)
if organization:
is_valid = validate_length(len(get_stripped_string(organization)), 0, ORGANIZATION_MAX_LENGTH, 'organization')
if not is_valid[0]:
return is_valid[1]

slack_username = data.get('slack_username', None)
if slack_username:
is_valid = validate_length(len(get_stripped_string(slack_username)), 0, SLACK_USERNAME_MAX_LENGTH,
'slack_username')
if not is_valid[0]:
return is_valid[1]

social_media_links = data.get('social_media_links', None)
if social_media_links:
is_valid = validate_length(len(get_stripped_string(social_media_links)), 0, SOCIALS_MAX_LENGTH,
'social_media_links')
if not is_valid[0]:
return is_valid[1]

skills = data.get('skills', None)
if skills:
is_valid = validate_length(len(get_stripped_string(skills)), 0, SKILLS_MAX_LENGTH, 'skills')
if not is_valid[0]:
return is_valid[1]

interests = data.get('interests', None)
if interests:
is_valid = validate_length(len(get_stripped_string(interests)), 0, INTERESTS_MAX_LENGTH, 'interests')
if not is_valid[0]:
return is_valid[1]

return {}


def validate_new_password(data):
if 'current_password' not in data:
return {"message": "Current password field is missing."}
if 'new_password' not in data:
return {"message": "New password field is missing."}

new_password = data['new_password']
if not (PASSWORD_MIN_LENGTH <= len(new_password) <= PASSWORD_MAX_LENGTH):
return {"message":"The password field has to be longer than {min_limit} "
"characters and shorter than {max_limit} characters.".format(min_limit=PASSWORD_MIN_LENGTH-1,
max_limit=PASSWORD_MAX_LENGTH+1)}

if " " in new_password:
return {"message": "Password shouldn't contain spaces"}
return {"message": "Password shouldn't contain spaces."}

is_valid = validate_length(len(get_stripped_string(new_password)), PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
'new_password')
if not is_valid[0]:
return is_valid[1]

return {}
28 changes: 27 additions & 1 deletion app/utils/validation_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re

email_regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
username_regex = r"(^[a-zA-Z0-9]+$)"
username_regex = r"(^[a-zA-Z0-9_]+$)"


def is_email_valid(email):
Expand All @@ -10,3 +10,29 @@ def is_email_valid(email):

def is_username_valid(username):
return re.match(username_regex, username)


def validate_length(field_length, min_length, max_length, field_name):
if not (min_length <= field_length <= max_length):
if min_length <= 0:
error_msg = {"message": get_length_validation_error_message(field_name, None, max_length)}
else:
error_msg = {"message": get_length_validation_error_message(field_name, min_length, max_length)}
return False, error_msg
else:
return True, {}


def get_length_validation_error_message(field_name, min_length, max_length):
if min_length is None:
return "The {field_name} field has to be shorter than {max_limit} characters.".format(field_name=field_name,
max_limit=max_length + 1)
else:
return "The {field_name} field has to be longer than {min_limit} " \
"characters and shorter than {max_limit} characters.".format(field_name=field_name,
min_limit=min_length - 1,
max_limit=max_length + 1)


def get_stripped_string(string_with_whitespaces):
return ''.join(string_with_whitespaces.split())
37 changes: 37 additions & 0 deletions tests/users/test_api_update_user.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import unittest
from random import SystemRandom
from string import ascii_lowercase

from flask import json

from app.api.validations.user import USERNAME_MAX_LENGTH, USERNAME_MIN_LENGTH
from app.database.models.user import UserModel
from app.database.sqlalchemy_extension import db
from app.utils.validation_utils import get_length_validation_error_message
from tests.base_test_case import BaseTestCase
from tests.test_data import user1
from tests.test_utils import get_test_request_header
Expand Down Expand Up @@ -66,6 +72,37 @@ def test_update_username_not_taken(self):
self.assertEqual(expected_response, json.loads(actual_response.data))
self.assertEqual(user1_new_username, self.first_user.username)

def test_update_username_invalid_length(self):

self.first_user = UserModel(
name=user1['name'],
email=user1['email'],
username=user1['username'],
password=user1['password'],
terms_and_conditions_checked=user1['terms_and_conditions_checked']
)
self.first_user.is_email_verified = True

db.session.add(self.first_user)
db.session.commit()

field_name = 'username'
secure_random = SystemRandom()
random_generated_username = "".join(
secure_random.choice(ascii_lowercase) for x in range(USERNAME_MAX_LENGTH + 1))

auth_header = get_test_request_header(self.first_user.id)
expected_response = {"message": get_length_validation_error_message(field_name, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH)}
actual_response = self.client.put('/user', follow_redirects=True,
headers=auth_header,
data=json.dumps(dict(username=random_generated_username)),
content_type='application/json')

self.assertEqual(400, actual_response.status_code)
self.assertEqual(expected_response, json.loads(actual_response.data))
self.assertNotEqual(random_generated_username, self.first_user.username)
self.assertEqual(user1['username'], self.first_user.username)


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion tests/users/test_dao_update_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_dao_update_user(self):

data = dict(
occupation='good_developer',
organization='good_org',
organization='good_org'
)
UserDAO.update_user_profile(self.admin_user.id, data)

Expand Down

0 comments on commit eadfd3e

Please sign in to comment.