365 lines
11 KiB
Python
365 lines
11 KiB
Python
"""
|
|
Unit tests for user models and validation.
|
|
|
|
Tests the Pydantic models used for user creation, update, and response.
|
|
Validates email format, password strength, and data serialization.
|
|
"""
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
from datetime import datetime
|
|
from bson import ObjectId
|
|
|
|
from app.models.user import UserCreate, UserUpdate, UserInDB
|
|
from app.models.auth import UserRole, UserResponse
|
|
|
|
|
|
class TestUserCreateModel:
|
|
"""Tests for UserCreate Pydantic model validation."""
|
|
|
|
def test_i_can_create_user_create_model(self):
|
|
"""Test creation of valid UserCreate model with all required fields."""
|
|
user_data = {
|
|
"username": "testuser",
|
|
"email": "test@example.com",
|
|
"password": "TestPass123!",
|
|
"role": UserRole.USER
|
|
}
|
|
|
|
user = UserCreate(**user_data)
|
|
|
|
assert user.username == "testuser"
|
|
assert user.email == "test@example.com"
|
|
assert user.password == "TestPass123!"
|
|
assert user.role == UserRole.USER
|
|
|
|
def test_i_can_create_admin_user(self):
|
|
"""Test creation of admin user with admin role."""
|
|
user_data = {
|
|
"username": "adminuser",
|
|
"email": "admin@example.com",
|
|
"password": "AdminPass123!",
|
|
"role": UserRole.ADMIN
|
|
}
|
|
|
|
user = UserCreate(**user_data)
|
|
|
|
assert user.role == UserRole.ADMIN
|
|
|
|
def test_i_cannot_create_user_with_invalid_email(self):
|
|
"""Test that invalid email formats are rejected."""
|
|
invalid_emails = [
|
|
"notanemail",
|
|
"@example.com",
|
|
"test@",
|
|
"test.example.com",
|
|
"",
|
|
"test@.com"
|
|
]
|
|
|
|
for invalid_email in invalid_emails:
|
|
user_data = {
|
|
"username": "testuser",
|
|
"email": invalid_email,
|
|
"password": "TestPass123!",
|
|
"role": UserRole.USER
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(**user_data)
|
|
|
|
assert "email" in str(exc_info.value).lower()
|
|
|
|
def test_i_cannot_create_user_with_short_password(self):
|
|
"""Test that passwords shorter than 8 characters are rejected."""
|
|
short_passwords = [
|
|
"Test1!", # 6 chars
|
|
"Te1!", # 4 chars
|
|
"1234567", # 7 chars
|
|
"" # empty
|
|
]
|
|
|
|
for short_password in short_passwords:
|
|
user_data = {
|
|
"username": "testuser",
|
|
"email": "test@example.com",
|
|
"password": short_password,
|
|
"role": UserRole.USER
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(**user_data)
|
|
|
|
assert "password" in str(exc_info.value).lower()
|
|
|
|
def test_i_cannot_create_user_without_uppercase(self):
|
|
"""Test that passwords without uppercase letters are rejected."""
|
|
passwords_without_uppercase = [
|
|
"testpass123!",
|
|
"mypassword1!",
|
|
"lowercase123!"
|
|
]
|
|
|
|
for password in passwords_without_uppercase:
|
|
user_data = {
|
|
"username": "testuser",
|
|
"email": "test@example.com",
|
|
"password": password,
|
|
"role": UserRole.USER
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(**user_data)
|
|
|
|
assert "password" in str(exc_info.value).lower()
|
|
|
|
def test_i_cannot_create_user_without_lowercase(self):
|
|
"""Test that passwords without lowercase letters are rejected."""
|
|
passwords_without_lowercase = [
|
|
"TESTPASS123!",
|
|
"MYPASSWORD1!",
|
|
"UPPERCASE123!"
|
|
]
|
|
|
|
for password in passwords_without_lowercase:
|
|
user_data = {
|
|
"username": "testuser",
|
|
"email": "test@example.com",
|
|
"password": password,
|
|
"role": UserRole.USER
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(**user_data)
|
|
|
|
assert "password" in str(exc_info.value).lower()
|
|
|
|
def test_i_cannot_create_user_without_digit(self):
|
|
"""Test that passwords without digits are rejected."""
|
|
passwords_without_digit = [
|
|
"TestPassword!",
|
|
"MyPassword!",
|
|
"UpperLower!"
|
|
]
|
|
|
|
for password in passwords_without_digit:
|
|
user_data = {
|
|
"username": "testuser",
|
|
"email": "test@example.com",
|
|
"password": password,
|
|
"role": UserRole.USER
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(**user_data)
|
|
|
|
assert "password" in str(exc_info.value).lower()
|
|
|
|
def test_i_cannot_create_user_without_special_character(self):
|
|
"""Test that passwords without special characters are rejected."""
|
|
passwords_without_special = [
|
|
"TestPass123",
|
|
"MyPassword1",
|
|
"UpperLower1"
|
|
]
|
|
|
|
for password in passwords_without_special:
|
|
user_data = {
|
|
"username": "testuser",
|
|
"email": "test@example.com",
|
|
"password": password,
|
|
"role": UserRole.USER
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(**user_data)
|
|
|
|
assert "password" in str(exc_info.value).lower()
|
|
|
|
def test_i_cannot_create_user_with_empty_username(self):
|
|
"""Test that empty username is rejected."""
|
|
user_data = {
|
|
"username": "",
|
|
"email": "test@example.com",
|
|
"password": "TestPass123!",
|
|
"role": UserRole.USER
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(**user_data)
|
|
|
|
assert "username" in str(exc_info.value).lower()
|
|
|
|
def test_i_cannot_create_user_with_whitespace_username(self):
|
|
"""Test that username with only whitespace is rejected."""
|
|
whitespace_usernames = [" ", "\t", "\n", " \t\n "]
|
|
|
|
for username in whitespace_usernames:
|
|
user_data = {
|
|
"username": username,
|
|
"email": "test@example.com",
|
|
"password": "TestPass123!",
|
|
"role": UserRole.USER
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserCreate(**user_data)
|
|
|
|
assert "username" in str(exc_info.value).lower()
|
|
|
|
|
|
class TestUserUpdateModel:
|
|
"""Tests for UserUpdate Pydantic model validation."""
|
|
|
|
def test_i_can_create_user_update_model(self):
|
|
"""Test creation of valid UserUpdate model with optional fields."""
|
|
update_data = {
|
|
"email": "newemail@example.com",
|
|
"role": UserRole.ADMIN
|
|
}
|
|
|
|
user_update = UserUpdate(**update_data)
|
|
|
|
assert user_update.email == "newemail@example.com"
|
|
assert user_update.role == UserRole.ADMIN
|
|
assert user_update.username is None
|
|
assert user_update.password is None
|
|
|
|
def test_i_can_create_empty_user_update_model(self):
|
|
"""Test creation of UserUpdate model with no fields (all optional)."""
|
|
user_update = UserUpdate()
|
|
|
|
assert user_update.username is None
|
|
assert user_update.email is None
|
|
assert user_update.password is None
|
|
assert user_update.role is None
|
|
|
|
def test_i_can_update_password_with_valid_format(self):
|
|
"""Test that valid password can be used in update."""
|
|
update_data = {
|
|
"password": "NewPass123!"
|
|
}
|
|
|
|
user_update = UserUpdate(**update_data)
|
|
|
|
assert user_update.password == "NewPass123!"
|
|
|
|
def test_i_cannot_update_with_invalid_password(self):
|
|
"""Test that invalid password format is rejected in update."""
|
|
update_data = {
|
|
"password": "weak"
|
|
}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UserUpdate(**update_data)
|
|
|
|
assert "password" in str(exc_info.value).lower()
|
|
|
|
|
|
class TestUserInDBModel:
|
|
"""Tests for UserInDB Pydantic model (database representation)."""
|
|
|
|
def test_i_can_create_user_in_db_model(self):
|
|
"""Test creation of valid UserInDB model with all fields."""
|
|
user_id = ObjectId()
|
|
created_at = datetime.now()
|
|
updated_at = datetime.now()
|
|
|
|
user_data = {
|
|
"id": user_id,
|
|
"username": "testuser",
|
|
"email": "test@example.com",
|
|
"hashed_password": "$2b$12$hashedpassword",
|
|
"role": UserRole.USER,
|
|
"is_active": True,
|
|
"created_at": created_at,
|
|
"updated_at": updated_at
|
|
}
|
|
|
|
user = UserInDB(**user_data)
|
|
|
|
assert user.id == user_id
|
|
assert user.username == "testuser"
|
|
assert user.email == "test@example.com"
|
|
assert user.hashed_password == "$2b$12$hashedpassword"
|
|
assert user.role == UserRole.USER
|
|
assert user.is_active is True
|
|
assert user.created_at == created_at
|
|
assert user.updated_at == updated_at
|
|
|
|
|
|
class TestUserResponseModel:
|
|
"""Tests for UserResponse Pydantic model (API response)."""
|
|
|
|
def test_i_can_create_user_response_model(self):
|
|
"""Test creation of valid UserResponse model without password."""
|
|
user_id = ObjectId()
|
|
created_at = datetime.now()
|
|
updated_at = datetime.now()
|
|
|
|
user_data = {
|
|
"id": user_id,
|
|
"username": "testuser",
|
|
"email": "test@example.com",
|
|
"role": UserRole.USER,
|
|
"is_active": True,
|
|
"created_at": created_at,
|
|
"updated_at": updated_at
|
|
}
|
|
|
|
user = UserResponse(**user_data)
|
|
|
|
assert user.id == user_id
|
|
assert user.username == "testuser"
|
|
assert user.email == "test@example.com"
|
|
assert user.role == UserRole.USER
|
|
assert user.is_active is True
|
|
assert user.created_at == created_at
|
|
assert user.updated_at == updated_at
|
|
# Verify password_hash is not included
|
|
assert not hasattr(user, 'password_hash')
|
|
|
|
def test_user_response_excludes_password_hash(self):
|
|
"""Test that UserResponse model does not expose password_hash."""
|
|
# This test verifies the model structure doesn't include password_hash
|
|
response_fields = UserResponse.__fields__.keys()
|
|
|
|
assert 'password_hash' not in response_fields
|
|
assert 'username' in response_fields
|
|
assert 'email' in response_fields
|
|
assert 'role' in response_fields
|
|
assert 'is_active' in response_fields
|
|
|
|
def test_i_can_convert_user_in_db_to_response(self):
|
|
"""Test conversion from UserInDB to UserResponse model."""
|
|
user_id = ObjectId()
|
|
created_at = datetime.now()
|
|
updated_at = datetime.now()
|
|
|
|
user_in_db = UserInDB(
|
|
id=user_id,
|
|
username="testuser",
|
|
email="test@example.com",
|
|
hashed_password="$2b$12$hashedpassword",
|
|
role=UserRole.USER,
|
|
is_active=True,
|
|
created_at=created_at,
|
|
updated_at=updated_at
|
|
)
|
|
|
|
# Convert to response model (excluding password_hash)
|
|
user_response = UserResponse(
|
|
_id=user_in_db.id,
|
|
username=user_in_db.username,
|
|
email=user_in_db.email,
|
|
role=user_in_db.role,
|
|
is_active=user_in_db.is_active,
|
|
created_at=user_in_db.created_at,
|
|
updated_at=user_in_db.updated_at
|
|
)
|
|
|
|
assert user_response.id == user_in_db.id
|
|
assert user_response.username == user_in_db.username
|
|
assert user_response.email == user_in_db.email
|
|
assert user_response.role == user_in_db.role
|
|
assert not hasattr(user_response, 'password_hash') |