Add extensive tests for handling CRUD and API error scenarios

- Introduced comprehensive tests for session CRUD error cases, covering exception handling, rollback mechanics, and database failure propagation.
- Added robust API error handling tests for admin routes, including user and organization management.
- Enhanced test coverage for unexpected errors, edge cases, and validation flows in session and admin operations.
This commit is contained in:
Felipe Cardoso
2025-11-01 13:12:36 +01:00
parent a95b25cab8
commit a9e972d583
5 changed files with 1467 additions and 0 deletions

View File

@@ -0,0 +1,546 @@
# tests/api/test_admin_error_handlers.py
"""
Tests for admin route exception handlers and error paths.
Focus on code coverage of error handling branches.
"""
import pytest
import pytest_asyncio
from unittest.mock import patch
from fastapi import status
from uuid import uuid4
@pytest_asyncio.fixture
async def superuser_token(client, async_test_superuser):
"""Get access token for superuser."""
response = await client.post(
"/api/v1/auth/login",
json={
"email": "superuser@example.com",
"password": "SuperPassword123!"
}
)
assert response.status_code == 200
return response.json()["access_token"]
# ===== USER MANAGEMENT ERROR TESTS =====
class TestAdminListUsersFilters:
"""Test admin list users with various filters."""
@pytest.mark.asyncio
async def test_list_users_with_is_superuser_filter(self, client, superuser_token):
"""Test listing users with is_superuser filter (covers line 96)."""
response = await client.get(
"/api/v1/admin/users?is_superuser=true",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "data" in data
@pytest.mark.asyncio
async def test_list_users_database_error_propagates(self, client, superuser_token):
"""Test that database errors propagate correctly (covers line 118-120)."""
with patch('app.api.routes.admin.user_crud.get_multi_with_total', side_effect=Exception("DB error")):
with pytest.raises(Exception):
await client.get(
"/api/v1/admin/users",
headers={"Authorization": f"Bearer {superuser_token}"}
)
class TestAdminCreateUserErrors:
"""Test admin create user error handling."""
@pytest.mark.asyncio
async def test_create_user_duplicate_email(self, client, async_test_user, superuser_token):
"""Test creating user with duplicate email (covers line 145-150)."""
response = await client.post(
"/api/v1/admin/users",
headers={"Authorization": f"Bearer {superuser_token}"},
json={
"email": async_test_user.email,
"password": "NewPassword123!",
"first_name": "Duplicate",
"last_name": "User"
}
)
# Should get error for duplicate email
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_create_user_unexpected_error_propagates(self, client, superuser_token):
"""Test unexpected errors during user creation (covers line 151-153)."""
with patch('app.api.routes.admin.user_crud.create', side_effect=RuntimeError("Unexpected error")):
with pytest.raises(RuntimeError):
await client.post(
"/api/v1/admin/users",
headers={"Authorization": f"Bearer {superuser_token}"},
json={
"email": "newerror@example.com",
"password": "NewPassword123!",
"first_name": "New",
"last_name": "User"
}
)
class TestAdminGetUserErrors:
"""Test admin get user error handling."""
@pytest.mark.asyncio
async def test_get_nonexistent_user(self, client, superuser_token):
"""Test getting a user that doesn't exist (covers line 170-175)."""
fake_id = uuid4()
response = await client.get(
f"/api/v1/admin/users/{fake_id}",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminUpdateUserErrors:
"""Test admin update user error handling."""
@pytest.mark.asyncio
async def test_update_nonexistent_user(self, client, superuser_token):
"""Test updating a user that doesn't exist (covers line 194-198)."""
fake_id = uuid4()
response = await client.put(
f"/api/v1/admin/users/{fake_id}",
headers={"Authorization": f"Bearer {superuser_token}"},
json={"first_name": "Updated"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_update_user_unexpected_error(self, client, async_test_user, superuser_token):
"""Test unexpected errors during user update (covers line 206-208)."""
with patch('app.api.routes.admin.user_crud.update', side_effect=RuntimeError("Update failed")):
with pytest.raises(RuntimeError):
await client.put(
f"/api/v1/admin/users/{async_test_user.id}",
headers={"Authorization": f"Bearer {superuser_token}"},
json={"first_name": "Updated"}
)
class TestAdminDeleteUserErrors:
"""Test admin delete user error handling."""
@pytest.mark.asyncio
async def test_delete_nonexistent_user(self, client, superuser_token):
"""Test deleting a user that doesn't exist (covers line 226-230)."""
fake_id = uuid4()
response = await client.delete(
f"/api/v1/admin/users/{fake_id}",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_delete_user_unexpected_error(self, client, async_test_user, superuser_token):
"""Test unexpected errors during user deletion (covers line 238-240)."""
with patch('app.api.routes.admin.user_crud.soft_delete', side_effect=Exception("Delete failed")):
with pytest.raises(Exception):
await client.delete(
f"/api/v1/admin/users/{async_test_user.id}",
headers={"Authorization": f"Bearer {superuser_token}"}
)
class TestAdminActivateUserErrors:
"""Test admin activate user error handling."""
@pytest.mark.asyncio
async def test_activate_nonexistent_user(self, client, superuser_token):
"""Test activating a user that doesn't exist (covers line 270-274)."""
fake_id = uuid4()
response = await client.post(
f"/api/v1/admin/users/{fake_id}/activate",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_activate_user_unexpected_error(self, client, async_test_user, superuser_token):
"""Test unexpected errors during user activation (covers line 282-284)."""
with patch('app.api.routes.admin.user_crud.update', side_effect=Exception("Activation failed")):
with pytest.raises(Exception):
await client.post(
f"/api/v1/admin/users/{async_test_user.id}/activate",
headers={"Authorization": f"Bearer {superuser_token}"}
)
class TestAdminDeactivateUserErrors:
"""Test admin deactivate user error handling."""
@pytest.mark.asyncio
async def test_deactivate_nonexistent_user(self, client, superuser_token):
"""Test deactivating a user that doesn't exist (covers line 306-310)."""
fake_id = uuid4()
response = await client.post(
f"/api/v1/admin/users/{fake_id}/deactivate",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_deactivate_self_forbidden(self, client, async_test_superuser, superuser_token):
"""Test that admin cannot deactivate themselves (covers line 319-323)."""
response = await client.post(
f"/api/v1/admin/users/{async_test_superuser.id}/deactivate",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@pytest.mark.asyncio
async def test_deactivate_user_unexpected_error(self, client, async_test_user, superuser_token):
"""Test unexpected errors during user deactivation (covers line 326-328)."""
with patch('app.api.routes.admin.user_crud.update', side_effect=Exception("Deactivation failed")):
with pytest.raises(Exception):
await client.post(
f"/api/v1/admin/users/{async_test_user.id}/deactivate",
headers={"Authorization": f"Bearer {superuser_token}"}
)
# ===== ORGANIZATION MANAGEMENT ERROR TESTS =====
class TestAdminListOrganizationsErrors:
"""Test admin list organizations error handling."""
@pytest.mark.asyncio
async def test_list_organizations_database_error(self, client, superuser_token):
"""Test list organizations with database error (covers line 427-456)."""
with patch('app.api.routes.admin.organization_crud.get_multi_with_member_counts', side_effect=Exception("DB error")):
with pytest.raises(Exception):
await client.get(
"/api/v1/admin/organizations",
headers={"Authorization": f"Bearer {superuser_token}"}
)
class TestAdminCreateOrganizationErrors:
"""Test admin create organization error handling."""
@pytest.mark.asyncio
async def test_create_organization_duplicate_slug(self, client, async_test_db, superuser_token):
"""Test creating organization with duplicate slug (covers line 480-483)."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create an organization first
async with AsyncTestingSessionLocal() as session:
from app.models.organization import Organization
org = Organization(
name="Existing Org",
slug="existing-org",
description="Test org"
)
session.add(org)
await session.commit()
# Try to create another with same slug
response = await client.post(
"/api/v1/admin/organizations",
headers={"Authorization": f"Bearer {superuser_token}"},
json={
"name": "New Org",
"slug": "existing-org",
"description": "Duplicate slug"
}
)
# Should get error for duplicate slug
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_create_organization_unexpected_error(self, client, superuser_token):
"""Test unexpected errors during organization creation (covers line 484-485)."""
with patch('app.api.routes.admin.organization_crud.create', side_effect=RuntimeError("Creation failed")):
with pytest.raises(RuntimeError):
await client.post(
"/api/v1/admin/organizations",
headers={"Authorization": f"Bearer {superuser_token}"},
json={
"name": "New Org",
"slug": "new-org",
"description": "Test"
}
)
class TestAdminGetOrganizationErrors:
"""Test admin get organization error handling."""
@pytest.mark.asyncio
async def test_get_nonexistent_organization(self, client, superuser_token):
"""Test getting an organization that doesn't exist (covers line 516-520)."""
fake_id = uuid4()
response = await client.get(
f"/api/v1/admin/organizations/{fake_id}",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminUpdateOrganizationErrors:
"""Test admin update organization error handling."""
@pytest.mark.asyncio
async def test_update_nonexistent_organization(self, client, superuser_token):
"""Test updating an organization that doesn't exist (covers line 552-556)."""
fake_id = uuid4()
response = await client.put(
f"/api/v1/admin/organizations/{fake_id}",
headers={"Authorization": f"Bearer {superuser_token}"},
json={"name": "Updated Org"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_update_organization_unexpected_error(self, client, async_test_db, superuser_token):
"""Test unexpected errors during organization update (covers line 573-575)."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create an organization
async with AsyncTestingSessionLocal() as session:
from app.models.organization import Organization
org = Organization(
name="Test Org",
slug="test-org-update-error",
description="Test"
)
session.add(org)
await session.commit()
await session.refresh(org)
org_id = org.id
with patch('app.api.routes.admin.organization_crud.update', side_effect=Exception("Update failed")):
with pytest.raises(Exception):
await client.put(
f"/api/v1/admin/organizations/{org_id}",
headers={"Authorization": f"Bearer {superuser_token}"},
json={"name": "Updated"}
)
class TestAdminDeleteOrganizationErrors:
"""Test admin delete organization error handling."""
@pytest.mark.asyncio
async def test_delete_nonexistent_organization(self, client, superuser_token):
"""Test deleting an organization that doesn't exist (covers line 596-600)."""
fake_id = uuid4()
response = await client.delete(
f"/api/v1/admin/organizations/{fake_id}",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_delete_organization_unexpected_error(self, client, async_test_db, superuser_token):
"""Test unexpected errors during organization deletion (covers line 611-613)."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create organization
async with AsyncTestingSessionLocal() as session:
from app.models.organization import Organization
org = Organization(
name="Error Org",
slug="error-org-delete",
description="Test"
)
session.add(org)
await session.commit()
await session.refresh(org)
org_id = org.id
with patch('app.api.routes.admin.organization_crud.remove', side_effect=Exception("Delete failed")):
with pytest.raises(Exception):
await client.delete(
f"/api/v1/admin/organizations/{org_id}",
headers={"Authorization": f"Bearer {superuser_token}"}
)
class TestAdminListOrganizationMembersErrors:
"""Test admin list organization members error handling."""
@pytest.mark.asyncio
async def test_list_members_nonexistent_organization(self, client, superuser_token):
"""Test listing members of non-existent organization (covers line 634-638)."""
fake_id = uuid4()
response = await client.get(
f"/api/v1/admin/organizations/{fake_id}/members",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_list_members_database_error(self, client, async_test_db, superuser_token):
"""Test database errors during member listing (covers line 660-662)."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create organization
async with AsyncTestingSessionLocal() as session:
from app.models.organization import Organization
org = Organization(
name="Members Error Org",
slug="members-error-org",
description="Test"
)
session.add(org)
await session.commit()
await session.refresh(org)
org_id = org.id
with patch('app.api.routes.admin.organization_crud.get_organization_members', side_effect=Exception("DB error")):
with pytest.raises(Exception):
await client.get(
f"/api/v1/admin/organizations/{org_id}/members",
headers={"Authorization": f"Bearer {superuser_token}"}
)
class TestAdminAddOrganizationMemberErrors:
"""Test admin add organization member error handling."""
@pytest.mark.asyncio
async def test_add_member_nonexistent_organization(self, client, async_test_user, superuser_token):
"""Test adding member to non-existent organization (covers line 689-693)."""
fake_id = uuid4()
response = await client.post(
f"/api/v1/admin/organizations/{fake_id}/members",
headers={"Authorization": f"Bearer {superuser_token}"},
json={
"user_id": str(async_test_user.id),
"role": "member"
}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_add_nonexistent_user_to_organization(self, client, async_test_db, superuser_token):
"""Test adding non-existent user to organization (covers line 696-700)."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create organization
async with AsyncTestingSessionLocal() as session:
from app.models.organization import Organization
org = Organization(
name="Add Member Org",
slug="add-member-org",
description="Test"
)
session.add(org)
await session.commit()
await session.refresh(org)
org_id = org.id
fake_user_id = uuid4()
response = await client.post(
f"/api/v1/admin/organizations/{org_id}/members",
headers={"Authorization": f"Bearer {superuser_token}"},
json={
"user_id": str(fake_user_id),
"role": "member"
}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_add_member_unexpected_error(self, client, async_test_db, async_test_user, superuser_token):
"""Test unexpected errors during member addition (covers line 727-729)."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create organization
async with AsyncTestingSessionLocal() as session:
from app.models.organization import Organization
org = Organization(
name="Error Add Org",
slug="error-add-org",
description="Test"
)
session.add(org)
await session.commit()
await session.refresh(org)
org_id = org.id
with patch('app.api.routes.admin.organization_crud.add_user', side_effect=Exception("Add failed")):
with pytest.raises(Exception):
await client.post(
f"/api/v1/admin/organizations/{org_id}/members",
headers={"Authorization": f"Bearer {superuser_token}"},
json={
"user_id": str(async_test_user.id),
"role": "member"
}
)
class TestAdminRemoveOrganizationMemberErrors:
"""Test admin remove organization member error handling."""
@pytest.mark.asyncio
async def test_remove_member_nonexistent_organization(self, client, async_test_user, superuser_token):
"""Test removing member from non-existent organization (covers line 750-754)."""
fake_id = uuid4()
response = await client.delete(
f"/api/v1/admin/organizations/{fake_id}/members/{async_test_user.id}",
headers={"Authorization": f"Bearer {superuser_token}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_remove_member_unexpected_error(self, client, async_test_db, async_test_user, superuser_token):
"""Test unexpected errors during member removal (covers line 780-782)."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create organization with member
async with AsyncTestingSessionLocal() as session:
from app.models.organization import Organization
from app.models.user_organization import UserOrganization, OrganizationRole
org = Organization(
name="Remove Member Org",
slug="remove-member-org",
description="Test"
)
session.add(org)
await session.commit()
await session.refresh(org)
member = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.MEMBER
)
session.add(member)
await session.commit()
org_id = org.id
with patch('app.api.routes.admin.organization_crud.remove_user', side_effect=Exception("Remove failed")):
with pytest.raises(Exception):
await client.delete(
f"/api/v1/admin/organizations/{org_id}/members/{async_test_user.id}",
headers={"Authorization": f"Bearer {superuser_token}"}
)

View File

@@ -0,0 +1,216 @@
# tests/api/test_auth_error_handlers.py
"""
Tests for auth route exception handlers and error paths.
"""
import pytest
from unittest.mock import patch, AsyncMock
from fastapi import status
class TestLoginSessionCreationFailure:
"""Test login when session creation fails."""
@pytest.mark.asyncio
async def test_login_succeeds_despite_session_creation_failure(self, client, async_test_user):
"""Test that login succeeds even if session creation fails."""
# Mock session creation to fail
with patch('app.api.routes.auth.session_crud.create_session', side_effect=Exception("Session creation failed")):
response = await client.post(
"/api/v1/auth/login",
json={
"email": "testuser@example.com",
"password": "TestPassword123!"
}
)
# Login should still succeed, just without session record
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "access_token" in data
assert "refresh_token" in data
class TestOAuthLoginSessionCreationFailure:
"""Test OAuth login when session creation fails."""
@pytest.mark.asyncio
async def test_oauth_login_succeeds_despite_session_failure(self, client, async_test_user):
"""Test OAuth login succeeds even if session creation fails."""
with patch('app.api.routes.auth.session_crud.create_session', side_effect=Exception("Session failed")):
response = await client.post(
"/api/v1/auth/login/oauth",
data={
"username": "testuser@example.com",
"password": "TestPassword123!"
}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "access_token" in data
class TestRefreshTokenSessionUpdateFailure:
"""Test refresh token when session update fails."""
@pytest.mark.asyncio
async def test_refresh_token_succeeds_despite_session_update_failure(self, client, async_test_user):
"""Test that token refresh succeeds even if session update fails."""
# First login to get tokens
response = await client.post(
"/api/v1/auth/login",
json={
"email": "testuser@example.com",
"password": "TestPassword123!"
}
)
tokens = response.json()
# Mock session update to fail
with patch('app.api.routes.auth.session_crud.update_refresh_token', side_effect=Exception("Update failed")):
response = await client.post(
"/api/v1/auth/refresh",
json={"refresh_token": tokens["refresh_token"]}
)
# Should still succeed - tokens are issued before update
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "access_token" in data
class TestLogoutWithExpiredToken:
"""Test logout with expired/invalid token."""
@pytest.mark.asyncio
async def test_logout_with_invalid_token_still_succeeds(self, client, async_test_user):
"""Test logout succeeds even with invalid refresh token."""
# Login first
response = await client.post(
"/api/v1/auth/login",
json={
"email": "testuser@example.com",
"password": "TestPassword123!"
}
)
access_token = response.json()["access_token"]
# Try logout with invalid refresh token
response = await client.post(
"/api/v1/auth/logout",
headers={"Authorization": f"Bearer {access_token}"},
json={"refresh_token": "invalid.token.here"}
)
# Should succeed (idempotent)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True
class TestLogoutWithNonExistentSession:
"""Test logout when session doesn't exist."""
@pytest.mark.asyncio
async def test_logout_with_no_session_succeeds(self, client, async_test_user):
"""Test logout succeeds even if session not found."""
response = await client.post(
"/api/v1/auth/login",
json={
"email": "testuser@example.com",
"password": "TestPassword123!"
}
)
tokens = response.json()
# Mock session lookup to return None
with patch('app.api.routes.auth.session_crud.get_by_jti', return_value=None):
response = await client.post(
"/api/v1/auth/logout",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"refresh_token": tokens["refresh_token"]}
)
# Should succeed (idempotent)
assert response.status_code == status.HTTP_200_OK
class TestLogoutUnexpectedError:
"""Test logout with unexpected errors."""
@pytest.mark.asyncio
async def test_logout_with_unexpected_error_returns_success(self, client, async_test_user):
"""Test logout returns success even on unexpected errors."""
response = await client.post(
"/api/v1/auth/login",
json={
"email": "testuser@example.com",
"password": "TestPassword123!"
}
)
tokens = response.json()
# Mock to raise unexpected error
with patch('app.api.routes.auth.session_crud.get_by_jti', side_effect=Exception("Unexpected error")):
response = await client.post(
"/api/v1/auth/logout",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"refresh_token": tokens["refresh_token"]}
)
# Should still return success (don't expose errors)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True
class TestLogoutAllUnexpectedError:
"""Test logout-all with unexpected errors."""
@pytest.mark.asyncio
async def test_logout_all_database_error(self, client, async_test_user):
"""Test logout-all handles database errors."""
response = await client.post(
"/api/v1/auth/login",
json={
"email": "testuser@example.com",
"password": "TestPassword123!"
}
)
access_token = response.json()["access_token"]
# Mock to raise database error
with patch('app.api.routes.auth.session_crud.deactivate_all_user_sessions', side_effect=Exception("DB error")):
response = await client.post(
"/api/v1/auth/logout-all",
headers={"Authorization": f"Bearer {access_token}"}
)
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
class TestPasswordResetConfirmSessionInvalidation:
"""Test password reset invalidates sessions."""
@pytest.mark.asyncio
async def test_password_reset_continues_despite_session_invalidation_failure(self, client, async_test_user):
"""Test password reset succeeds even if session invalidation fails."""
# Create a valid password reset token
from app.utils.security import create_password_reset_token
token = create_password_reset_token(async_test_user.email)
# Mock session invalidation to fail
with patch('app.api.routes.auth.session_crud.deactivate_all_user_sessions', side_effect=Exception("Invalidation failed")):
response = await client.post(
"/api/v1/auth/password-reset/confirm",
json={
"token": token,
"new_password": "NewPassword123!"
}
)
# Should still succeed - password was reset
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True

View File

@@ -757,3 +757,79 @@ class TestCRUDBaseRestore:
restored = await user_crud.restore(session, id=user_id) # UUID object
assert restored is not None
assert restored.deleted_at is None
class TestCRUDBasePaginationValidation:
"""Tests for pagination parameter validation (covers lines 254-260)."""
@pytest.mark.asyncio
async def test_get_multi_with_total_negative_skip(self, async_test_db):
"""Test that negative skip raises ValueError."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
with pytest.raises(ValueError, match="skip must be non-negative"):
await user_crud.get_multi_with_total(session, skip=-1, limit=10)
@pytest.mark.asyncio
async def test_get_multi_with_total_negative_limit(self, async_test_db):
"""Test that negative limit raises ValueError."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
with pytest.raises(ValueError, match="limit must be non-negative"):
await user_crud.get_multi_with_total(session, skip=0, limit=-1)
@pytest.mark.asyncio
async def test_get_multi_with_total_limit_too_large(self, async_test_db):
"""Test that limit > 1000 raises ValueError."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
with pytest.raises(ValueError, match="Maximum limit is 1000"):
await user_crud.get_multi_with_total(session, skip=0, limit=1001)
@pytest.mark.asyncio
async def test_get_multi_with_total_with_filters(self, async_test_db, async_test_user):
"""Test pagination with filters (covers lines 270-273)."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
users, total = await user_crud.get_multi_with_total(
session,
skip=0,
limit=10,
filters={"is_active": True}
)
assert isinstance(users, list)
assert total >= 0
@pytest.mark.asyncio
async def test_get_multi_with_total_with_sorting_desc(self, async_test_db):
"""Test pagination with descending sort (covers lines 283-284)."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
users, total = await user_crud.get_multi_with_total(
session,
skip=0,
limit=10,
sort_by="created_at",
sort_order="desc"
)
assert isinstance(users, list)
@pytest.mark.asyncio
async def test_get_multi_with_total_with_sorting_asc(self, async_test_db):
"""Test pagination with ascending sort (covers lines 285-286)."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
users, total = await user_crud.get_multi_with_total(
session,
skip=0,
limit=10,
sort_by="created_at",
sort_order="asc"
)
assert isinstance(users, list)

View File

@@ -0,0 +1,293 @@
# tests/crud/test_base_db_failures.py
"""
Comprehensive tests for base CRUD database failure scenarios.
Tests exception handling, rollbacks, and error messages.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.exc import IntegrityError, OperationalError, DataError
from uuid import uuid4
from app.crud.user import user as user_crud
from app.schemas.users import UserCreate, UserUpdate
class TestBaseCRUDCreateFailures:
"""Test base CRUD create method exception handling."""
@pytest.mark.asyncio
async def test_create_operational_error_triggers_rollback(self, async_test_db):
"""Test that OperationalError triggers rollback (User CRUD catches as Exception)."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise OperationalError("Connection lost", {}, Exception("DB connection failed"))
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
user_data = UserCreate(
email="operror@example.com",
password="TestPassword123!",
first_name="Test",
last_name="User"
)
# User CRUD catches this as generic Exception and re-raises
with pytest.raises(OperationalError):
await user_crud.create(session, obj_in=user_data)
# Verify rollback was called
mock_rollback.assert_called_once()
@pytest.mark.asyncio
async def test_create_data_error_triggers_rollback(self, async_test_db):
"""Test that DataError triggers rollback (User CRUD catches as Exception)."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise DataError("Invalid data type", {}, Exception("Data overflow"))
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
user_data = UserCreate(
email="dataerror@example.com",
password="TestPassword123!",
first_name="Test",
last_name="User"
)
# User CRUD catches this as generic Exception and re-raises
with pytest.raises(DataError):
await user_crud.create(session, obj_in=user_data)
mock_rollback.assert_called_once()
@pytest.mark.asyncio
async def test_create_unexpected_exception_triggers_rollback(self, async_test_db):
"""Test that unexpected exceptions trigger rollback and re-raise."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise RuntimeError("Unexpected database error")
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
user_data = UserCreate(
email="unexpected@example.com",
password="TestPassword123!",
first_name="Test",
last_name="User"
)
with pytest.raises(RuntimeError, match="Unexpected database error"):
await user_crud.create(session, obj_in=user_data)
mock_rollback.assert_called_once()
class TestBaseCRUDUpdateFailures:
"""Test base CRUD update method exception handling."""
@pytest.mark.asyncio
async def test_update_operational_error(self, async_test_db, async_test_user):
"""Test update with OperationalError."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
user = await user_crud.get(session, id=str(async_test_user.id))
async def mock_commit():
raise OperationalError("Connection timeout", {}, Exception("Timeout"))
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(ValueError, match="Database operation failed"):
await user_crud.update(session, db_obj=user, obj_in={"first_name": "Updated"})
mock_rollback.assert_called_once()
@pytest.mark.asyncio
async def test_update_data_error(self, async_test_db, async_test_user):
"""Test update with DataError."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
user = await user_crud.get(session, id=str(async_test_user.id))
async def mock_commit():
raise DataError("Invalid data", {}, Exception("Data type mismatch"))
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(ValueError, match="Database operation failed"):
await user_crud.update(session, db_obj=user, obj_in={"first_name": "Updated"})
mock_rollback.assert_called_once()
@pytest.mark.asyncio
async def test_update_unexpected_error(self, async_test_db, async_test_user):
"""Test update with unexpected error."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
user = await user_crud.get(session, id=str(async_test_user.id))
async def mock_commit():
raise KeyError("Unexpected error")
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(KeyError):
await user_crud.update(session, db_obj=user, obj_in={"first_name": "Updated"})
mock_rollback.assert_called_once()
class TestBaseCRUDRemoveFailures:
"""Test base CRUD remove method exception handling."""
@pytest.mark.asyncio
async def test_remove_unexpected_error_triggers_rollback(self, async_test_db, async_test_user):
"""Test that unexpected errors in remove trigger rollback."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise RuntimeError("Database write failed")
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(RuntimeError, match="Database write failed"):
await user_crud.remove(session, id=str(async_test_user.id))
mock_rollback.assert_called_once()
class TestBaseCRUDGetMultiWithTotalFailures:
"""Test get_multi_with_total exception handling."""
@pytest.mark.asyncio
async def test_get_multi_with_total_database_error(self, async_test_db):
"""Test get_multi_with_total handles database errors."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
# Mock execute to raise an error
original_execute = session.execute
async def mock_execute(*args, **kwargs):
raise OperationalError("Query failed", {}, Exception("Database error"))
with patch.object(session, 'execute', side_effect=mock_execute):
with pytest.raises(OperationalError):
await user_crud.get_multi_with_total(session, skip=0, limit=10)
class TestBaseCRUDCountFailures:
"""Test count method exception handling."""
@pytest.mark.asyncio
async def test_count_database_error_propagates(self, async_test_db):
"""Test count propagates database errors."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_execute(*args, **kwargs):
raise OperationalError("Count failed", {}, Exception("DB error"))
with patch.object(session, 'execute', side_effect=mock_execute):
with pytest.raises(OperationalError):
await user_crud.count(session)
class TestBaseCRUDSoftDeleteFailures:
"""Test soft_delete method exception handling."""
@pytest.mark.asyncio
async def test_soft_delete_unexpected_error_triggers_rollback(self, async_test_db, async_test_user):
"""Test soft_delete handles unexpected errors with rollback."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise RuntimeError("Soft delete failed")
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(RuntimeError, match="Soft delete failed"):
await user_crud.soft_delete(session, id=str(async_test_user.id))
mock_rollback.assert_called_once()
class TestBaseCRUDRestoreFailures:
"""Test restore method exception handling."""
@pytest.mark.asyncio
async def test_restore_unexpected_error_triggers_rollback(self, async_test_db):
"""Test restore handles unexpected errors with rollback."""
test_engine, SessionLocal = async_test_db
# First create and soft delete a user
async with SessionLocal() as session:
user_data = UserCreate(
email="restore_test@example.com",
password="TestPassword123!",
first_name="Restore",
last_name="Test"
)
user = await user_crud.create(session, obj_in=user_data)
user_id = user.id
await session.commit()
async with SessionLocal() as session:
await user_crud.soft_delete(session, id=str(user_id))
# Now test restore failure
async with SessionLocal() as session:
async def mock_commit():
raise RuntimeError("Restore failed")
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(RuntimeError, match="Restore failed"):
await user_crud.restore(session, id=str(user_id))
mock_rollback.assert_called_once()
class TestBaseCRUDGetFailures:
"""Test get method exception handling."""
@pytest.mark.asyncio
async def test_get_database_error_propagates(self, async_test_db):
"""Test get propagates database errors."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_execute(*args, **kwargs):
raise OperationalError("Get failed", {}, Exception("DB error"))
with patch.object(session, 'execute', side_effect=mock_execute):
with pytest.raises(OperationalError):
await user_crud.get(session, id=str(uuid4()))
class TestBaseCRUDGetMultiFailures:
"""Test get_multi method exception handling."""
@pytest.mark.asyncio
async def test_get_multi_database_error_propagates(self, async_test_db):
"""Test get_multi propagates database errors."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_execute(*args, **kwargs):
raise OperationalError("Query failed", {}, Exception("DB error"))
with patch.object(session, 'execute', side_effect=mock_execute):
with pytest.raises(OperationalError):
await user_crud.get_multi(session, skip=0, limit=10)

View File

@@ -0,0 +1,336 @@
# tests/crud/test_session_db_failures.py
"""
Comprehensive tests for session CRUD database failure scenarios.
"""
import pytest
from unittest.mock import AsyncMock, patch
from sqlalchemy.exc import OperationalError, IntegrityError
from datetime import datetime, timedelta, timezone
from uuid import uuid4
from app.crud.session import session as session_crud
from app.models.user_session import UserSession
from app.schemas.sessions import SessionCreate
class TestSessionCRUDGetByJtiFailures:
"""Test get_by_jti exception handling."""
@pytest.mark.asyncio
async def test_get_by_jti_database_error(self, async_test_db):
"""Test get_by_jti handles database errors."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_execute(*args, **kwargs):
raise OperationalError("DB connection lost", {}, Exception())
with patch.object(session, 'execute', side_effect=mock_execute):
with pytest.raises(OperationalError):
await session_crud.get_by_jti(session, jti="test_jti")
class TestSessionCRUDGetActiveByJtiFailures:
"""Test get_active_by_jti exception handling."""
@pytest.mark.asyncio
async def test_get_active_by_jti_database_error(self, async_test_db):
"""Test get_active_by_jti handles database errors."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_execute(*args, **kwargs):
raise OperationalError("Query timeout", {}, Exception())
with patch.object(session, 'execute', side_effect=mock_execute):
with pytest.raises(OperationalError):
await session_crud.get_active_by_jti(session, jti="test_jti")
class TestSessionCRUDGetUserSessionsFailures:
"""Test get_user_sessions exception handling."""
@pytest.mark.asyncio
async def test_get_user_sessions_database_error(self, async_test_db, async_test_user):
"""Test get_user_sessions handles database errors."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_execute(*args, **kwargs):
raise OperationalError("Database error", {}, Exception())
with patch.object(session, 'execute', side_effect=mock_execute):
with pytest.raises(OperationalError):
await session_crud.get_user_sessions(
session,
user_id=str(async_test_user.id)
)
class TestSessionCRUDCreateSessionFailures:
"""Test create_session exception handling."""
@pytest.mark.asyncio
async def test_create_session_commit_failure_triggers_rollback(self, async_test_db, async_test_user):
"""Test create_session handles commit failures with rollback."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise OperationalError("Commit failed", {}, Exception())
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
session_data = SessionCreate(
user_id=async_test_user.id,
refresh_token_jti=str(uuid4()),
device_name="Test Device",
ip_address="127.0.0.1",
user_agent="Test Agent",
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
with pytest.raises(ValueError, match="Failed to create session"):
await session_crud.create_session(session, obj_in=session_data)
mock_rollback.assert_called_once()
@pytest.mark.asyncio
async def test_create_session_unexpected_error_triggers_rollback(self, async_test_db, async_test_user):
"""Test create_session handles unexpected errors."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise RuntimeError("Unexpected error")
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
session_data = SessionCreate(
user_id=async_test_user.id,
refresh_token_jti=str(uuid4()),
device_name="Test Device",
ip_address="127.0.0.1",
user_agent="Test Agent",
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
with pytest.raises(ValueError, match="Failed to create session"):
await session_crud.create_session(session, obj_in=session_data)
mock_rollback.assert_called_once()
class TestSessionCRUDDeactivateFailures:
"""Test deactivate exception handling."""
@pytest.mark.asyncio
async def test_deactivate_commit_failure_triggers_rollback(self, async_test_db, async_test_user):
"""Test deactivate handles commit failures."""
test_engine, SessionLocal = async_test_db
# Create a session first
async with SessionLocal() as session:
user_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti=str(uuid4()),
device_name="Test Device",
ip_address="127.0.0.1",
user_agent="Test Agent",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add(user_session)
await session.commit()
await session.refresh(user_session)
session_id = user_session.id
# Test deactivate failure
async with SessionLocal() as session:
async def mock_commit():
raise OperationalError("Deactivate failed", {}, Exception())
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(OperationalError):
await session_crud.deactivate(session, session_id=str(session_id))
mock_rollback.assert_called_once()
class TestSessionCRUDDeactivateAllFailures:
"""Test deactivate_all_user_sessions exception handling."""
@pytest.mark.asyncio
async def test_deactivate_all_commit_failure_triggers_rollback(self, async_test_db, async_test_user):
"""Test deactivate_all handles commit failures."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise OperationalError("Bulk deactivate failed", {}, Exception())
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(OperationalError):
await session_crud.deactivate_all_user_sessions(
session,
user_id=str(async_test_user.id)
)
mock_rollback.assert_called_once()
class TestSessionCRUDUpdateLastUsedFailures:
"""Test update_last_used exception handling."""
@pytest.mark.asyncio
async def test_update_last_used_commit_failure_triggers_rollback(self, async_test_db, async_test_user):
"""Test update_last_used handles commit failures."""
test_engine, SessionLocal = async_test_db
# Create a session
async with SessionLocal() as session:
user_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti=str(uuid4()),
device_name="Test Device",
ip_address="127.0.0.1",
user_agent="Test Agent",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc) - timedelta(hours=1)
)
session.add(user_session)
await session.commit()
await session.refresh(user_session)
# Test update failure
async with SessionLocal() as session:
from sqlalchemy import select
from app.models.user_session import UserSession as US
result = await session.execute(select(US).where(US.id == user_session.id))
sess = result.scalar_one()
async def mock_commit():
raise OperationalError("Update failed", {}, Exception())
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(OperationalError):
await session_crud.update_last_used(session, session=sess)
mock_rollback.assert_called_once()
class TestSessionCRUDUpdateRefreshTokenFailures:
"""Test update_refresh_token exception handling."""
@pytest.mark.asyncio
async def test_update_refresh_token_commit_failure_triggers_rollback(self, async_test_db, async_test_user):
"""Test update_refresh_token handles commit failures."""
test_engine, SessionLocal = async_test_db
# Create a session
async with SessionLocal() as session:
user_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti=str(uuid4()),
device_name="Test Device",
ip_address="127.0.0.1",
user_agent="Test Agent",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add(user_session)
await session.commit()
await session.refresh(user_session)
# Test update failure
async with SessionLocal() as session:
from sqlalchemy import select
from app.models.user_session import UserSession as US
result = await session.execute(select(US).where(US.id == user_session.id))
sess = result.scalar_one()
async def mock_commit():
raise OperationalError("Token update failed", {}, Exception())
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(OperationalError):
await session_crud.update_refresh_token(
session,
session=sess,
new_jti=str(uuid4()),
new_expires_at=datetime.now(timezone.utc) + timedelta(days=14)
)
mock_rollback.assert_called_once()
class TestSessionCRUDCleanupExpiredFailures:
"""Test cleanup_expired exception handling."""
@pytest.mark.asyncio
async def test_cleanup_expired_commit_failure_triggers_rollback(self, async_test_db):
"""Test cleanup_expired handles commit failures."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise OperationalError("Cleanup failed", {}, Exception())
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(OperationalError):
await session_crud.cleanup_expired(session, keep_days=30)
mock_rollback.assert_called_once()
class TestSessionCRUDCleanupExpiredForUserFailures:
"""Test cleanup_expired_for_user exception handling."""
@pytest.mark.asyncio
async def test_cleanup_expired_for_user_commit_failure_triggers_rollback(self, async_test_db, async_test_user):
"""Test cleanup_expired_for_user handles commit failures."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_commit():
raise OperationalError("User cleanup failed", {}, Exception())
with patch.object(session, 'commit', side_effect=mock_commit):
with patch.object(session, 'rollback', new_callable=AsyncMock) as mock_rollback:
with pytest.raises(OperationalError):
await session_crud.cleanup_expired_for_user(
session,
user_id=str(async_test_user.id)
)
mock_rollback.assert_called_once()
class TestSessionCRUDGetUserSessionCountFailures:
"""Test get_user_session_count exception handling."""
@pytest.mark.asyncio
async def test_get_user_session_count_database_error(self, async_test_db, async_test_user):
"""Test get_user_session_count handles database errors."""
test_engine, SessionLocal = async_test_db
async with SessionLocal() as session:
async def mock_execute(*args, **kwargs):
raise OperationalError("Count query failed", {}, Exception())
with patch.object(session, 'execute', side_effect=mock_execute):
with pytest.raises(OperationalError):
await session_crud.get_user_session_count(
session,
user_id=str(async_test_user.id)
)