Add UUID handling, sorting, filtering, and soft delete functionality to CRUD operations

- Enhanced UUID validation by supporting both string and `UUID` formats.
- Added advanced filtering and sorting capabilities to `get_multi_with_total` method.
- Introduced soft delete and restore functionality for models with `deleted_at` column.
- Updated tests to reflect new endpoints and rate-limiting logic.
- Improved schema definitions with `SortParams` and `SortOrder` for consistent API inputs.
This commit is contained in:
Felipe Cardoso
2025-10-30 16:44:15 +01:00
parent 2c600290a1
commit c684f2ba95
5 changed files with 200 additions and 56 deletions

View File

@@ -10,6 +10,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.api.routes.auth import router as auth_router
from app.api.routes.users import router as users_router
from app.core.auth import get_password_hash
from app.core.database import get_db
from app.models.user import User
@@ -29,6 +30,7 @@ def app(override_get_db):
"""Create a FastAPI test application with overridden dependencies."""
app = FastAPI()
app.include_router(auth_router, prefix="/auth", tags=["auth"])
app.include_router(users_router, prefix="/api/v1/users", tags=["users"])
# Override the get_db dependency
app.dependency_overrides[get_db] = lambda: override_get_db
@@ -280,9 +282,9 @@ class TestChangePassword:
# Mock password change to return success
with patch.object(AuthService, 'change_password', return_value=True):
# Test request
response = client.post(
"/auth/change-password",
# Test request (new endpoint)
response = client.patch(
"/api/v1/users/me/password",
json={
"current_password": "OldPassword123",
"new_password": "NewPassword123"
@@ -291,7 +293,8 @@ class TestChangePassword:
# Assertions
assert response.status_code == 200
assert "success" in response.json()["message"].lower()
assert response.json()["success"] is True
assert "message" in response.json()
# Clean up override
if get_current_user in app.dependency_overrides:
@@ -312,18 +315,20 @@ class TestChangePassword:
# Mock password change to raise error
with patch.object(AuthService, 'change_password',
side_effect=AuthenticationError("Current password is incorrect")):
# Test request
response = client.post(
"/auth/change-password",
# Test request (new endpoint)
response = client.patch(
"/api/v1/users/me/password",
json={
"current_password": "WrongPassword",
"new_password": "NewPassword123"
}
)
# Assertions
assert response.status_code == 400
assert "incorrect" in response.json()["detail"].lower()
# Assertions - Now returns standardized error response
assert response.status_code == 403
# The response has standardized error format
data = response.json()
assert "detail" in data or "errors" in data
# Clean up override
if get_current_user in app.dependency_overrides:

View File

@@ -13,17 +13,10 @@ from app.core.database import get_db
@pytest.fixture
def client():
"""Create a FastAPI test client for the main app with mocked database."""
# Mock get_db to avoid connecting to the actual database
with patch("app.main.get_db") as mock_get_db:
def mock_session_generator():
mock_session = MagicMock()
# Mock the execute method to return successfully
mock_session.execute.return_value = None
mock_session.close.return_value = None
yield mock_session
# Return a new generator each time get_db is called
mock_get_db.side_effect = lambda: mock_session_generator()
# Mock check_database_health to avoid connecting to the actual database
with patch("app.main.check_database_health") as mock_health_check:
# By default, return True (healthy)
mock_health_check.return_value = True
yield TestClient(app)
@@ -90,23 +83,14 @@ class TestHealthEndpoint:
assert data["environment"] == settings.ENVIRONMENT
def test_health_check_database_connection_failure(self, client):
def test_health_check_database_connection_failure(self):
"""Test that health check returns unhealthy when database is not accessible"""
# Mock the database session to raise an exception
with patch("app.main.get_db") as mock_get_db:
def mock_session():
from unittest.mock import MagicMock
mock = MagicMock()
mock.execute.side_effect = OperationalError(
"Connection refused",
params=None,
orig=Exception("Connection refused")
)
yield mock
# Mock check_database_health to return False (unhealthy)
with patch("app.main.check_database_health") as mock_health_check:
mock_health_check.return_value = False
mock_get_db.return_value = mock_session()
response = client.get("/health")
test_client = TestClient(app)
response = test_client.get("/health")
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
data = response.json()

View File

@@ -5,6 +5,7 @@ from fastapi.testclient import TestClient
from unittest.mock import patch, MagicMock
from app.api.routes.auth import router as auth_router, limiter
from app.api.routes.users import router as users_router
from app.core.database import get_db
@@ -26,6 +27,7 @@ def app(override_get_db):
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.include_router(auth_router, prefix="/auth", tags=["auth"])
app.include_router(users_router, prefix="/api/v1/users", tags=["users"])
# Override the get_db dependency
app.dependency_overrides[get_db] = lambda: override_get_db
@@ -159,10 +161,10 @@ class TestChangePasswordRateLimiting:
"new_password": "NewPassword123!"
}
# Make 6 requests (limit is 5/minute)
# Make 6 requests (limit is 5/minute) - using new endpoint
responses = []
for i in range(6):
response = client.post("/auth/change-password", json=password_data)
response = client.patch("/api/v1/users/me/password", json=password_data)
responses.append(response)
# Last request should be rate limited