Coverage for server / validators / accounts_validator.py: 80%

74 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-10-04 09:33 +0000

1import re 

2from fastapi import HTTPException, status 

3 

4 

5def validate_fields(cls, values): 

6 try: 

7 values = is_not_empty(cls, values) 

8 values = validate_field_lengths(cls, values) 

9 values["email"] = check_email(cls, values["email"]) 

10 values = password_must_be_valid(cls, values) 

11 return values 

12 except Exception as e: 

13 raise 

14 

15 

16def validate_update_fields(cls, values): 

17 try: 

18 values = is_not_empty(cls, values) 

19 values = validate_field_lengths(cls, values) 

20 values["email"] = check_email(cls, values["email"]) 

21 return values 

22 except ValueError as e: 

23 raise 

24 

25 

26def validate_field_lengths(cls, values): 

27 """ 

28 Validates that fields don't exceed maximum character limits. 

29  

30 Field length limits: 

31 - first_name: 50 characters 

32 - middle_name: 50 characters 

33 - last_name: 50 characters 

34 - email: 100 characters 

35 - school: 100 characters 

36 # - created_by: 100 characters 

37 # - updated_by: 100 characters 

38  

39 Args: 

40 cls: The class instance 

41 values (dict): Dictionary containing field values 

42  

43 Returns: 

44 dict: The validated values dictionary 

45  

46 Raises: 

47 ValueError: If any field exceeds its maximum length 

48 """ 

49 field_limits = { 

50 "first_name": 50, 

51 "middle_name": 50, 

52 "last_name": 50, 

53 "email": 100, 

54 "school": 100, 

55 # "created_by": 100, 

56 # "updated_by": 100 

57 } 

58 

59 for field, limit in field_limits.items(): 

60 if field in values and values[field] is not None: 

61 if len(str(values[field])) > limit: 

62 raise ValueError(f"{field} field exceeds maximum length of {limit} characters") 

63 

64 return values 

65 

66 

67def is_not_empty(cls, values): 

68 for attr, value in values.items(): 

69 if attr in ["middle_name","created_by", "updated_by", "organization", "profile_picture"]: 

70 continue 

71 else: 

72 if str(value).strip() == "" and attr != "school": 

73 raise ValueError(f"{attr} field should not be empty") 

74 

75 if ( 

76 attr == "school" 

77 and "role" in values.keys() 

78 and values["role"] == "subscriber" 

79 ): 

80 if str(value).strip() == "": 

81 raise ValueError(f"{attr} field should not be empty") 

82 if ("role" not in values): 

83 raise ValueError("Role field should not be empty") 

84 if ("email" not in values): 

85 raise ValueError("Email field is required") 

86 

87 return values 

88 

89 

90def password_must_be_valid(cls, values): 

91 """ 

92 Validates password fields according to security requirements. 

93  

94 Password requirements: 

95 - Length: 10-30 characters 

96 - Must contain at least one lowercase letter 

97 - Must contain at least one uppercase letter 

98 - Must contain at least one number 

99 - Must contain at least one special character (@$!%*#?&.) 

100 - New password must differ from old password 

101 - New password must match the confirmation password 

102  

103 Args: 

104 cls: The class instance 

105 values (dict): Dictionary containing password fields: 

106 - 'password': For initial password setting 

107 - 'new_password': For password changes 

108 - 'old_password': Current password (required for changes) 

109 - 'repeat_new_password': Password confirmation 

110  

111 Returns: 

112 dict: The validated values dictionary 

113  

114 Raises: 

115 ValueError: If any password validation fails 

116 """ 

117 # Define regex pattern for password requirements 

118 password_requirements = ( 

119 r"^(?=.*[a-z])" # At least one lowercase letter 

120 r"(?=.*[A-Z])" # At least one uppercase letter 

121 r"(?=.*\d)" # At least one digit 

122 r"(?=.*[@$!%*#?&.])" # At least one special character 

123 r"[A-Za-z\d@$!%*#?&.]" # Only allow these characters 

124 r"{10,25}$" # Length between 10-25 characters (canonical) 

125 ) 

126 password_pattern = re.compile(password_requirements) 

127 

128 # Determine which password fields to use 

129 if "new_password" in values: 

130 password_field = "new_password" 

131 repeat_field = "repeat_new_password" 

132 password = values[password_field] 

133 

134 # Validate password complexity 

135 if not password_pattern.match(password): 

136 raise ValueError("New password is invalid.") 

137 

138 # Ensure new password is different from old password 

139 if "old_password" in values and password == values["old_password"]: 

140 raise ValueError("New password must be different from the old password.") 

141 

142 # Verify password confirmation matches 

143 if values.get(repeat_field) != password: 

144 raise HTTPException( 

145 status_code=status.HTTP_400_BAD_REQUEST, 

146 detail="Passwords do not match" 

147 ) 

148 

149 # Handle initial password setting scenario 

150 elif "password" in values: 

151 password_field = "password" 

152 repeat_field = "repeat_password" 

153 

154 if password_field not in values or repeat_field not in values: 

155 raise HTTPException( 

156 status_code=status.HTTP_400_BAD_REQUEST, 

157 detail="Field required" 

158 ) 

159 

160 password = values[password_field] 

161 repeat_password = values[repeat_field] 

162 

163 

164 if not password_pattern.match(password): 

165 raise HTTPException( 

166 status_code=status.HTTP_400_BAD_REQUEST, 

167 detail="Invalid password" 

168 ) 

169 

170 if repeat_password != password: 

171 raise HTTPException( 

172 status_code=status.HTTP_400_BAD_REQUEST, 

173 detail="Passwords do not match" 

174 ) 

175 

176 return values 

177 

178 

179def check_email(cls, email_value): 

180 if not email_value or email_value is None: 

181 raise ValueError("Email field should not be empty.") 

182 

183 # Convert email to lowercase for case insensitivity 

184 email_value = email_value 

185 

186 # Define the email validation regex 

187 email_pattern = ( 

188 r"^([a-zA-Z][a-zA-Z0-9_\.]{7,29})" # Username: 8-30 alphanumeric chars, starts with a letter, allows underscores 

189 r"@eruditiontx\.com$" # Domain: exactly "@eruditiontx.com" 

190 ) 

191 pattern = re.compile(email_pattern) 

192 

193 # Validate the email 

194 if not pattern.match(email_value): 

195 raise ValueError("Email is invalid.") 

196 

197 return email_value