Add comprehensive test coverage for email service, password reset endpoints, and soft delete functionality
- Introduced unit tests for `EmailService` covering `ConsoleEmailBackend` and `SMTPEmailBackend`. - Added tests for password reset request and confirmation endpoints, including edge cases and error handling. - Implemented soft delete CRUD tests to validate `deleted_at` behavior and data exclusion in queries. - Enhanced API tests for email functionality and user management workflows.
This commit is contained in:
448
backend/tests/crud/test_crud_base.py
Normal file
448
backend/tests/crud/test_crud_base.py
Normal file
@@ -0,0 +1,448 @@
|
||||
# tests/crud/test_crud_base.py
|
||||
"""
|
||||
Tests for CRUD base operations.
|
||||
"""
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
|
||||
from app.models.user import User
|
||||
from app.crud.user import user as user_crud
|
||||
from app.schemas.users import UserCreate, UserUpdate
|
||||
|
||||
|
||||
class TestCRUDGet:
|
||||
"""Tests for CRUD get operations."""
|
||||
|
||||
def test_get_by_valid_uuid(self, db_session):
|
||||
"""Test getting a record by valid UUID."""
|
||||
user = User(
|
||||
email="get_uuid@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Get",
|
||||
last_name="UUID",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
retrieved = user_crud.get(db_session, id=user.id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.id == user.id
|
||||
assert retrieved.email == user.email
|
||||
|
||||
def test_get_by_string_uuid(self, db_session):
|
||||
"""Test getting a record by UUID string."""
|
||||
user = User(
|
||||
email="get_string@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Get",
|
||||
last_name="String",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
retrieved = user_crud.get(db_session, id=str(user.id))
|
||||
assert retrieved is not None
|
||||
assert retrieved.id == user.id
|
||||
|
||||
def test_get_nonexistent(self, db_session):
|
||||
"""Test getting a non-existent record."""
|
||||
fake_id = uuid4()
|
||||
result = user_crud.get(db_session, id=fake_id)
|
||||
assert result is None
|
||||
|
||||
def test_get_invalid_uuid(self, db_session):
|
||||
"""Test getting with invalid UUID format."""
|
||||
result = user_crud.get(db_session, id="not-a-uuid")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestCRUDGetMulti:
|
||||
"""Tests for get_multi operations."""
|
||||
|
||||
def test_get_multi_basic(self, db_session):
|
||||
"""Test basic get_multi functionality."""
|
||||
# Create multiple users
|
||||
users = [
|
||||
User(email=f"multi{i}@example.com", password_hash="hash", first_name=f"User{i}",
|
||||
is_active=True, is_superuser=False)
|
||||
for i in range(5)
|
||||
]
|
||||
db_session.add_all(users)
|
||||
db_session.commit()
|
||||
|
||||
results = user_crud.get_multi(db_session, skip=0, limit=10)
|
||||
assert len(results) >= 5
|
||||
|
||||
def test_get_multi_pagination(self, db_session):
|
||||
"""Test pagination with get_multi."""
|
||||
# Create users
|
||||
users = [
|
||||
User(email=f"page{i}@example.com", password_hash="hash", first_name=f"Page{i}",
|
||||
is_active=True, is_superuser=False)
|
||||
for i in range(10)
|
||||
]
|
||||
db_session.add_all(users)
|
||||
db_session.commit()
|
||||
|
||||
# First page
|
||||
page1 = user_crud.get_multi(db_session, skip=0, limit=3)
|
||||
assert len(page1) == 3
|
||||
|
||||
# Second page
|
||||
page2 = user_crud.get_multi(db_session, skip=3, limit=3)
|
||||
assert len(page2) == 3
|
||||
|
||||
# Pages should have different users
|
||||
page1_ids = {u.id for u in page1}
|
||||
page2_ids = {u.id for u in page2}
|
||||
assert len(page1_ids.intersection(page2_ids)) == 0
|
||||
|
||||
def test_get_multi_negative_skip(self, db_session):
|
||||
"""Test that negative skip raises ValueError."""
|
||||
with pytest.raises(ValueError, match="skip must be non-negative"):
|
||||
user_crud.get_multi(db_session, skip=-1, limit=10)
|
||||
|
||||
def test_get_multi_negative_limit(self, db_session):
|
||||
"""Test that negative limit raises ValueError."""
|
||||
with pytest.raises(ValueError, match="limit must be non-negative"):
|
||||
user_crud.get_multi(db_session, skip=0, limit=-1)
|
||||
|
||||
def test_get_multi_limit_too_large(self, db_session):
|
||||
"""Test that limit over 1000 raises ValueError."""
|
||||
with pytest.raises(ValueError, match="Maximum limit is 1000"):
|
||||
user_crud.get_multi(db_session, skip=0, limit=1001)
|
||||
|
||||
|
||||
class TestCRUDGetMultiWithTotal:
|
||||
"""Tests for get_multi_with_total operations."""
|
||||
|
||||
def test_get_multi_with_total_basic(self, db_session):
|
||||
"""Test basic get_multi_with_total functionality."""
|
||||
# Create users
|
||||
users = [
|
||||
User(email=f"total{i}@example.com", password_hash="hash", first_name=f"Total{i}",
|
||||
is_active=True, is_superuser=False)
|
||||
for i in range(7)
|
||||
]
|
||||
db_session.add_all(users)
|
||||
db_session.commit()
|
||||
|
||||
results, total = user_crud.get_multi_with_total(db_session, skip=0, limit=10)
|
||||
assert total >= 7
|
||||
assert len(results) >= 7
|
||||
|
||||
def test_get_multi_with_total_pagination(self, db_session):
|
||||
"""Test pagination returns correct total."""
|
||||
# Create users
|
||||
users = [
|
||||
User(email=f"pagetotal{i}@example.com", password_hash="hash", first_name=f"PageTotal{i}",
|
||||
is_active=True, is_superuser=False)
|
||||
for i in range(15)
|
||||
]
|
||||
db_session.add_all(users)
|
||||
db_session.commit()
|
||||
|
||||
# First page
|
||||
page1, total1 = user_crud.get_multi_with_total(db_session, skip=0, limit=5)
|
||||
assert len(page1) == 5
|
||||
assert total1 >= 15
|
||||
|
||||
# Second page should have same total
|
||||
page2, total2 = user_crud.get_multi_with_total(db_session, skip=5, limit=5)
|
||||
assert len(page2) == 5
|
||||
assert total2 == total1
|
||||
|
||||
def test_get_multi_with_total_sorting_asc(self, db_session):
|
||||
"""Test sorting in ascending order."""
|
||||
# Create users
|
||||
users = [
|
||||
User(email=f"sort{i}@example.com", password_hash="hash", first_name=f"User{chr(90-i)}",
|
||||
is_active=True, is_superuser=False)
|
||||
for i in range(5)
|
||||
]
|
||||
db_session.add_all(users)
|
||||
db_session.commit()
|
||||
|
||||
results, _ = user_crud.get_multi_with_total(
|
||||
db_session,
|
||||
sort_by="first_name",
|
||||
sort_order="asc"
|
||||
)
|
||||
|
||||
# Check that results are sorted
|
||||
first_names = [u.first_name for u in results if u.first_name.startswith("User")]
|
||||
assert first_names == sorted(first_names)
|
||||
|
||||
def test_get_multi_with_total_sorting_desc(self, db_session):
|
||||
"""Test sorting in descending order."""
|
||||
# Create users
|
||||
users = [
|
||||
User(email=f"desc{i}@example.com", password_hash="hash", first_name=f"User{chr(65+i)}",
|
||||
is_active=True, is_superuser=False)
|
||||
for i in range(5)
|
||||
]
|
||||
db_session.add_all(users)
|
||||
db_session.commit()
|
||||
|
||||
results, _ = user_crud.get_multi_with_total(
|
||||
db_session,
|
||||
sort_by="first_name",
|
||||
sort_order="desc"
|
||||
)
|
||||
|
||||
# Check that results are sorted descending
|
||||
first_names = [u.first_name for u in results if u.first_name.startswith("User")]
|
||||
assert first_names == sorted(first_names, reverse=True)
|
||||
|
||||
def test_get_multi_with_total_filtering(self, db_session):
|
||||
"""Test filtering with get_multi_with_total."""
|
||||
# Create active and inactive users
|
||||
active_user = User(
|
||||
email="active_filter@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Active",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
inactive_user = User(
|
||||
email="inactive_filter@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Inactive",
|
||||
is_active=False,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add_all([active_user, inactive_user])
|
||||
db_session.commit()
|
||||
|
||||
# Filter for active users only
|
||||
results, total = user_crud.get_multi_with_total(
|
||||
db_session,
|
||||
filters={"is_active": True}
|
||||
)
|
||||
|
||||
emails = [u.email for u in results]
|
||||
assert "active_filter@example.com" in emails
|
||||
assert "inactive_filter@example.com" not in emails
|
||||
|
||||
def test_get_multi_with_total_multiple_filters(self, db_session):
|
||||
"""Test multiple filters."""
|
||||
# Create users with different combinations
|
||||
user1 = User(
|
||||
email="multi1@example.com",
|
||||
password_hash="hash",
|
||||
first_name="User1",
|
||||
is_active=True,
|
||||
is_superuser=True
|
||||
)
|
||||
user2 = User(
|
||||
email="multi2@example.com",
|
||||
password_hash="hash",
|
||||
first_name="User2",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
user3 = User(
|
||||
email="multi3@example.com",
|
||||
password_hash="hash",
|
||||
first_name="User3",
|
||||
is_active=False,
|
||||
is_superuser=True
|
||||
)
|
||||
db_session.add_all([user1, user2, user3])
|
||||
db_session.commit()
|
||||
|
||||
# Filter for active superusers
|
||||
results, _ = user_crud.get_multi_with_total(
|
||||
db_session,
|
||||
filters={"is_active": True, "is_superuser": True}
|
||||
)
|
||||
|
||||
emails = [u.email for u in results]
|
||||
assert "multi1@example.com" in emails
|
||||
assert "multi2@example.com" not in emails
|
||||
assert "multi3@example.com" not in emails
|
||||
|
||||
def test_get_multi_with_total_nonexistent_sort_field(self, db_session):
|
||||
"""Test sorting by non-existent field is ignored."""
|
||||
results, _ = user_crud.get_multi_with_total(
|
||||
db_session,
|
||||
sort_by="nonexistent_field",
|
||||
sort_order="asc"
|
||||
)
|
||||
|
||||
# Should not raise an error, just ignore the invalid sort field
|
||||
assert results is not None
|
||||
|
||||
def test_get_multi_with_total_nonexistent_filter_field(self, db_session):
|
||||
"""Test filtering by non-existent field is ignored."""
|
||||
results, _ = user_crud.get_multi_with_total(
|
||||
db_session,
|
||||
filters={"nonexistent_field": "value"}
|
||||
)
|
||||
|
||||
# Should not raise an error, just ignore the invalid filter
|
||||
assert results is not None
|
||||
|
||||
def test_get_multi_with_total_none_filter_values(self, db_session):
|
||||
"""Test that None filter values are ignored."""
|
||||
user = User(
|
||||
email="none_filter@example.com",
|
||||
password_hash="hash",
|
||||
first_name="None",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
|
||||
# Pass None as a filter value - should be ignored
|
||||
results, _ = user_crud.get_multi_with_total(
|
||||
db_session,
|
||||
filters={"is_active": None}
|
||||
)
|
||||
|
||||
# Should return all users (not filtered)
|
||||
assert len(results) >= 1
|
||||
|
||||
|
||||
class TestCRUDCreate:
|
||||
"""Tests for create operations."""
|
||||
|
||||
def test_create_basic(self, db_session):
|
||||
"""Test basic record creation."""
|
||||
user_data = UserCreate(
|
||||
email="create@example.com",
|
||||
password="Password123",
|
||||
first_name="Create",
|
||||
last_name="Test"
|
||||
)
|
||||
|
||||
created = user_crud.create(db_session, obj_in=user_data)
|
||||
|
||||
assert created.id is not None
|
||||
assert created.email == "create@example.com"
|
||||
assert created.first_name == "Create"
|
||||
|
||||
def test_create_duplicate_email(self, db_session):
|
||||
"""Test that creating duplicate email raises error."""
|
||||
user_data = UserCreate(
|
||||
email="duplicate@example.com",
|
||||
password="Password123",
|
||||
first_name="First"
|
||||
)
|
||||
|
||||
# Create first user
|
||||
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)
|
||||
|
||||
|
||||
class TestCRUDUpdate:
|
||||
"""Tests for update operations."""
|
||||
|
||||
def test_update_basic(self, db_session):
|
||||
"""Test basic record update."""
|
||||
user = User(
|
||||
email="update@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_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.email == "update@example.com" # Unchanged
|
||||
|
||||
def test_update_with_dict(self, db_session):
|
||||
"""Test updating with dictionary."""
|
||||
user = User(
|
||||
email="updatedict@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_data = {"first_name": "DictUpdated", "last_name": "DictLast"}
|
||||
updated = user_crud.update(db_session, db_obj=user, obj_in=update_data)
|
||||
|
||||
assert updated.first_name == "DictUpdated"
|
||||
assert updated.last_name == "DictLast"
|
||||
|
||||
def test_update_partial(self, db_session):
|
||||
"""Test partial update (only some fields)."""
|
||||
user = User(
|
||||
email="partial@example.com",
|
||||
password_hash="hash",
|
||||
first_name="First",
|
||||
last_name="Last",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
# Only update last_name
|
||||
update_data = UserUpdate(last_name="NewLast")
|
||||
updated = user_crud.update(db_session, db_obj=user, obj_in=update_data)
|
||||
|
||||
assert updated.first_name == "First" # Unchanged
|
||||
assert updated.last_name == "NewLast" # Changed
|
||||
|
||||
|
||||
class TestCRUDRemove:
|
||||
"""Tests for remove (hard delete) operations."""
|
||||
|
||||
def test_remove_basic(self, db_session):
|
||||
"""Test basic record removal."""
|
||||
user = User(
|
||||
email="remove@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)
|
||||
|
||||
user_id = user.id
|
||||
|
||||
# Remove the user
|
||||
removed = user_crud.remove(db_session, id=user_id)
|
||||
|
||||
assert removed is not None
|
||||
assert removed.id == user_id
|
||||
|
||||
# User should no longer exist
|
||||
retrieved = user_crud.get(db_session, id=user_id)
|
||||
assert retrieved is None
|
||||
|
||||
def test_remove_nonexistent(self, db_session):
|
||||
"""Test removing non-existent record."""
|
||||
fake_id = uuid4()
|
||||
result = user_crud.remove(db_session, id=fake_id)
|
||||
assert result is None
|
||||
|
||||
def test_remove_invalid_uuid(self, db_session):
|
||||
"""Test removing with invalid UUID."""
|
||||
result = user_crud.remove(db_session, id="not-a-uuid")
|
||||
assert result is None
|
||||
324
backend/tests/crud/test_soft_delete.py
Normal file
324
backend/tests/crud/test_soft_delete.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# tests/crud/test_soft_delete.py
|
||||
"""
|
||||
Tests for soft delete functionality in CRUD operations.
|
||||
"""
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.models.user import User
|
||||
from app.crud.user import user as user_crud
|
||||
|
||||
|
||||
class TestSoftDelete:
|
||||
"""Tests for soft delete functionality."""
|
||||
|
||||
def test_soft_delete_marks_deleted_at(self, db_session):
|
||||
"""Test that soft delete sets deleted_at timestamp."""
|
||||
# Create a user
|
||||
test_user = User(
|
||||
email="softdelete@example.com",
|
||||
password_hash="hashedpassword",
|
||||
first_name="Soft",
|
||||
last_name="Delete",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(test_user)
|
||||
db_session.commit()
|
||||
db_session.refresh(test_user)
|
||||
|
||||
user_id = test_user.id
|
||||
assert test_user.deleted_at is None
|
||||
|
||||
# Soft delete the user
|
||||
deleted_user = user_crud.soft_delete(db_session, id=user_id)
|
||||
|
||||
assert deleted_user is not None
|
||||
assert deleted_user.deleted_at is not None
|
||||
assert isinstance(deleted_user.deleted_at, datetime)
|
||||
|
||||
def test_soft_delete_excludes_from_get_multi(self, db_session):
|
||||
"""Test that soft deleted records are excluded from get_multi."""
|
||||
# Create two users
|
||||
user1 = User(
|
||||
email="user1@example.com",
|
||||
password_hash="hash1",
|
||||
first_name="User",
|
||||
last_name="One",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
user2 = User(
|
||||
email="user2@example.com",
|
||||
password_hash="hash2",
|
||||
first_name="User",
|
||||
last_name="Two",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add_all([user1, user2])
|
||||
db_session.commit()
|
||||
db_session.refresh(user1)
|
||||
db_session.refresh(user2)
|
||||
|
||||
# Both users should be returned
|
||||
users, total = user_crud.get_multi_with_total(db_session)
|
||||
assert total >= 2
|
||||
user_emails = [u.email for u in users]
|
||||
assert "user1@example.com" in user_emails
|
||||
assert "user2@example.com" in user_emails
|
||||
|
||||
# Soft delete user1
|
||||
user_crud.soft_delete(db_session, id=user1.id)
|
||||
|
||||
# Only user2 should be returned
|
||||
users, total = user_crud.get_multi_with_total(db_session)
|
||||
user_emails = [u.email for u in users]
|
||||
assert "user1@example.com" not in user_emails
|
||||
assert "user2@example.com" in user_emails
|
||||
|
||||
def test_soft_delete_still_retrievable_by_get(self, db_session):
|
||||
"""Test that soft deleted records can still be retrieved by get() method."""
|
||||
# Create a user
|
||||
user = User(
|
||||
email="gettest@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Get",
|
||||
last_name="Test",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
user_id = user.id
|
||||
|
||||
# User should be retrievable
|
||||
retrieved = user_crud.get(db_session, id=user_id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.email == "gettest@example.com"
|
||||
assert retrieved.deleted_at is None
|
||||
|
||||
# Soft delete the user
|
||||
user_crud.soft_delete(db_session, id=user_id)
|
||||
|
||||
# User should still be retrievable by ID (soft delete doesn't prevent direct access)
|
||||
retrieved = user_crud.get(db_session, id=user_id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.deleted_at is not None
|
||||
|
||||
def test_soft_delete_nonexistent_record(self, db_session):
|
||||
"""Test soft deleting a record that doesn't exist."""
|
||||
import uuid
|
||||
fake_id = uuid.uuid4()
|
||||
|
||||
result = user_crud.soft_delete(db_session, id=fake_id)
|
||||
assert result is None
|
||||
|
||||
def test_restore_sets_deleted_at_to_none(self, db_session):
|
||||
"""Test that restore clears the deleted_at timestamp."""
|
||||
# Create and soft delete a user
|
||||
user = User(
|
||||
email="restore@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Restore",
|
||||
last_name="Test",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
user_id = user.id
|
||||
|
||||
# Soft delete
|
||||
user_crud.soft_delete(db_session, id=user_id)
|
||||
db_session.refresh(user)
|
||||
assert user.deleted_at is not None
|
||||
|
||||
# Restore
|
||||
restored_user = user_crud.restore(db_session, id=user_id)
|
||||
|
||||
assert restored_user is not None
|
||||
assert restored_user.deleted_at is None
|
||||
|
||||
def test_restore_makes_record_available(self, db_session):
|
||||
"""Test that restored records appear in queries."""
|
||||
# Create and soft delete a user
|
||||
user = User(
|
||||
email="available@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Available",
|
||||
last_name="Test",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
user_id = user.id
|
||||
user_email = user.email
|
||||
|
||||
# Soft delete
|
||||
user_crud.soft_delete(db_session, id=user_id)
|
||||
|
||||
# User should not be in query results
|
||||
users, _ = user_crud.get_multi_with_total(db_session)
|
||||
emails = [u.email for u in users]
|
||||
assert user_email not in emails
|
||||
|
||||
# Restore
|
||||
user_crud.restore(db_session, id=user_id)
|
||||
|
||||
# User should now be in query results
|
||||
users, _ = user_crud.get_multi_with_total(db_session)
|
||||
emails = [u.email for u in users]
|
||||
assert user_email in emails
|
||||
|
||||
def test_restore_nonexistent_record(self, db_session):
|
||||
"""Test restoring a record that doesn't exist."""
|
||||
import uuid
|
||||
fake_id = uuid.uuid4()
|
||||
|
||||
result = user_crud.restore(db_session, id=fake_id)
|
||||
assert result is None
|
||||
|
||||
def test_restore_already_active_record(self, db_session):
|
||||
"""Test restoring a record that was never deleted returns None."""
|
||||
# Create a user (not deleted)
|
||||
user = User(
|
||||
email="never_deleted@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Never",
|
||||
last_name="Deleted",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
user_id = user.id
|
||||
assert user.deleted_at is None
|
||||
|
||||
# Restore should return None (record is not soft-deleted)
|
||||
restored = user_crud.restore(db_session, id=user_id)
|
||||
assert restored is None
|
||||
|
||||
def test_soft_delete_multiple_times(self, db_session):
|
||||
"""Test soft deleting the same record multiple times."""
|
||||
# Create a user
|
||||
user = User(
|
||||
email="multiple_delete@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Multiple",
|
||||
last_name="Delete",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
user_id = user.id
|
||||
|
||||
# First soft delete
|
||||
first_deleted = user_crud.soft_delete(db_session, id=user_id)
|
||||
assert first_deleted is not None
|
||||
first_timestamp = first_deleted.deleted_at
|
||||
|
||||
# Restore
|
||||
user_crud.restore(db_session, id=user_id)
|
||||
|
||||
# Second soft delete
|
||||
second_deleted = user_crud.soft_delete(db_session, id=user_id)
|
||||
assert second_deleted is not None
|
||||
second_timestamp = second_deleted.deleted_at
|
||||
|
||||
# Timestamps should be different
|
||||
assert second_timestamp != first_timestamp
|
||||
assert second_timestamp > first_timestamp
|
||||
|
||||
def test_get_multi_with_filters_excludes_deleted(self, db_session):
|
||||
"""Test that get_multi_with_total with filters excludes deleted records."""
|
||||
# Create active and inactive users
|
||||
active_user = User(
|
||||
email="active_not_deleted@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Active",
|
||||
last_name="NotDeleted",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
inactive_user = User(
|
||||
email="inactive_not_deleted@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Inactive",
|
||||
last_name="NotDeleted",
|
||||
is_active=False,
|
||||
is_superuser=False
|
||||
)
|
||||
deleted_active_user = User(
|
||||
email="active_deleted@example.com",
|
||||
password_hash="hash",
|
||||
first_name="Active",
|
||||
last_name="Deleted",
|
||||
is_active=True,
|
||||
is_superuser=False
|
||||
)
|
||||
|
||||
db_session.add_all([active_user, inactive_user, deleted_active_user])
|
||||
db_session.commit()
|
||||
db_session.refresh(deleted_active_user)
|
||||
|
||||
# Soft delete one active user
|
||||
user_crud.soft_delete(db_session, id=deleted_active_user.id)
|
||||
|
||||
# Filter for active users - should only return non-deleted active user
|
||||
users, total = user_crud.get_multi_with_total(
|
||||
db_session,
|
||||
filters={"is_active": True}
|
||||
)
|
||||
|
||||
emails = [u.email for u in users]
|
||||
assert "active_not_deleted@example.com" in emails
|
||||
assert "active_deleted@example.com" not in emails
|
||||
assert "inactive_not_deleted@example.com" not in emails
|
||||
|
||||
def test_soft_delete_preserves_other_fields(self, db_session):
|
||||
"""Test that soft delete doesn't modify other fields."""
|
||||
# Create a user with specific data
|
||||
user = User(
|
||||
email="preserve@example.com",
|
||||
password_hash="original_hash",
|
||||
first_name="Preserve",
|
||||
last_name="Fields",
|
||||
phone_number="+1234567890",
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
preferences={"theme": "dark"}
|
||||
)
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
db_session.refresh(user)
|
||||
|
||||
user_id = user.id
|
||||
original_email = user.email
|
||||
original_hash = user.password_hash
|
||||
original_first_name = user.first_name
|
||||
original_phone = user.phone_number
|
||||
original_preferences = user.preferences
|
||||
|
||||
# Soft delete
|
||||
deleted = user_crud.soft_delete(db_session, id=user_id)
|
||||
|
||||
# All other fields should remain unchanged
|
||||
assert deleted.email == original_email
|
||||
assert deleted.password_hash == original_hash
|
||||
assert deleted.first_name == original_first_name
|
||||
assert deleted.phone_number == original_phone
|
||||
assert deleted.preferences == original_preferences
|
||||
assert deleted.is_active is True # is_active unchanged
|
||||
Reference in New Issue
Block a user