Added frontend. Working on user management

This commit is contained in:
2025-09-16 22:58:28 +02:00
parent 10650420ef
commit 2958d5cf82
31 changed files with 5101 additions and 15 deletions

194
tests/test_connection.py Normal file
View File

@@ -0,0 +1,194 @@
"""
Unit tests for MongoDB database connection module.
Tests the database connection functionality with mocking
to avoid requiring actual MongoDB instance during tests.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
from app.database.connection import (
create_mongodb_client,
get_database,
close_database_connection,
get_mongodb_client,
test_database_connection
)
def test_i_can_get_database_connection():
"""Test successful database connection creation."""
mock_client = Mock()
mock_database = Mock()
mock_client.__getitem__.return_value = mock_database
with patch('app.database.connection.MongoClient', return_value=mock_client):
with patch('app.database.connection.get_mongodb_url', return_value="mongodb://localhost:27017"):
with patch('app.database.connection.get_mongodb_database_name', return_value="testdb"):
# Reset global variables
import app.database.connection
app.database.connection._client = None
app.database.connection._database = None
result = get_database()
assert result == mock_database
mock_client.admin.command.assert_called_with('ping')
def test_i_cannot_connect_to_invalid_mongodb_url():
"""Test fail-fast behavior with invalid MongoDB URL."""
mock_client = Mock()
mock_client.admin.command.side_effect = ConnectionFailure("Connection failed")
with patch('app.database.connection.MongoClient', return_value=mock_client):
with patch('app.database.connection.get_mongodb_url', return_value="mongodb://invalid:27017"):
with pytest.raises(SystemExit) as exc_info:
create_mongodb_client()
assert exc_info.value.code == 1
def test_i_cannot_connect_with_server_selection_timeout():
"""Test fail-fast behavior with server selection timeout."""
mock_client = Mock()
mock_client.admin.command.side_effect = ServerSelectionTimeoutError("Timeout")
with patch('app.database.connection.MongoClient', return_value=mock_client):
with patch('app.database.connection.get_mongodb_url', return_value="mongodb://timeout:27017"):
with pytest.raises(SystemExit) as exc_info:
create_mongodb_client()
assert exc_info.value.code == 1
def test_i_cannot_connect_with_unexpected_error():
"""Test fail-fast behavior with unexpected connection error."""
with patch('app.database.connection.MongoClient', side_effect=Exception("Unexpected error")):
with patch('app.database.connection.get_mongodb_url', return_value="mongodb://error:27017"):
with pytest.raises(SystemExit) as exc_info:
create_mongodb_client()
assert exc_info.value.code == 1
def test_i_can_get_database_singleton():
"""Test that get_database returns the same instance (singleton pattern)."""
mock_client = Mock()
mock_database = Mock()
mock_client.__getitem__.return_value = mock_database
with patch('app.database.connection.MongoClient', return_value=mock_client):
with patch('app.database.connection.get_mongodb_url', return_value="mongodb://localhost:27017"):
with patch('app.database.connection.get_mongodb_database_name', return_value="testdb"):
# Reset global variables
import app.database.connection
app.database.connection._client = None
app.database.connection._database = None
# First call
db1 = get_database()
# Second call
db2 = get_database()
assert db1 is db2
# MongoClient should be called only once
assert mock_client.admin.command.call_count == 1
def test_i_can_close_database_connection():
"""Test closing database connection."""
mock_client = Mock()
mock_database = Mock()
mock_client.__getitem__.return_value = mock_database
with patch('app.database.connection.MongoClient', return_value=mock_client):
with patch('app.database.connection.get_mongodb_url', return_value="mongodb://localhost:27017"):
with patch('app.database.connection.get_mongodb_database_name', return_value="testdb"):
# Reset global variables
import app.database.connection
app.database.connection._client = None
app.database.connection._database = None
# Create connection
get_database()
# Close connection
close_database_connection()
mock_client.close.assert_called_once()
assert app.database.connection._client is None
assert app.database.connection._database is None
def test_i_can_get_mongodb_client():
"""Test getting raw MongoDB client instance."""
mock_client = Mock()
mock_database = Mock()
mock_client.__getitem__.return_value = mock_database
with patch('app.database.connection.MongoClient', return_value=mock_client):
with patch('app.database.connection.get_mongodb_url', return_value="mongodb://localhost:27017"):
with patch('app.database.connection.get_mongodb_database_name', return_value="testdb"):
# Reset global variables
import app.database.connection
app.database.connection._client = None
app.database.connection._database = None
# Create connection first
get_database()
# Get client
result = get_mongodb_client()
assert result == mock_client
def test_i_can_get_none_mongodb_client_when_not_connected():
"""Test getting MongoDB client returns None when not connected."""
# Reset global variables
import app.database.connection
app.database.connection._client = None
app.database.connection._database = None
result = get_mongodb_client()
assert result is None
def test_i_can_test_database_connection_success():
"""Test database connection health check - success case."""
mock_database = Mock()
mock_database.command.return_value = True
with patch('app.database.connection.get_database', return_value=mock_database):
result = test_database_connection()
assert result is True
mock_database.command.assert_called_with('ping')
def test_i_cannot_test_database_connection_failure():
"""Test database connection health check - failure case."""
mock_database = Mock()
mock_database.command.side_effect = Exception("Connection error")
with patch('app.database.connection.get_database', return_value=mock_database):
result = test_database_connection()
assert result is False
def test_i_can_close_connection_when_no_client():
"""Test closing connection when no client exists (should not raise error)."""
# Reset global variables
import app.database.connection
app.database.connection._client = None
app.database.connection._database = None
# Should not raise any exception
close_database_connection()
assert app.database.connection._client is None
assert app.database.connection._database is None

105
tests/test_security.py Normal file
View File

@@ -0,0 +1,105 @@
"""
Unit tests for password security utilities.
Tests the bcrypt-based password hashing and verification functions
including edge cases and error handling.
"""
import pytest
from app.utils.security import hash_password, verify_password
def test_i_can_hash_password():
"""Test that a password is correctly hashed and different from original."""
password = "my_secure_password"
hashed = hash_password(password)
# Hash should be different from original password
assert hashed != password
# Hash should be a non-empty string
assert isinstance(hashed, str)
assert len(hashed) > 0
# Hash should start with bcrypt identifier
assert hashed.startswith("$2b$")
def test_same_password_generates_different_hashes():
"""Test that the salt generates different hashes for the same password."""
password = "identical_password"
hash1 = hash_password(password)
hash2 = hash_password(password)
# Same password should generate different hashes due to salt
assert hash1 != hash2
# But both should be valid bcrypt hashes
assert hash1.startswith("$2b$")
assert hash2.startswith("$2b$")
def test_i_can_verify_correct_password():
"""Test that a correct password is validated against its hash."""
password = "correct_password"
hashed = hash_password(password)
# Correct password should verify successfully
assert verify_password(password, hashed) is True
def test_i_cannot_verify_incorrect_password():
"""Test that an incorrect password is rejected."""
password = "correct_password"
wrong_password = "wrong_password"
hashed = hash_password(password)
# Wrong password should fail verification
assert verify_password(wrong_password, hashed) is False
def test_i_cannot_hash_empty_password():
"""Test that empty passwords are rejected during hashing."""
# Empty string should raise ValueError
with pytest.raises(ValueError, match="Password cannot be empty or None"):
hash_password("")
# None should raise ValueError
with pytest.raises(ValueError, match="Password cannot be empty or None"):
hash_password(None)
def test_i_cannot_verify_with_malformed_hash():
"""Test that malformed hashes are rejected during verification."""
password = "test_password"
malformed_hash = "not_a_valid_bcrypt_hash"
# Malformed hash should raise RuntimeError
with pytest.raises(RuntimeError, match="Invalid hash format"):
verify_password(password, malformed_hash)
def test_i_cannot_verify_with_none_values():
"""Test that None values are rejected during verification."""
password = "test_password"
hashed = hash_password(password)
# None password should raise ValueError
with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
verify_password(None, hashed)
# None hash should raise ValueError
with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
verify_password(password, None)
# Both None should raise ValueError
with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
verify_password(None, None)
# Empty strings should also raise ValueError
with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
verify_password("", hashed)
with pytest.raises(ValueError, match="Password and hashed_password cannot be empty or None"):
verify_password(password, "")

141
tests/test_settings.py Normal file
View File

@@ -0,0 +1,141 @@
"""
Unit tests for configuration settings module.
Tests the environment variable loading and default values
for application configuration.
"""
import os
import pytest
from unittest.mock import patch
from app.config.settings import (
get_mongodb_url,
get_mongodb_database_name,
get_jwt_secret_key,
get_jwt_algorithm,
get_jwt_expire_hours,
is_development_environment
)
def test_i_can_load_mongodb_url_from_env():
"""Test loading MongoDB URL from environment variable."""
test_url = "mongodb://test-server:27017"
with patch.dict(os.environ, {"MONGODB_URL": test_url}):
result = get_mongodb_url()
assert result == test_url
def test_i_can_use_default_mongodb_url():
"""Test default MongoDB URL when environment variable is not set."""
with patch.dict(os.environ, {}, clear=True):
result = get_mongodb_url()
assert result == "mongodb://localhost:27017"
def test_i_can_load_mongodb_database_name_from_env():
"""Test loading MongoDB database name from environment variable."""
test_db_name = "test_database"
with patch.dict(os.environ, {"MONGODB_DATABASE": test_db_name}):
result = get_mongodb_database_name()
assert result == test_db_name
def test_i_can_use_default_mongodb_database_name():
"""Test default MongoDB database name when environment variable is not set."""
with patch.dict(os.environ, {}, clear=True):
result = get_mongodb_database_name()
assert result == "mydocmanager"
def test_i_can_load_jwt_secret_from_env():
"""Test loading JWT secret from environment variable."""
test_secret = "super-secret-key-123"
with patch.dict(os.environ, {"JWT_SECRET": test_secret}):
result = get_jwt_secret_key()
assert result == test_secret
def test_i_can_use_default_jwt_secret_in_development():
"""Test default JWT secret in development environment."""
with patch.dict(os.environ, {"ENVIRONMENT": "development"}, clear=True):
result = get_jwt_secret_key()
assert result == "dev-secret-key-change-in-production"
def test_i_cannot_get_jwt_secret_in_production_without_env():
"""Test that JWT secret raises error in production without environment variable."""
with patch.dict(os.environ, {"ENVIRONMENT": "production"}, clear=True):
with pytest.raises(ValueError, match="JWT_SECRET environment variable must be set in production"):
get_jwt_secret_key()
def test_i_can_load_jwt_algorithm_from_env():
"""Test loading JWT algorithm from environment variable."""
test_algorithm = "RS256"
with patch.dict(os.environ, {"JWT_ALGORITHM": test_algorithm}):
result = get_jwt_algorithm()
assert result == test_algorithm
def test_i_can_use_default_jwt_algorithm():
"""Test default JWT algorithm when environment variable is not set."""
with patch.dict(os.environ, {}, clear=True):
result = get_jwt_algorithm()
assert result == "HS256"
def test_i_can_load_jwt_expire_hours_from_env():
"""Test loading JWT expiration hours from environment variable."""
test_hours = "48"
with patch.dict(os.environ, {"JWT_EXPIRE_HOURS": test_hours}):
result = get_jwt_expire_hours()
assert result == 48
def test_i_can_use_default_jwt_expire_hours():
"""Test default JWT expiration hours when environment variable is not set."""
with patch.dict(os.environ, {}, clear=True):
result = get_jwt_expire_hours()
assert result == 24
def test_i_can_handle_invalid_jwt_expire_hours():
"""Test handling of invalid JWT expiration hours value."""
with patch.dict(os.environ, {"JWT_EXPIRE_HOURS": "invalid"}):
result = get_jwt_expire_hours()
assert result == 24 # Should fall back to default
def test_i_can_detect_development_environment():
"""Test detection of development environment."""
with patch.dict(os.environ, {"ENVIRONMENT": "development"}):
result = is_development_environment()
assert result is True
def test_i_can_detect_production_environment():
"""Test detection of production environment."""
with patch.dict(os.environ, {"ENVIRONMENT": "production"}):
result = is_development_environment()
assert result is False
def test_i_can_use_default_development_environment():
"""Test default environment is development."""
with patch.dict(os.environ, {}, clear=True):
result = is_development_environment()
assert result is True
def test_i_can_handle_case_insensitive_environment():
"""Test case insensitive environment detection."""
with patch.dict(os.environ, {"ENVIRONMENT": "DEVELOPMENT"}):
result = is_development_environment()
assert result is True

382
tests/test_user_models.py Normal file
View File

@@ -0,0 +1,382 @@
"""
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.utcnow()
updated_at = datetime.utcnow()
user_data = {
"id": user_id,
"username": "testuser",
"email": "test@example.com",
"password_hash": "$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.password_hash == "$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
def test_i_can_create_inactive_user(self):
"""Test creation of inactive user."""
user_data = {
"id": ObjectId(),
"username": "testuser",
"email": "test@example.com",
"password_hash": "$2b$12$hashedpassword",
"role": UserRole.USER,
"is_active": False,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
}
user = UserInDB(**user_data)
assert user.is_active is False
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.utcnow()
updated_at = datetime.utcnow()
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.utcnow()
updated_at = datetime.utcnow()
user_in_db = UserInDB(
id=user_id,
username="testuser",
email="test@example.com",
password_hash="$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')

View File

@@ -0,0 +1,385 @@
"""
Unit tests for user repository module.
Tests all CRUD operations for users with MongoDB mocking
to ensure proper database interactions without requiring
actual MongoDB instance during tests.
"""
import pytest
from unittest.mock import Mock, MagicMock
from datetime import datetime
from bson import ObjectId
from pymongo.errors import DuplicateKeyError
from app.database.repositories.user_repository import UserRepository
from app.models.user import UserCreate, UserUpdate, UserInDB, UserRole
@pytest.fixture
def mock_database():
"""Create mock database with users collection."""
db = Mock()
collection = Mock()
db.users = collection
return db
@pytest.fixture
def user_repository(mock_database):
"""Create UserRepository instance with mocked database."""
return UserRepository(mock_database)
@pytest.fixture
def sample_user_create():
"""Create sample UserCreate object for testing."""
return UserCreate(
username="testuser",
email="test@example.com",
hashed_password="hashed_password_123",
role=UserRole.USER,
is_active=True
)
@pytest.fixture
def sample_user_update():
"""Create sample UserUpdate object for testing."""
return UserUpdate(
email="updated@example.com",
role=UserRole.ADMIN,
is_active=False
)
def test_i_can_create_user(user_repository, mock_database, sample_user_create):
"""Test successful user creation."""
# Mock successful insertion
mock_result = Mock()
mock_result.inserted_id = ObjectId()
mock_database.users.insert_one.return_value = mock_result
result = user_repository.create_user(sample_user_create)
assert isinstance(result, UserInDB)
assert result.username == sample_user_create.username
assert result.email == sample_user_create.email
assert result.hashed_password == sample_user_create.hashed_password
assert result.role == sample_user_create.role
assert result.is_active == sample_user_create.is_active
assert result.id is not None
assert isinstance(result.created_at, datetime)
assert isinstance(result.updated_at, datetime)
# Verify insert_one was called with correct data
mock_database.users.insert_one.assert_called_once()
call_args = mock_database.users.insert_one.call_args[0][0]
assert call_args["username"] == sample_user_create.username
assert call_args["email"] == sample_user_create.email
def test_i_cannot_create_duplicate_username(user_repository, mock_database, sample_user_create):
"""Test that creating user with duplicate username raises DuplicateKeyError."""
# Mock DuplicateKeyError from MongoDB
mock_database.users.insert_one.side_effect = DuplicateKeyError("duplicate key error")
with pytest.raises(DuplicateKeyError, match="User with username 'testuser' already exists"):
user_repository.create_user(sample_user_create)
def test_i_can_find_user_by_username(user_repository, mock_database):
"""Test finding user by username."""
# Mock user document from database
user_doc = {
"_id": ObjectId(),
"username": "testuser",
"email": "test@example.com",
"hashed_password": "hashed_password_123",
"role": "user",
"is_active": True,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
}
mock_database.users.find_one.return_value = user_doc
result = user_repository.find_user_by_username("testuser")
assert isinstance(result, UserInDB)
assert result.username == "testuser"
assert result.email == "test@example.com"
mock_database.users.find_one.assert_called_once_with({"username": "testuser"})
def test_i_cannot_find_nonexistent_user_by_username(user_repository, mock_database):
"""Test finding nonexistent user by username returns None."""
mock_database.users.find_one.return_value = None
result = user_repository.find_user_by_username("nonexistent")
assert result is None
mock_database.users.find_one.assert_called_once_with({"username": "nonexistent"})
def test_i_can_find_user_by_id(user_repository, mock_database):
"""Test finding user by ID."""
user_id = ObjectId()
user_doc = {
"_id": user_id,
"username": "testuser",
"email": "test@example.com",
"hashed_password": "hashed_password_123",
"role": "user",
"is_active": True,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
}
mock_database.users.find_one.return_value = user_doc
result = user_repository.find_user_by_id(str(user_id))
assert isinstance(result, UserInDB)
assert result.id == user_id
assert result.username == "testuser"
mock_database.users.find_one.assert_called_once_with({"_id": user_id})
def test_i_cannot_find_user_with_invalid_id(user_repository, mock_database):
"""Test finding user with invalid ObjectId returns None."""
result = user_repository.find_user_by_id("invalid_id")
assert result is None
# find_one should not be called with invalid ID
mock_database.users.find_one.assert_not_called()
def test_i_cannot_find_nonexistent_user_by_id(user_repository, mock_database):
"""Test finding nonexistent user by ID returns None."""
user_id = ObjectId()
mock_database.users.find_one.return_value = None
result = user_repository.find_user_by_id(str(user_id))
assert result is None
mock_database.users.find_one.assert_called_once_with({"_id": user_id})
def test_i_can_find_user_by_email(user_repository, mock_database):
"""Test finding user by email address."""
user_doc = {
"_id": ObjectId(),
"username": "testuser",
"email": "test@example.com",
"hashed_password": "hashed_password_123",
"role": "user",
"is_active": True,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
}
mock_database.users.find_one.return_value = user_doc
result = user_repository.find_user_by_email("test@example.com")
assert isinstance(result, UserInDB)
assert result.email == "test@example.com"
mock_database.users.find_one.assert_called_once_with({"email": "test@example.com"})
def test_i_can_update_user(user_repository, mock_database, sample_user_update):
"""Test updating user information."""
user_id = ObjectId()
# Mock successful update
mock_update_result = Mock()
mock_update_result.matched_count = 1
mock_database.users.update_one.return_value = mock_update_result
# Mock find_one for returning updated user
updated_user_doc = {
"_id": user_id,
"username": "testuser",
"email": "updated@example.com",
"hashed_password": "hashed_password_123",
"role": "admin",
"is_active": False,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
}
mock_database.users.find_one.return_value = updated_user_doc
result = user_repository.update_user(str(user_id), sample_user_update)
assert isinstance(result, UserInDB)
assert result.email == "updated@example.com"
assert result.role == UserRole.ADMIN
assert result.is_active is False
# Verify update_one was called with correct data
mock_database.users.update_one.assert_called_once()
call_args = mock_database.users.update_one.call_args
assert call_args[0][0] == {"_id": user_id} # Filter
update_data = call_args[0][1]["$set"] # Update data
assert update_data["email"] == "updated@example.com"
assert update_data["role"] == UserRole.ADMIN
assert update_data["is_active"] is False
assert "updated_at" in update_data
def test_i_cannot_update_nonexistent_user(user_repository, mock_database, sample_user_update):
"""Test updating nonexistent user returns None."""
user_id = ObjectId()
# Mock no match found
mock_update_result = Mock()
mock_update_result.matched_count = 0
mock_database.users.update_one.return_value = mock_update_result
result = user_repository.update_user(str(user_id), sample_user_update)
assert result is None
def test_i_cannot_update_user_with_invalid_id(user_repository, mock_database, sample_user_update):
"""Test updating user with invalid ID returns None."""
result = user_repository.update_user("invalid_id", sample_user_update)
assert result is None
# update_one should not be called with invalid ID
mock_database.users.update_one.assert_not_called()
def test_i_can_delete_user(user_repository, mock_database):
"""Test successful user deletion."""
user_id = ObjectId()
# Mock successful deletion
mock_delete_result = Mock()
mock_delete_result.deleted_count = 1
mock_database.users.delete_one.return_value = mock_delete_result
result = user_repository.delete_user(str(user_id))
assert result is True
mock_database.users.delete_one.assert_called_once_with({"_id": user_id})
def test_i_cannot_delete_nonexistent_user(user_repository, mock_database):
"""Test deleting nonexistent user returns False."""
user_id = ObjectId()
# Mock no deletion occurred
mock_delete_result = Mock()
mock_delete_result.deleted_count = 0
mock_database.users.delete_one.return_value = mock_delete_result
result = user_repository.delete_user(str(user_id))
assert result is False
def test_i_cannot_delete_user_with_invalid_id(user_repository, mock_database):
"""Test deleting user with invalid ID returns False."""
result = user_repository.delete_user("invalid_id")
assert result is False
# delete_one should not be called with invalid ID
mock_database.users.delete_one.assert_not_called()
def test_i_can_list_users(user_repository, mock_database):
"""Test listing users with pagination."""
# Mock cursor with user documents
user_docs = [
{
"_id": ObjectId(),
"username": "user1",
"email": "user1@example.com",
"hashed_password": "hash1",
"role": "user",
"is_active": True,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
},
{
"_id": ObjectId(),
"username": "user2",
"email": "user2@example.com",
"hashed_password": "hash2",
"role": "admin",
"is_active": False,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
}
]
mock_cursor = Mock()
mock_cursor.__iter__.return_value = iter(user_docs)
mock_cursor.skip.return_value = mock_cursor
mock_cursor.limit.return_value = mock_cursor
mock_database.users.find.return_value = mock_cursor
result = user_repository.list_users(skip=10, limit=50)
assert len(result) == 2
assert all(isinstance(user, UserInDB) for user in result)
assert result[0].username == "user1"
assert result[1].username == "user2"
mock_database.users.find.assert_called_once()
mock_cursor.skip.assert_called_once_with(10)
mock_cursor.limit.assert_called_once_with(50)
def test_i_can_count_users(user_repository, mock_database):
"""Test counting total users."""
mock_database.users.count_documents.return_value = 42
result = user_repository.count_users()
assert result == 42
mock_database.users.count_documents.assert_called_once_with({})
def test_i_can_check_user_exists(user_repository, mock_database):
"""Test checking if user exists by username."""
mock_database.users.count_documents.return_value = 1
result = user_repository.user_exists("testuser")
assert result is True
mock_database.users.count_documents.assert_called_once_with({"username": "testuser"})
def test_i_can_check_user_does_not_exist(user_repository, mock_database):
"""Test checking if user does not exist by username."""
mock_database.users.count_documents.return_value = 0
result = user_repository.user_exists("nonexistent")
assert result is False
mock_database.users.count_documents.assert_called_once_with({"username": "nonexistent"})
def test_i_can_create_indexes_on_initialization(mock_database):
"""Test that indexes are created when repository is initialized."""
# Mock create_index to not raise exception
mock_database.users.create_index.return_value = None
repository = UserRepository(mock_database)
mock_database.users.create_index.assert_called_once_with("username", unique=True)
def test_i_can_handle_index_creation_error(mock_database):
"""Test that index creation errors are handled gracefully."""
# Mock create_index to raise exception (index already exists)
mock_database.users.create_index.side_effect = Exception("Index already exists")
# Should not raise exception
repository = UserRepository(mock_database)
assert repository is not None
mock_database.users.create_index.assert_called_once_with("username", unique=True)