Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class UserCreateRequest(UserBase):
Request schema for user creation with conditional password validation
"""

password: Optional[str] = Field(None, min_length=8)
password: Optional[str] = Field(None)
auth_id: Optional[str] = Field(None)
signup_method: SignUpMethod = Field(default=SignUpMethod.PASSWORD)

Expand All @@ -65,12 +65,18 @@ def validate_password(cls, password: Optional[str], info):
raise ValueError("Password is required for password signup")

if password:
if not any(char.isdigit() for char in password):
raise ValueError("Password must contain at least one digit")
errors = []
if len(password) < 8:
errors.append("Password must be at least 8 characters long")
if not any(char.isupper() for char in password):
raise ValueError("Password must contain at least one uppercase letter")
errors.append("Password must contain at least one uppercase letter")
if not any(char.islower() for char in password):
raise ValueError("Password must contain at least one lowercase letter")
errors.append("Password must contain at least one lowercase letter")
if not any(char in "!@#$%^&*" for char in password):
errors.append("Password must contain at least one special character (!, @, #, $, %, ^, &, or *)")

if errors:
raise ValueError(errors)

return password

Expand Down
161 changes: 161 additions & 0 deletions backend/tests/unit/test_user_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import pytest
from pydantic import ValidationError

from app.schemas.user import SignUpMethod, UserCreateRequest, UserRole


class TestUserCreateRequestPasswordValidation:
"""Test password validation in UserCreateRequest schema"""

def test_password_validation_success_valid_password(self):
"""Test that a valid password passes validation"""
user_data = {
"first_name": "Test",
"last_name": "User",
"email": "[email protected]",
"password": "ValidPass123!",
"role": UserRole.PARTICIPANT,
"signup_method": SignUpMethod.PASSWORD,
}

# Should not raise any exception
user_request = UserCreateRequest(**user_data)
assert user_request.password == "ValidPass123!"

def test_password_validation_too_short(self):
"""Test that password less than 8 characters fails validation"""
user_data = {
"first_name": "Test",
"last_name": "User",
"email": "[email protected]",
"password": "short1!", # Only 7 characters
"role": UserRole.PARTICIPANT,
"signup_method": SignUpMethod.PASSWORD,
}

with pytest.raises(ValidationError) as exc_info:
UserCreateRequest(**user_data)

# Check that the error message contains the expected validation message
error_msg = str(exc_info.value)
assert "Password must be at least 8 characters long" in error_msg

def test_password_validation_missing_uppercase(self):
"""Test that password without uppercase letter fails validation"""
user_data = {
"first_name": "Test",
"last_name": "User",
"email": "[email protected]",
"password": "lowercase123!", # No uppercase
"role": UserRole.PARTICIPANT,
"signup_method": SignUpMethod.PASSWORD,
}

with pytest.raises(ValidationError) as exc_info:
UserCreateRequest(**user_data)

error_msg = str(exc_info.value)
assert "Password must contain at least one uppercase letter" in error_msg

def test_password_validation_missing_lowercase(self):
"""Test that password without lowercase letter fails validation"""
user_data = {
"first_name": "Test",
"last_name": "User",
"email": "[email protected]",
"password": "UPPERCASE123!", # No lowercase
"role": UserRole.PARTICIPANT,
"signup_method": SignUpMethod.PASSWORD,
}

with pytest.raises(ValidationError) as exc_info:
UserCreateRequest(**user_data)

error_msg = str(exc_info.value)
assert "Password must contain at least one lowercase letter" in error_msg

def test_password_validation_missing_special_character(self):
"""Test that password without special character fails validation"""
user_data = {
"first_name": "Test",
"last_name": "User",
"email": "[email protected]",
"password": "NoSpecial123", # No special characters
"role": UserRole.PARTICIPANT,
"signup_method": SignUpMethod.PASSWORD,
}

with pytest.raises(ValidationError) as exc_info:
UserCreateRequest(**user_data)

error_msg = str(exc_info.value)
assert "Password must contain at least one special character" in error_msg

def test_password_validation_multiple_errors(self):
"""Test that password with multiple issues returns all errors"""
user_data = {
"first_name": "Test",
"last_name": "User",
"email": "[email protected]",
"password": "short", # Too short, no uppercase, no special char
"role": UserRole.PARTICIPANT,
"signup_method": SignUpMethod.PASSWORD,
}

with pytest.raises(ValidationError) as exc_info:
UserCreateRequest(**user_data)

error_msg = str(exc_info.value)
# Should contain multiple validation errors
assert "Password must be at least 8 characters long" in error_msg
assert "Password must contain at least one uppercase letter" in error_msg
assert "Password must contain at least one special character" in error_msg

def test_password_validation_google_signup_no_password_required(self):
"""Test that Google signup doesn't require password validation"""
user_data = {
"first_name": "Test",
"last_name": "User",
"email": "[email protected]",
# No password provided
"role": UserRole.PARTICIPANT,
"signup_method": SignUpMethod.GOOGLE,
}

# Should not raise any exception for Google signup
user_request = UserCreateRequest(**user_data)
assert user_request.password is None

def test_password_validation_edge_cases(self):
"""Test edge cases for password validation"""
# Test with exactly 8 characters
user_data = {
"first_name": "Test",
"last_name": "User",
"email": "[email protected]",
"password": "MinPass1!", # Exactly 8 chars, meets all requirements
"role": UserRole.PARTICIPANT,
"signup_method": SignUpMethod.PASSWORD,
}

# Should not raise any exception
user_request = UserCreateRequest(**user_data)
assert user_request.password == "MinPass1!"

def test_password_validation_all_special_characters(self):
"""Test that all allowed special characters work"""
special_chars = ["!", "@", "#", "$", "%", "^", "&", "*"]

for char in special_chars:
user_data = {
"first_name": "Test",
"last_name": "User",
"email": "[email protected]",
"password": f"ValidPass1{char}",
"role": UserRole.PARTICIPANT,
"signup_method": SignUpMethod.PASSWORD,
}

# Should not raise any exception
user_request = UserCreateRequest(**user_data)
assert user_request.password == f"ValidPass1{char}"
22 changes: 21 additions & 1 deletion frontend/src/APIClients/authAPIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface AuthResult {
user?: AuthenticatedUser;
error?: string;
errorCode?: string;
validationErrors?: string[];
}

const login = async (email: string, password: string): Promise<AuthResult> => {
Expand Down Expand Up @@ -223,9 +224,28 @@ export const register = async ({
} else if (response?.status === 400) {
const detail = response?.data?.detail || 'Invalid registration data';
return { success: false, error: detail };
} else if (response?.status === 422) {
// Pydantic validation errors
const validationErrors = response?.data?.detail;
console.log('[REGISTER] Validation errors:', validationErrors);
if (Array.isArray(validationErrors)) {
// Extract password validation errors specifically
const passwordErrors = validationErrors
.filter((err) => err.loc && err.loc.includes('password'))
.map((err) => err.msg);

if (passwordErrors.length > 0) {
return {
success: false,
error: 'password_validation',
validationErrors: passwordErrors,
};
}
}
// Fallback for other validation errors
return { success: false, error: response?.data?.detail || 'Validation failed' };
}
}

return { success: false, error: 'Registration failed. Please try again.' };
}
};
Expand Down
Loading