Files
fast-next-template/backend/tests/api/test_admin.py
Felipe Cardoso c589b565f0 Add pyproject.toml for consolidated project configuration and replace Black, isort, and Flake8 with Ruff
- Introduced `pyproject.toml` to centralize backend tool configurations (e.g., Ruff, mypy, coverage, pytest).
- Replaced Black, isort, and Flake8 with Ruff for linting, formatting, and import sorting.
- Updated `requirements.txt` to include Ruff and remove replaced tools.
- Added `Makefile` to streamline development workflows with commands for linting, formatting, type-checking, testing, and cleanup.
2025-11-10 11:55:15 +01:00

1100 lines
38 KiB
Python

# tests/api/test_admin.py
"""
Comprehensive tests for admin endpoints.
"""
from datetime import UTC, datetime, timedelta
from uuid import uuid4
import pytest
import pytest_asyncio
from fastapi import status
from app.models.organization import Organization
from app.models.user_organization import OrganizationRole, UserOrganization
from app.models.user_session import UserSession
@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, f"Login failed: {response.json()}"
return response.json()["access_token"]
# ===== USER MANAGEMENT TESTS =====
class TestAdminListUsers:
"""Tests for GET /admin/users endpoint."""
@pytest.mark.asyncio
async def test_admin_list_users_success(self, client, superuser_token):
"""Test successfully listing users as admin."""
response = await client.get(
"/api/v1/admin/users",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "data" in data
assert "pagination" in data
assert isinstance(data["data"], list)
@pytest.mark.asyncio
async def test_admin_list_users_with_filters(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test listing users with filters."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create inactive user
async with AsyncTestingSessionLocal() as session:
from app.core.auth import get_password_hash
from app.models.user import User
inactive_user = User(
email="inactive@example.com",
password_hash=get_password_hash("TestPassword123!"),
first_name="Inactive",
last_name="User",
is_active=False,
)
session.add(inactive_user)
await session.commit()
response = await client.get(
"/api/v1/admin/users?is_active=false",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data["data"]) >= 1
@pytest.mark.asyncio
async def test_admin_list_users_with_search(
self, client, async_test_superuser, superuser_token
):
"""Test searching users."""
response = await client.get(
"/api/v1/admin/users?search=superuser",
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_admin_list_users_unauthorized(self, client, async_test_user):
"""Test non-admin cannot list users."""
# Login as regular user
login_response = await client.post(
"/api/v1/auth/login",
json={"email": async_test_user.email, "password": "TestPassword123!"},
)
token = login_response.json()["access_token"]
response = await client.get(
"/api/v1/admin/users", headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == status.HTTP_403_FORBIDDEN
class TestAdminCreateUser:
"""Tests for POST /admin/users endpoint."""
@pytest.mark.asyncio
async def test_admin_create_user_success(
self, client, async_test_superuser, superuser_token
):
"""Test successfully creating a user as admin."""
response = await client.post(
"/api/v1/admin/users",
json={
"email": "newadminuser@example.com",
"password": "SecurePassword123!",
"first_name": "New",
"last_name": "User",
},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["email"] == "newadminuser@example.com"
@pytest.mark.asyncio
async def test_admin_create_user_duplicate_email(
self, client, async_test_superuser, async_test_user, superuser_token
):
"""Test creating user with duplicate email fails."""
response = await client.post(
"/api/v1/admin/users",
json={
"email": async_test_user.email,
"password": "SecurePassword123!",
"first_name": "Duplicate",
"last_name": "User",
},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminGetUser:
"""Tests for GET /admin/users/{user_id} endpoint."""
@pytest.mark.asyncio
async def test_admin_get_user_success(
self, client, async_test_superuser, async_test_user, superuser_token
):
"""Test successfully getting user details."""
response = await client.get(
f"/api/v1/admin/users/{async_test_user.id}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == str(async_test_user.id)
assert data["email"] == async_test_user.email
@pytest.mark.asyncio
async def test_admin_get_user_not_found(
self, client, async_test_superuser, superuser_token
):
"""Test getting non-existent user."""
response = await client.get(
f"/api/v1/admin/users/{uuid4()}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminUpdateUser:
"""Tests for PUT /admin/users/{user_id} endpoint."""
@pytest.mark.asyncio
async def test_admin_update_user_success(
self, client, async_test_superuser, async_test_user, superuser_token
):
"""Test successfully updating a user."""
response = await client.put(
f"/api/v1/admin/users/{async_test_user.id}",
json={"first_name": "Updated"},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["first_name"] == "Updated"
@pytest.mark.asyncio
async def test_admin_update_user_not_found(
self, client, async_test_superuser, superuser_token
):
"""Test updating non-existent user."""
response = await client.put(
f"/api/v1/admin/users/{uuid4()}",
json={"first_name": "Updated"},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminDeleteUser:
"""Tests for DELETE /admin/users/{user_id} endpoint."""
@pytest.mark.asyncio
async def test_admin_delete_user_success(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test successfully deleting a user."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create user to delete
async with AsyncTestingSessionLocal() as session:
from app.core.auth import get_password_hash
from app.models.user import User
user_to_delete = User(
email="todelete@example.com",
password_hash=get_password_hash("TestPassword123!"),
first_name="To",
last_name="Delete",
)
session.add(user_to_delete)
await session.commit()
user_id = user_to_delete.id
response = await client.delete(
f"/api/v1/admin/users/{user_id}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True
@pytest.mark.asyncio
async def test_admin_delete_user_not_found(
self, client, async_test_superuser, superuser_token
):
"""Test deleting non-existent user."""
response = await client.delete(
f"/api/v1/admin/users/{uuid4()}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_admin_delete_self_forbidden(
self, client, async_test_superuser, superuser_token
):
"""Test admin cannot delete their own account."""
response = await client.delete(
f"/api/v1/admin/users/{async_test_superuser.id}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_403_FORBIDDEN
class TestAdminActivateUser:
"""Tests for POST /admin/users/{user_id}/activate endpoint."""
@pytest.mark.asyncio
async def test_admin_activate_user_success(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test successfully activating a user."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create inactive user
async with AsyncTestingSessionLocal() as session:
from app.core.auth import get_password_hash
from app.models.user import User
inactive_user = User(
email="toactivate@example.com",
password_hash=get_password_hash("TestPassword123!"),
first_name="To",
last_name="Activate",
is_active=False,
)
session.add(inactive_user)
await session.commit()
user_id = inactive_user.id
response = await client.post(
f"/api/v1/admin/users/{user_id}/activate",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True
@pytest.mark.asyncio
async def test_admin_activate_user_not_found(
self, client, async_test_superuser, superuser_token
):
"""Test activating non-existent user."""
response = await client.post(
f"/api/v1/admin/users/{uuid4()}/activate",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminDeactivateUser:
"""Tests for POST /admin/users/{user_id}/deactivate endpoint."""
@pytest.mark.asyncio
async def test_admin_deactivate_user_success(
self, client, async_test_superuser, async_test_user, superuser_token
):
"""Test successfully deactivating a user."""
response = await client.post(
f"/api/v1/admin/users/{async_test_user.id}/deactivate",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True
@pytest.mark.asyncio
async def test_admin_deactivate_user_not_found(
self, client, async_test_superuser, superuser_token
):
"""Test deactivating non-existent user."""
response = await client.post(
f"/api/v1/admin/users/{uuid4()}/deactivate",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_admin_deactivate_self_forbidden(
self, client, async_test_superuser, superuser_token
):
"""Test admin cannot deactivate their own account."""
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
class TestAdminBulkUserAction:
"""Tests for POST /admin/users/bulk-action endpoint."""
@pytest.mark.asyncio
async def test_admin_bulk_activate_users(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test bulk activating users."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create inactive users
user_ids = []
async with AsyncTestingSessionLocal() as session:
from app.core.auth import get_password_hash
from app.models.user import User
for i in range(3):
user = User(
email=f"bulk{i}@example.com",
password_hash=get_password_hash("TestPassword123!"),
first_name=f"Bulk{i}",
last_name="User",
is_active=False,
)
session.add(user)
await session.flush()
user_ids.append(str(user.id))
await session.commit()
response = await client.post(
"/api/v1/admin/users/bulk-action",
json={"action": "activate", "user_ids": user_ids},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["affected_count"] == 3
@pytest.mark.asyncio
async def test_admin_bulk_deactivate_users(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test bulk deactivating users."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create active users
user_ids = []
async with AsyncTestingSessionLocal() as session:
from app.core.auth import get_password_hash
from app.models.user import User
for i in range(2):
user = User(
email=f"deactivate{i}@example.com",
password_hash=get_password_hash("TestPassword123!"),
first_name=f"Deactivate{i}",
last_name="User",
is_active=True,
)
session.add(user)
await session.flush()
user_ids.append(str(user.id))
await session.commit()
response = await client.post(
"/api/v1/admin/users/bulk-action",
json={"action": "deactivate", "user_ids": user_ids},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["affected_count"] == 2
@pytest.mark.asyncio
async def test_admin_bulk_delete_users(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test bulk deleting users."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create users to delete
user_ids = []
async with AsyncTestingSessionLocal() as session:
from app.core.auth import get_password_hash
from app.models.user import User
for i in range(2):
user = User(
email=f"bulkdelete{i}@example.com",
password_hash=get_password_hash("TestPassword123!"),
first_name=f"BulkDelete{i}",
last_name="User",
)
session.add(user)
await session.flush()
user_ids.append(str(user.id))
await session.commit()
response = await client.post(
"/api/v1/admin/users/bulk-action",
json={"action": "delete", "user_ids": user_ids},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["affected_count"] >= 0
# ===== ORGANIZATION MANAGEMENT TESTS =====
class TestAdminListOrganizations:
"""Tests for GET /admin/organizations endpoint."""
@pytest.mark.asyncio
async def test_admin_list_organizations_success(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test successfully listing organizations."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organization
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
response = await client.get(
"/api/v1/admin/organizations",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "data" in data
assert "pagination" in data
@pytest.mark.asyncio
async def test_admin_list_organizations_with_search(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test searching organizations."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organization
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Searchable Org", slug="searchable-org")
session.add(org)
await session.commit()
response = await client.get(
"/api/v1/admin/organizations?search=Searchable",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
class TestAdminCreateOrganization:
"""Tests for POST /admin/organizations endpoint."""
@pytest.mark.asyncio
async def test_admin_create_organization_success(
self, client, async_test_superuser, superuser_token
):
"""Test successfully creating an organization."""
response = await client.post(
"/api/v1/admin/organizations",
json={
"name": "New Admin Org",
"slug": "new-admin-org",
"description": "Created by admin",
},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["name"] == "New Admin Org"
assert data["member_count"] == 0
@pytest.mark.asyncio
async def test_admin_create_organization_duplicate_slug(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test creating organization with duplicate slug fails."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create existing organization
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Existing", slug="duplicate-slug")
session.add(org)
await session.commit()
response = await client.post(
"/api/v1/admin/organizations",
json={"name": "Duplicate", "slug": "duplicate-slug"},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminGetOrganization:
"""Tests for GET /admin/organizations/{org_id} endpoint."""
@pytest.mark.asyncio
async def test_admin_get_organization_success(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test successfully getting organization details."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organization
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Get Test Org", slug="get-test-org")
session.add(org)
await session.commit()
org_id = org.id
response = await client.get(
f"/api/v1/admin/organizations/{org_id}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["name"] == "Get Test Org"
@pytest.mark.asyncio
async def test_admin_get_organization_not_found(
self, client, async_test_superuser, superuser_token
):
"""Test getting non-existent organization."""
response = await client.get(
f"/api/v1/admin/organizations/{uuid4()}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminUpdateOrganization:
"""Tests for PUT /admin/organizations/{org_id} endpoint."""
@pytest.mark.asyncio
async def test_admin_update_organization_success(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test successfully updating an organization."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organization
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Update Test", slug="update-test")
session.add(org)
await session.commit()
org_id = org.id
response = await client.put(
f"/api/v1/admin/organizations/{org_id}",
json={"name": "Updated Name"},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["name"] == "Updated Name"
@pytest.mark.asyncio
async def test_admin_update_organization_not_found(
self, client, async_test_superuser, superuser_token
):
"""Test updating non-existent organization."""
response = await client.put(
f"/api/v1/admin/organizations/{uuid4()}",
json={"name": "Updated"},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminDeleteOrganization:
"""Tests for DELETE /admin/organizations/{org_id} endpoint."""
@pytest.mark.asyncio
async def test_admin_delete_organization_success(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test successfully deleting an organization."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organization
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Delete Test", slug="delete-test")
session.add(org)
await session.commit()
org_id = org.id
response = await client.delete(
f"/api/v1/admin/organizations/{org_id}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True
@pytest.mark.asyncio
async def test_admin_delete_organization_not_found(
self, client, async_test_superuser, superuser_token
):
"""Test deleting non-existent organization."""
response = await client.delete(
f"/api/v1/admin/organizations/{uuid4()}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminListOrganizationMembers:
"""Tests for GET /admin/organizations/{org_id}/members endpoint."""
@pytest.mark.asyncio
async def test_admin_list_organization_members_success(
self,
client,
async_test_superuser,
async_test_db,
async_test_user,
superuser_token,
):
"""Test successfully listing organization members."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organization with member
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Members Test", slug="members-test")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.MEMBER,
is_active=True,
)
session.add(user_org)
await session.commit()
org_id = org.id
response = await client.get(
f"/api/v1/admin/organizations/{org_id}/members",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "data" in data
assert len(data["data"]) >= 1
@pytest.mark.asyncio
async def test_admin_list_organization_members_not_found(
self, client, async_test_superuser, superuser_token
):
"""Test listing members of non-existent organization."""
response = await client.get(
f"/api/v1/admin/organizations/{uuid4()}/members",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminAddOrganizationMember:
"""Tests for POST /admin/organizations/{org_id}/members endpoint."""
@pytest.mark.asyncio
async def test_admin_add_organization_member_success(
self,
client,
async_test_superuser,
async_test_db,
async_test_user,
superuser_token,
):
"""Test successfully adding a member to organization."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organization
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Add Member Test", slug="add-member-test")
session.add(org)
await session.commit()
org_id = org.id
response = await client.post(
f"/api/v1/admin/organizations/{org_id}/members",
json={"user_id": str(async_test_user.id), "role": "member"},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True
@pytest.mark.asyncio
async def test_admin_add_organization_member_already_exists(
self,
client,
async_test_superuser,
async_test_db,
async_test_user,
superuser_token,
):
"""Test adding member who is already a member."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create organization with existing member
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Existing Member", slug="existing-member")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.MEMBER,
is_active=True,
)
session.add(user_org)
await session.commit()
org_id = org.id
response = await client.post(
f"/api/v1/admin/organizations/{org_id}/members",
json={"user_id": str(async_test_user.id), "role": "member"},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_409_CONFLICT
@pytest.mark.asyncio
async def test_admin_add_organization_member_org_not_found(
self, client, async_test_superuser, async_test_user, superuser_token
):
"""Test adding member to non-existent organization."""
response = await client.post(
f"/api/v1/admin/organizations/{uuid4()}/members",
json={"user_id": str(async_test_user.id), "role": "member"},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
async def test_admin_add_organization_member_user_not_found(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test adding non-existent user to organization."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organization
async with AsyncTestingSessionLocal() as session:
org = Organization(name="User Not Found", slug="user-not-found")
session.add(org)
await session.commit()
org_id = org.id
response = await client.post(
f"/api/v1/admin/organizations/{org_id}/members",
json={"user_id": str(uuid4()), "role": "member"},
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestAdminRemoveOrganizationMember:
"""Tests for DELETE /admin/organizations/{org_id}/members/{user_id} endpoint."""
@pytest.mark.asyncio
async def test_admin_remove_organization_member_success(
self,
client,
async_test_superuser,
async_test_db,
async_test_user,
superuser_token,
):
"""Test successfully removing a member from organization."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create organization with member
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Remove Member", slug="remove-member")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.MEMBER,
is_active=True,
)
session.add(user_org)
await session.commit()
org_id = org.id
response = await client.delete(
f"/api/v1/admin/organizations/{org_id}/members/{async_test_user.id}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["success"] is True
@pytest.mark.asyncio
async def test_admin_remove_organization_member_not_member(
self,
client,
async_test_superuser,
async_test_db,
async_test_user,
superuser_token,
):
"""Test removing user who is not a member."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create organization without member
async with AsyncTestingSessionLocal() as session:
org = Organization(name="No Member", slug="no-member")
session.add(org)
await session.commit()
org_id = org.id
response = await client.delete(
f"/api/v1/admin/organizations/{org_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_admin_remove_organization_member_org_not_found(
self, client, async_test_superuser, async_test_user, superuser_token
):
"""Test removing member from non-existent organization."""
response = await client.delete(
f"/api/v1/admin/organizations/{uuid4()}/members/{async_test_user.id}",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
# ===== SESSION MANAGEMENT TESTS =====
class TestAdminListSessions:
"""Tests for admin sessions list endpoint."""
@pytest.mark.asyncio
async def test_admin_list_sessions_success(
self,
client,
async_test_superuser,
async_test_user,
async_test_db,
superuser_token,
):
"""Test listing all sessions as admin."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create some test sessions
async with AsyncTestingSessionLocal() as session:
now = datetime.now(UTC)
expires_at = now + timedelta(days=7)
session1 = UserSession(
user_id=async_test_user.id,
refresh_token_jti="jti-test-1",
device_name="iPhone 14",
device_id="device-1",
ip_address="192.168.1.100",
user_agent="Mozilla/5.0",
last_used_at=now,
expires_at=expires_at,
is_active=True,
location_city="San Francisco",
location_country="United States",
)
session2 = UserSession(
user_id=async_test_superuser.id,
refresh_token_jti="jti-test-2",
device_name="MacBook Pro",
device_id="device-2",
ip_address="192.168.1.101",
user_agent="Mozilla/5.0",
last_used_at=now,
expires_at=expires_at,
is_active=True,
)
session.add_all([session1, session2])
await session.commit()
response = await client.get(
"/api/v1/admin/sessions?page=1&limit=10",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "data" in data
assert "pagination" in data
assert len(data["data"]) >= 2 # At least our 2 test sessions
assert data["pagination"]["total"] >= 2
# Verify session structure includes user info
first_session = data["data"][0]
assert "id" in first_session
assert "user_id" in first_session
assert "user_email" in first_session
assert "device_name" in first_session
assert "ip_address" in first_session
assert "is_active" in first_session
@pytest.mark.asyncio
async def test_admin_list_sessions_filter_active(
self,
client,
async_test_superuser,
async_test_user,
async_test_db,
superuser_token,
):
"""Test filtering sessions by active status."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create active and inactive sessions
async with AsyncTestingSessionLocal() as session:
now = datetime.now(UTC)
expires_at = now + timedelta(days=7)
active_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti="jti-active",
device_name="Active Device",
ip_address="192.168.1.100",
last_used_at=now,
expires_at=expires_at,
is_active=True,
)
inactive_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti="jti-inactive",
device_name="Inactive Device",
ip_address="192.168.1.101",
last_used_at=now,
expires_at=expires_at,
is_active=False,
)
session.add_all([active_session, inactive_session])
await session.commit()
# Get only active sessions (default)
response = await client.get(
"/api/v1/admin/sessions?page=1&limit=100",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned sessions should be active
for sess in data["data"]:
assert sess["is_active"] is True
@pytest.mark.asyncio
async def test_admin_list_sessions_pagination(
self, client, async_test_superuser, async_test_db, superuser_token
):
"""Test pagination of sessions list."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create multiple sessions
async with AsyncTestingSessionLocal() as session:
now = datetime.now(UTC)
expires_at = now + timedelta(days=7)
sessions = []
for i in range(5):
sess = UserSession(
user_id=async_test_superuser.id,
refresh_token_jti=f"jti-pagination-{i}",
device_name=f"Device {i}",
ip_address=f"192.168.1.{100 + i}",
last_used_at=now,
expires_at=expires_at,
is_active=True,
)
sessions.append(sess)
session.add_all(sessions)
await session.commit()
# Get first page with limit 2
response = await client.get(
"/api/v1/admin/sessions?page=1&limit=2",
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data["data"]) == 2
assert data["pagination"]["page"] == 1
assert data["pagination"]["page_size"] == 2
assert data["pagination"]["total"] >= 5
@pytest.mark.asyncio
async def test_admin_list_sessions_unauthorized(
self, client, async_test_user, user_token
):
"""Test that non-admin users cannot access admin sessions endpoint."""
response = await client.get(
"/api/v1/admin/sessions?page=1&limit=10",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_403_FORBIDDEN