Refactored DocumentService to save document in the filesystem. Fixed docker application
This commit is contained in:
365
tests/models/test_user_models.py
Normal file
365
tests/models/test_user_models.py
Normal file
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
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, UserResponse
|
||||
from app.models.auth import UserRole
|
||||
|
||||
|
||||
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')
|
||||
Reference in New Issue
Block a user