Files
fast-next-template/backend/tests/api/test_admin_error_handlers.py
Felipe Cardoso a9e972d583 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.
2025-11-01 13:12:36 +01:00

547 lines
22 KiB
Python

# 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}"}
)