""" 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')