Files
fast-next-template/backend/tests/crud/test_crud_error_paths.py
Felipe Cardoso 3fe5d301f8 Refactor authentication services to async password handling; optimize bulk operations and queries
- Updated `verify_password` and `get_password_hash` to their async counterparts to prevent event loop blocking.
- Replaced N+1 query patterns in `admin.py` and `session_async.py` with optimized bulk operations for improved performance.
- Enhanced `user_async.py` with bulk update and soft delete methods for efficient user management.
- Added eager loading support in CRUD operations to prevent N+1 query issues.
- Updated test cases with stronger password examples for better security representation.
2025-11-01 03:53:22 +01:00

296 lines
10 KiB
Python
Executable File

# tests/crud/test_crud_error_paths.py
"""
Tests for CRUD error handling paths to increase coverage.
These tests focus on exception handling and edge cases.
"""
import pytest
from unittest.mock import patch, MagicMock
from sqlalchemy.exc import IntegrityError, OperationalError
from app.models.user import User
from app.crud.user import user as user_crud
from app.schemas.users import UserCreate, UserUpdate
class TestCRUDErrorPaths:
"""Tests for error handling in CRUD operations."""
def test_get_database_error(self, db_session):
"""Test get method handles database errors."""
import uuid
user_id = uuid.uuid4()
with patch.object(db_session, 'query') as mock_query:
mock_query.side_effect = OperationalError("statement", "params", "orig")
with pytest.raises(OperationalError):
user_crud.get(db_session, id=user_id)
def test_get_multi_database_error(self, db_session):
"""Test get_multi handles database errors."""
with patch.object(db_session, 'query') as mock_query:
mock_query.side_effect = OperationalError("statement", "params", "orig")
with pytest.raises(OperationalError):
user_crud.get_multi(db_session, skip=0, limit=10)
def test_create_integrity_error_non_unique(self, db_session):
"""Test create handles integrity errors for non-unique constraints."""
# Create first user
user_data = UserCreate(
email="unique@example.com",
password="Password123!",
first_name="First"
)
user_crud.create(db_session, obj_in=user_data)
# Try to create duplicate
with pytest.raises(ValueError, match="already exists"):
user_crud.create(db_session, obj_in=user_data)
def test_create_generic_integrity_error(self, db_session):
"""Test create handles other integrity errors."""
user_data = UserCreate(
email="integrityerror@example.com",
password="Password123!",
first_name="Integrity"
)
with patch('app.crud.base.jsonable_encoder') as mock_encoder:
mock_encoder.return_value = {"email": "test@example.com"}
with patch.object(db_session, 'add') as mock_add:
# Simulate a non-unique integrity error
error = IntegrityError("statement", "params", Exception("check constraint failed"))
mock_add.side_effect = error
with pytest.raises(ValueError):
user_crud.create(db_session, obj_in=user_data)
def test_create_unexpected_error(self, db_session):
"""Test create handles unexpected errors."""
user_data = UserCreate(
email="unexpectederror@example.com",
password="Password123!",
first_name="Unexpected"
)
with patch.object(db_session, 'commit') as mock_commit:
mock_commit.side_effect = Exception("Unexpected database error")
with pytest.raises(Exception):
user_crud.create(db_session, obj_in=user_data)
def test_update_integrity_error(self, db_session):
"""Test update handles integrity errors."""
# Create a user
user = User(
email="updateintegrity@example.com",
password_hash="hash",
first_name="Update",
is_active=True,
is_superuser=False
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
# Create another user with a different email
user2 = User(
email="another@example.com",
password_hash="hash",
first_name="Another",
is_active=True,
is_superuser=False
)
db_session.add(user2)
db_session.commit()
# Try to update user to have the same email as user2
with patch.object(db_session, 'commit') as mock_commit:
error = IntegrityError("statement", "params", Exception("UNIQUE constraint failed"))
mock_commit.side_effect = error
update_data = UserUpdate(email="another@example.com")
with pytest.raises(ValueError, match="already exists"):
user_crud.update(db_session, db_obj=user, obj_in=update_data)
def test_update_unexpected_error(self, db_session):
"""Test update handles unexpected errors."""
user = User(
email="updateunexpected@example.com",
password_hash="hash",
first_name="Update",
is_active=True,
is_superuser=False
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
with patch.object(db_session, 'commit') as mock_commit:
mock_commit.side_effect = Exception("Unexpected database error")
update_data = UserUpdate(first_name="Error")
with pytest.raises(Exception):
user_crud.update(db_session, db_obj=user, obj_in=update_data)
def test_remove_with_relationships(self, db_session):
"""Test remove handles cascade deletes."""
user = User(
email="removerelations@example.com",
password_hash="hash",
first_name="Remove",
is_active=True,
is_superuser=False
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
# Remove should succeed even with potential relationships
removed = user_crud.remove(db_session, id=user.id)
assert removed is not None
assert removed.id == user.id
def test_soft_delete_database_error(self, db_session):
"""Test soft_delete handles database errors."""
user = User(
email="softdeleteerror@example.com",
password_hash="hash",
first_name="SoftDelete",
is_active=True,
is_superuser=False
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
with patch.object(db_session, 'commit') as mock_commit:
mock_commit.side_effect = Exception("Database error")
with pytest.raises(Exception):
user_crud.soft_delete(db_session, id=user.id)
def test_restore_database_error(self, db_session):
"""Test restore handles database errors."""
user = User(
email="restoreerror@example.com",
password_hash="hash",
first_name="Restore",
is_active=True,
is_superuser=False
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
# First soft delete
user_crud.soft_delete(db_session, id=user.id)
# Then try to restore with error
with patch.object(db_session, 'commit') as mock_commit:
mock_commit.side_effect = Exception("Database error")
with pytest.raises(Exception):
user_crud.restore(db_session, id=user.id)
def test_get_multi_with_total_error_recovery(self, db_session):
"""Test get_multi_with_total handles errors gracefully."""
# Test that it doesn't crash on invalid sort fields
users, total = user_crud.get_multi_with_total(
db_session,
sort_by="nonexistent_field_xyz",
sort_order="asc"
)
# Should still return results, just ignore invalid sort
assert isinstance(users, list)
assert isinstance(total, int)
def test_update_with_model_dict(self, db_session):
"""Test update works with dict input."""
user = User(
email="updatedict2@example.com",
password_hash="hash",
first_name="Original",
is_active=True,
is_superuser=False
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
# Update with plain dict
update_data = {"first_name": "DictUpdated"}
updated = user_crud.update(db_session, db_obj=user, obj_in=update_data)
assert updated.first_name == "DictUpdated"
def test_update_preserves_unchanged_fields(self, db_session):
"""Test that update doesn't modify unspecified fields."""
user = User(
email="preserve@example.com",
password_hash="original_hash",
first_name="Original",
last_name="Name",
phone_number="+1234567890",
is_active=True,
is_superuser=False
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
original_password = user.password_hash
original_phone = user.phone_number
# Only update first_name
update_data = UserUpdate(first_name="Updated")
updated = user_crud.update(db_session, db_obj=user, obj_in=update_data)
assert updated.first_name == "Updated"
assert updated.password_hash == original_password # Unchanged
assert updated.phone_number == original_phone # Unchanged
assert updated.last_name == "Name" # Unchanged
class TestCRUDValidation:
"""Tests for validation in CRUD operations."""
def test_get_multi_with_empty_results(self, db_session):
"""Test get_multi with no results."""
# Query with filters that return no results
users, total = user_crud.get_multi_with_total(
db_session,
filters={"email": "nonexistent@example.com"}
)
assert users == []
assert total == 0
def test_get_multi_with_large_offset(self, db_session):
"""Test get_multi with offset larger than total records."""
users = user_crud.get_multi(db_session, skip=10000, limit=10)
assert users == []
def test_update_with_no_changes(self, db_session):
"""Test update when no fields are changed."""
user = User(
email="nochanges@example.com",
password_hash="hash",
first_name="NoChanges",
is_active=True,
is_superuser=False
)
db_session.add(user)
db_session.commit()
db_session.refresh(user)
# Update with empty dict
update_data = {}
updated = user_crud.update(db_session, db_obj=user, obj_in=update_data)
# Should still return the user, unchanged
assert updated.id == user.id
assert updated.first_name == "NoChanges"