forked from cardosofelipe/fast-next-template
- Extended OAuth callback tests to cover various scenarios (e.g., account linking, user creation, inactive users, and token/user info failures). - Added `app/init_db.py` to the excluded files in `pyproject.toml`.
1220 lines
42 KiB
Python
1220 lines
42 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
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_remove_organization_member_user_not_found(
|
|
self, client, async_test_superuser, async_test_db, superuser_token
|
|
):
|
|
"""Test removing non-existent user from organization."""
|
|
_test_engine, AsyncTestingSessionLocal = async_test_db
|
|
|
|
# Create organization
|
|
async with AsyncTestingSessionLocal() as session:
|
|
org = Organization(name="User Not Found Org", slug="user-not-found-org")
|
|
session.add(org)
|
|
await session.commit()
|
|
org_id = org.id
|
|
|
|
response = await client.delete(
|
|
f"/api/v1/admin/organizations/{org_id}/members/{uuid4()}",
|
|
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
|
|
|
|
|
|
# ===== ADMIN STATS TESTS =====
|
|
|
|
|
|
class TestAdminStats:
|
|
"""Tests for GET /admin/stats endpoint."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_stats_with_data(
|
|
self,
|
|
client,
|
|
async_test_superuser,
|
|
async_test_user,
|
|
async_test_db,
|
|
superuser_token,
|
|
):
|
|
"""Test getting admin stats with real data in database."""
|
|
_test_engine, AsyncTestingSessionLocal = async_test_db
|
|
|
|
# Create multiple users and organizations with members
|
|
async with AsyncTestingSessionLocal() as session:
|
|
from app.core.auth import get_password_hash
|
|
from app.models.user import User
|
|
|
|
# Create several users
|
|
for i in range(5):
|
|
user = User(
|
|
email=f"statsuser{i}@example.com",
|
|
password_hash=get_password_hash("TestPassword123!"),
|
|
first_name=f"Stats{i}",
|
|
last_name="User",
|
|
is_active=i % 2 == 0, # Mix of active/inactive
|
|
)
|
|
session.add(user)
|
|
await session.commit()
|
|
|
|
# Create organizations with members
|
|
async with AsyncTestingSessionLocal() as session:
|
|
orgs = []
|
|
for i in range(3):
|
|
org = Organization(name=f"Stats Org {i}", slug=f"stats-org-{i}")
|
|
session.add(org)
|
|
orgs.append(org)
|
|
await session.flush()
|
|
|
|
# Add some members to organizations
|
|
user_org = UserOrganization(
|
|
user_id=async_test_user.id,
|
|
organization_id=orgs[0].id,
|
|
role=OrganizationRole.MEMBER,
|
|
is_active=True,
|
|
)
|
|
session.add(user_org)
|
|
await session.commit()
|
|
|
|
response = await client.get(
|
|
"/api/v1/admin/stats",
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
|
|
# Verify response structure
|
|
assert "user_growth" in data
|
|
assert "organization_distribution" in data
|
|
assert "registration_activity" in data
|
|
assert "user_status" in data
|
|
|
|
# Verify user_growth has 30 days of data
|
|
assert len(data["user_growth"]) == 30
|
|
for item in data["user_growth"]:
|
|
assert "date" in item
|
|
assert "total_users" in item
|
|
assert "active_users" in item
|
|
|
|
# Verify registration_activity has 14 days of data
|
|
assert len(data["registration_activity"]) == 14
|
|
for item in data["registration_activity"]:
|
|
assert "date" in item
|
|
assert "registrations" in item
|
|
|
|
# Verify user_status has active/inactive counts
|
|
assert len(data["user_status"]) == 2
|
|
status_names = {item["name"] for item in data["user_status"]}
|
|
assert status_names == {"Active", "Inactive"}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_admin_get_stats_unauthorized(
|
|
self, client, async_test_user, user_token
|
|
):
|
|
"""Test that non-admin users cannot access stats endpoint."""
|
|
response = await client.get(
|
|
"/api/v1/admin/stats",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|