# 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