Files
fast-next-template/backend/tests/crud/test_crud_base.py
Felipe Cardoso defa33975f 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.
2025-10-30 17:18:25 +01:00

449 lines
14 KiB
Python

# 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