Add comprehensive tests for session cleanup and async CRUD operations; improve error handling and validation across schemas and API routes

- Introduced extensive tests for session cleanup, async session CRUD methods, and concurrent cleanup to ensure reliability and efficiency.
- Enhanced `schemas/users.py` with reusable password strength validation logic.
- Improved error handling in `admin.py` routes by replacing `detail` with `message` for consistency and readability.
This commit is contained in:
Felipe Cardoso
2025-11-01 05:22:45 +01:00
parent c79b76be41
commit 035e6af446
11 changed files with 3644 additions and 88 deletions

View File

@@ -0,0 +1,944 @@
# tests/crud/test_organization_async.py
"""
Comprehensive tests for async organization CRUD operations.
"""
import pytest
from uuid import uuid4
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.crud.organization_async import organization_async
from app.models.organization import Organization
from app.models.user_organization import UserOrganization, OrganizationRole
from app.models.user import User
from app.schemas.organizations import OrganizationCreate, OrganizationUpdate
class TestGetBySlug:
"""Tests for get_by_slug method."""
@pytest.mark.asyncio
async def test_get_by_slug_success(self, async_test_db):
"""Test successfully getting an organization by slug."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organization
async with AsyncTestingSessionLocal() as session:
org = Organization(
name="Test Org",
slug="test-org",
description="Test description"
)
session.add(org)
await session.commit()
org_id = org.id
# Get by slug
async with AsyncTestingSessionLocal() as session:
result = await organization_async.get_by_slug(session, slug="test-org")
assert result is not None
assert result.id == org_id
assert result.slug == "test-org"
@pytest.mark.asyncio
async def test_get_by_slug_not_found(self, async_test_db):
"""Test getting non-existent organization by slug."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await organization_async.get_by_slug(session, slug="nonexistent")
assert result is None
class TestCreate:
"""Tests for create method."""
@pytest.mark.asyncio
async def test_create_success(self, async_test_db):
"""Test successfully creating an organization."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org_in = OrganizationCreate(
name="New Org",
slug="new-org",
description="New organization",
is_active=True,
settings={"key": "value"}
)
result = await organization_async.create(session, obj_in=org_in)
assert result.name == "New Org"
assert result.slug == "new-org"
assert result.description == "New organization"
assert result.is_active is True
assert result.settings == {"key": "value"}
@pytest.mark.asyncio
async def test_create_duplicate_slug(self, async_test_db):
"""Test creating organization with duplicate slug raises error."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create first org
async with AsyncTestingSessionLocal() as session:
org1 = Organization(name="Org 1", slug="duplicate-slug")
session.add(org1)
await session.commit()
# Try to create second with same slug
async with AsyncTestingSessionLocal() as session:
org_in = OrganizationCreate(
name="Org 2",
slug="duplicate-slug"
)
with pytest.raises(ValueError, match="already exists"):
await organization_async.create(session, obj_in=org_in)
@pytest.mark.asyncio
async def test_create_without_settings(self, async_test_db):
"""Test creating organization without settings (defaults to empty dict)."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org_in = OrganizationCreate(
name="No Settings Org",
slug="no-settings"
)
result = await organization_async.create(session, obj_in=org_in)
assert result.settings == {}
class TestGetMultiWithFilters:
"""Tests for get_multi_with_filters method."""
@pytest.mark.asyncio
async def test_get_multi_with_filters_no_filters(self, async_test_db):
"""Test getting organizations without any filters."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create test organizations
async with AsyncTestingSessionLocal() as session:
for i in range(5):
org = Organization(name=f"Org {i}", slug=f"org-{i}")
session.add(org)
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs, total = await organization_async.get_multi_with_filters(session)
assert total == 5
assert len(orgs) == 5
@pytest.mark.asyncio
async def test_get_multi_with_filters_is_active(self, async_test_db):
"""Test filtering by is_active."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org1 = Organization(name="Active", slug="active", is_active=True)
org2 = Organization(name="Inactive", slug="inactive", is_active=False)
session.add_all([org1, org2])
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs, total = await organization_async.get_multi_with_filters(
session,
is_active=True
)
assert total == 1
assert orgs[0].name == "Active"
@pytest.mark.asyncio
async def test_get_multi_with_filters_search(self, async_test_db):
"""Test searching organizations."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org1 = Organization(name="Tech Corp", slug="tech-corp", description="Technology")
org2 = Organization(name="Food Inc", slug="food-inc", description="Restaurant")
session.add_all([org1, org2])
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs, total = await organization_async.get_multi_with_filters(
session,
search="tech"
)
assert total == 1
assert orgs[0].name == "Tech Corp"
@pytest.mark.asyncio
async def test_get_multi_with_filters_pagination(self, async_test_db):
"""Test pagination."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
for i in range(10):
org = Organization(name=f"Org {i}", slug=f"org-{i}")
session.add(org)
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs, total = await organization_async.get_multi_with_filters(
session,
skip=2,
limit=3
)
assert total == 10
assert len(orgs) == 3
@pytest.mark.asyncio
async def test_get_multi_with_filters_sorting(self, async_test_db):
"""Test sorting."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org1 = Organization(name="B Org", slug="b-org")
org2 = Organization(name="A Org", slug="a-org")
session.add_all([org1, org2])
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs, total = await organization_async.get_multi_with_filters(
session,
sort_by="name",
sort_order="asc"
)
assert orgs[0].name == "A Org"
assert orgs[1].name == "B Org"
class TestGetMemberCount:
"""Tests for get_member_count method."""
@pytest.mark.asyncio
async def test_get_member_count_success(self, async_test_db, async_test_user):
"""Test getting member count for organization."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
# Add 1 active member
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
async with AsyncTestingSessionLocal() as session:
count = await organization_async.get_member_count(session, organization_id=org_id)
assert count == 1
@pytest.mark.asyncio
async def test_get_member_count_no_members(self, async_test_db):
"""Test getting member count for organization with no members."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Empty Org", slug="empty-org")
session.add(org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
count = await organization_async.get_member_count(session, organization_id=org_id)
assert count == 0
class TestAddUser:
"""Tests for add_user method."""
@pytest.mark.asyncio
async def test_add_user_success(self, async_test_db, async_test_user):
"""Test successfully adding a user to organization."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
result = await organization_async.add_user(
session,
organization_id=org_id,
user_id=async_test_user.id,
role=OrganizationRole.ADMIN
)
assert result.user_id == async_test_user.id
assert result.organization_id == org_id
assert result.role == OrganizationRole.ADMIN
assert result.is_active is True
@pytest.mark.asyncio
async def test_add_user_already_active_member(self, async_test_db, async_test_user):
"""Test adding user who is already an active member raises error."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
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
async with AsyncTestingSessionLocal() as session:
with pytest.raises(ValueError, match="already a member"):
await organization_async.add_user(
session,
organization_id=org_id,
user_id=async_test_user.id
)
@pytest.mark.asyncio
async def test_add_user_reactivate_inactive(self, async_test_db, async_test_user):
"""Test adding user who was previously inactive reactivates them."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.MEMBER,
is_active=False
)
session.add(user_org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
result = await organization_async.add_user(
session,
organization_id=org_id,
user_id=async_test_user.id,
role=OrganizationRole.ADMIN
)
assert result.is_active is True
assert result.role == OrganizationRole.ADMIN
class TestRemoveUser:
"""Tests for remove_user method."""
@pytest.mark.asyncio
async def test_remove_user_success(self, async_test_db, async_test_user):
"""Test successfully removing a user from organization."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
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
async with AsyncTestingSessionLocal() as session:
result = await organization_async.remove_user(
session,
organization_id=org_id,
user_id=async_test_user.id
)
assert result is True
# Verify soft delete
async with AsyncTestingSessionLocal() as session:
stmt = select(UserOrganization).where(
UserOrganization.user_id == async_test_user.id,
UserOrganization.organization_id == org_id
)
result = await session.execute(stmt)
user_org = result.scalar_one_or_none()
assert user_org.is_active is False
@pytest.mark.asyncio
async def test_remove_user_not_found(self, async_test_db):
"""Test removing non-existent user returns False."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
result = await organization_async.remove_user(
session,
organization_id=org_id,
user_id=uuid4()
)
assert result is False
class TestUpdateUserRole:
"""Tests for update_user_role method."""
@pytest.mark.asyncio
async def test_update_user_role_success(self, async_test_db, async_test_user):
"""Test successfully updating user role."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
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
async with AsyncTestingSessionLocal() as session:
result = await organization_async.update_user_role(
session,
organization_id=org_id,
user_id=async_test_user.id,
role=OrganizationRole.ADMIN,
custom_permissions="custom"
)
assert result.role == OrganizationRole.ADMIN
assert result.custom_permissions == "custom"
@pytest.mark.asyncio
async def test_update_user_role_not_found(self, async_test_db):
"""Test updating role for non-existent user returns None."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
result = await organization_async.update_user_role(
session,
organization_id=org_id,
user_id=uuid4(),
role=OrganizationRole.ADMIN
)
assert result is None
class TestGetOrganizationMembers:
"""Tests for get_organization_members method."""
@pytest.mark.asyncio
async def test_get_organization_members_success(self, async_test_db, async_test_user):
"""Test getting organization members."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.ADMIN,
is_active=True
)
session.add(user_org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
members, total = await organization_async.get_organization_members(
session,
organization_id=org_id
)
assert total == 1
assert len(members) == 1
assert members[0]["user_id"] == async_test_user.id
assert members[0]["email"] == async_test_user.email
assert members[0]["role"] == OrganizationRole.ADMIN
@pytest.mark.asyncio
async def test_get_organization_members_with_pagination(self, async_test_db, async_test_user):
"""Test getting organization members with pagination."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
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
async with AsyncTestingSessionLocal() as session:
members, total = await organization_async.get_organization_members(
session,
organization_id=org_id,
skip=0,
limit=10
)
assert total == 1
assert len(members) <= 10
class TestGetUserOrganizations:
"""Tests for get_user_organizations method."""
@pytest.mark.asyncio
async def test_get_user_organizations_success(self, async_test_db, async_test_user):
"""Test getting user's organizations."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
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()
async with AsyncTestingSessionLocal() as session:
orgs = await organization_async.get_user_organizations(
session,
user_id=async_test_user.id
)
assert len(orgs) == 1
assert orgs[0].name == "Test Org"
@pytest.mark.asyncio
async def test_get_user_organizations_filter_inactive(self, async_test_db, async_test_user):
"""Test filtering inactive organizations."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org1 = Organization(name="Active Org", slug="active-org")
org2 = Organization(name="Inactive Org", slug="inactive-org")
session.add_all([org1, org2])
await session.commit()
user_org1 = UserOrganization(
user_id=async_test_user.id,
organization_id=org1.id,
role=OrganizationRole.MEMBER,
is_active=True
)
user_org2 = UserOrganization(
user_id=async_test_user.id,
organization_id=org2.id,
role=OrganizationRole.MEMBER,
is_active=False
)
session.add_all([user_org1, user_org2])
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs = await organization_async.get_user_organizations(
session,
user_id=async_test_user.id,
is_active=True
)
assert len(orgs) == 1
assert orgs[0].name == "Active Org"
class TestGetUserRole:
"""Tests for get_user_role_in_org method."""
@pytest.mark.asyncio
async def test_get_user_role_in_org_success(self, async_test_db, async_test_user):
"""Test getting user role in organization."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.ADMIN,
is_active=True
)
session.add(user_org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
role = await organization_async.get_user_role_in_org(
session,
user_id=async_test_user.id,
organization_id=org_id
)
assert role == OrganizationRole.ADMIN
@pytest.mark.asyncio
async def test_get_user_role_in_org_not_found(self, async_test_db):
"""Test getting role for non-member returns None."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
role = await organization_async.get_user_role_in_org(
session,
user_id=uuid4(),
organization_id=org_id
)
assert role is None
class TestIsUserOrgOwner:
"""Tests for is_user_org_owner method."""
@pytest.mark.asyncio
async def test_is_user_org_owner_true(self, async_test_db, async_test_user):
"""Test checking if user is owner."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.OWNER,
is_active=True
)
session.add(user_org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
is_owner = await organization_async.is_user_org_owner(
session,
user_id=async_test_user.id,
organization_id=org_id
)
assert is_owner is True
@pytest.mark.asyncio
async def test_is_user_org_owner_false(self, async_test_db, async_test_user):
"""Test checking if non-owner user is owner."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
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
async with AsyncTestingSessionLocal() as session:
is_owner = await organization_async.is_user_org_owner(
session,
user_id=async_test_user.id,
organization_id=org_id
)
assert is_owner is False
class TestGetMultiWithMemberCounts:
"""Tests for get_multi_with_member_counts method."""
@pytest.mark.asyncio
async def test_get_multi_with_member_counts_success(self, async_test_db, async_test_user):
"""Test getting organizations with member counts."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org1 = Organization(name="Org 1", slug="org-1")
org2 = Organization(name="Org 2", slug="org-2")
session.add_all([org1, org2])
await session.commit()
# Add members to org1
user_org1 = UserOrganization(
user_id=async_test_user.id,
organization_id=org1.id,
role=OrganizationRole.MEMBER,
is_active=True
)
session.add(user_org1)
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs_with_counts, total = await organization_async.get_multi_with_member_counts(session)
assert total == 2
assert len(orgs_with_counts) == 2
# Verify structure
assert 'organization' in orgs_with_counts[0]
assert 'member_count' in orgs_with_counts[0]
@pytest.mark.asyncio
async def test_get_multi_with_member_counts_with_filters(self, async_test_db):
"""Test getting organizations with member counts and filters."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org1 = Organization(name="Active Org", slug="active-org", is_active=True)
org2 = Organization(name="Inactive Org", slug="inactive-org", is_active=False)
session.add_all([org1, org2])
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs_with_counts, total = await organization_async.get_multi_with_member_counts(
session,
is_active=True
)
assert total == 1
assert orgs_with_counts[0]['organization'].name == "Active Org"
@pytest.mark.asyncio
async def test_get_multi_with_member_counts_with_search(self, async_test_db):
"""Test searching organizations with member counts."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org1 = Organization(name="Tech Corp", slug="tech-corp")
org2 = Organization(name="Food Inc", slug="food-inc")
session.add_all([org1, org2])
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs_with_counts, total = await organization_async.get_multi_with_member_counts(
session,
search="tech"
)
assert total == 1
assert orgs_with_counts[0]['organization'].name == "Tech Corp"
class TestGetUserOrganizationsWithDetails:
"""Tests for get_user_organizations_with_details method."""
@pytest.mark.asyncio
async def test_get_user_organizations_with_details_success(self, async_test_db, async_test_user):
"""Test getting user organizations with role and member count."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.ADMIN,
is_active=True
)
session.add(user_org)
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs_with_details = await organization_async.get_user_organizations_with_details(
session,
user_id=async_test_user.id
)
assert len(orgs_with_details) == 1
assert orgs_with_details[0]['organization'].name == "Test Org"
assert orgs_with_details[0]['role'] == OrganizationRole.ADMIN
assert 'member_count' in orgs_with_details[0]
@pytest.mark.asyncio
async def test_get_user_organizations_with_details_filter_inactive(self, async_test_db, async_test_user):
"""Test filtering inactive organizations in user details."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org1 = Organization(name="Active Org", slug="active-org")
org2 = Organization(name="Inactive Org", slug="inactive-org")
session.add_all([org1, org2])
await session.commit()
user_org1 = UserOrganization(
user_id=async_test_user.id,
organization_id=org1.id,
role=OrganizationRole.MEMBER,
is_active=True
)
user_org2 = UserOrganization(
user_id=async_test_user.id,
organization_id=org2.id,
role=OrganizationRole.MEMBER,
is_active=False
)
session.add_all([user_org1, user_org2])
await session.commit()
async with AsyncTestingSessionLocal() as session:
orgs_with_details = await organization_async.get_user_organizations_with_details(
session,
user_id=async_test_user.id,
is_active=True
)
assert len(orgs_with_details) == 1
assert orgs_with_details[0]['organization'].name == "Active Org"
class TestIsUserOrgAdmin:
"""Tests for is_user_org_admin method."""
@pytest.mark.asyncio
async def test_is_user_org_admin_owner(self, async_test_db, async_test_user):
"""Test checking if owner is admin (should be True)."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.OWNER,
is_active=True
)
session.add(user_org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
is_admin = await organization_async.is_user_org_admin(
session,
user_id=async_test_user.id,
organization_id=org_id
)
assert is_admin is True
@pytest.mark.asyncio
async def test_is_user_org_admin_admin_role(self, async_test_db, async_test_user):
"""Test checking if admin role is admin."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
session.add(org)
await session.commit()
user_org = UserOrganization(
user_id=async_test_user.id,
organization_id=org.id,
role=OrganizationRole.ADMIN,
is_active=True
)
session.add(user_org)
await session.commit()
org_id = org.id
async with AsyncTestingSessionLocal() as session:
is_admin = await organization_async.is_user_org_admin(
session,
user_id=async_test_user.id,
organization_id=org_id
)
assert is_admin is True
@pytest.mark.asyncio
async def test_is_user_org_admin_member_false(self, async_test_db, async_test_user):
"""Test checking if regular member is admin."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
org = Organization(name="Test Org", slug="test-org")
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
async with AsyncTestingSessionLocal() as session:
is_admin = await organization_async.is_user_org_admin(
session,
user_id=async_test_user.id,
organization_id=org_id
)
assert is_admin is False

View File

@@ -0,0 +1,339 @@
# tests/crud/test_session_async.py
"""
Comprehensive tests for async session CRUD operations.
"""
import pytest
from datetime import datetime, timedelta, timezone
from uuid import uuid4
from app.crud.session_async import session_async
from app.models.user_session import UserSession
from app.schemas.sessions import SessionCreate
class TestGetByJti:
"""Tests for get_by_jti method."""
@pytest.mark.asyncio
async def test_get_by_jti_success(self, async_test_db, async_test_user):
"""Test getting session by JTI."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti="test_jti_123",
device_name="Test Device",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add(user_session)
await session.commit()
async with AsyncTestingSessionLocal() as session:
result = await session_async.get_by_jti(session, jti="test_jti_123")
assert result is not None
assert result.refresh_token_jti == "test_jti_123"
@pytest.mark.asyncio
async def test_get_by_jti_not_found(self, async_test_db):
"""Test getting non-existent JTI returns None."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await session_async.get_by_jti(session, jti="nonexistent")
assert result is None
class TestGetActiveByJti:
"""Tests for get_active_by_jti method."""
@pytest.mark.asyncio
async def test_get_active_by_jti_success(self, async_test_db, async_test_user):
"""Test getting active session by JTI."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti="active_jti",
device_name="Test Device",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add(user_session)
await session.commit()
async with AsyncTestingSessionLocal() as session:
result = await session_async.get_active_by_jti(session, jti="active_jti")
assert result is not None
assert result.is_active is True
@pytest.mark.asyncio
async def test_get_active_by_jti_inactive(self, async_test_db, async_test_user):
"""Test getting inactive session by JTI returns None."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti="inactive_jti",
device_name="Test Device",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0",
is_active=False,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add(user_session)
await session.commit()
async with AsyncTestingSessionLocal() as session:
result = await session_async.get_active_by_jti(session, jti="inactive_jti")
assert result is None
class TestGetUserSessions:
"""Tests for get_user_sessions method."""
@pytest.mark.asyncio
async def test_get_user_sessions_active_only(self, async_test_db, async_test_user):
"""Test getting only active user sessions."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
active = UserSession(
user_id=async_test_user.id,
refresh_token_jti="active",
device_name="Active Device",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
inactive = UserSession(
user_id=async_test_user.id,
refresh_token_jti="inactive",
device_name="Inactive Device",
ip_address="192.168.1.2",
user_agent="Mozilla/5.0",
is_active=False,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add_all([active, inactive])
await session.commit()
async with AsyncTestingSessionLocal() as session:
results = await session_async.get_user_sessions(
session,
user_id=str(async_test_user.id),
active_only=True
)
assert len(results) == 1
assert results[0].is_active is True
@pytest.mark.asyncio
async def test_get_user_sessions_all(self, async_test_db, async_test_user):
"""Test getting all user sessions."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
for i in range(3):
sess = UserSession(
user_id=async_test_user.id,
refresh_token_jti=f"session_{i}",
device_name=f"Device {i}",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0",
is_active=i % 2 == 0,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add(sess)
await session.commit()
async with AsyncTestingSessionLocal() as session:
results = await session_async.get_user_sessions(
session,
user_id=str(async_test_user.id),
active_only=False
)
assert len(results) == 3
class TestCreateSession:
"""Tests for create_session method."""
@pytest.mark.asyncio
async def test_create_session_success(self, async_test_db, async_test_user):
"""Test successfully creating a session."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
session_data = SessionCreate(
user_id=async_test_user.id,
refresh_token_jti="new_jti",
device_name="New Device",
device_id="device_123",
ip_address="192.168.1.100",
user_agent="Mozilla/5.0",
last_used_at=datetime.now(timezone.utc),
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
location_city="San Francisco",
location_country="USA"
)
result = await session_async.create_session(session, obj_in=session_data)
assert result.user_id == async_test_user.id
assert result.refresh_token_jti == "new_jti"
assert result.is_active is True
assert result.location_city == "San Francisco"
class TestDeactivate:
"""Tests for deactivate method."""
@pytest.mark.asyncio
async def test_deactivate_success(self, async_test_db, async_test_user):
"""Test successfully deactivating a session."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti="to_deactivate",
device_name="Test Device",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add(user_session)
await session.commit()
session_id = user_session.id
async with AsyncTestingSessionLocal() as session:
result = await session_async.deactivate(session, session_id=str(session_id))
assert result is not None
assert result.is_active is False
@pytest.mark.asyncio
async def test_deactivate_not_found(self, async_test_db):
"""Test deactivating non-existent session returns None."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await session_async.deactivate(session, session_id=str(uuid4()))
assert result is None
class TestDeactivateAllUserSessions:
"""Tests for deactivate_all_user_sessions method."""
@pytest.mark.asyncio
async def test_deactivate_all_user_sessions_success(self, async_test_db, async_test_user):
"""Test deactivating all user sessions."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
for i in range(5):
sess = UserSession(
user_id=async_test_user.id,
refresh_token_jti=f"bulk_{i}",
device_name=f"Device {i}",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add(sess)
await session.commit()
async with AsyncTestingSessionLocal() as session:
count = await session_async.deactivate_all_user_sessions(
session,
user_id=str(async_test_user.id)
)
assert count == 5
class TestUpdateLastUsed:
"""Tests for update_last_used method."""
@pytest.mark.asyncio
async def test_update_last_used_success(self, async_test_db, async_test_user):
"""Test updating last_used_at timestamp."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user_session = UserSession(
user_id=async_test_user.id,
refresh_token_jti="update_test",
device_name="Test Device",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc) - timedelta(hours=1)
)
session.add(user_session)
await session.commit()
await session.refresh(user_session)
old_time = user_session.last_used_at
result = await session_async.update_last_used(session, session=user_session)
assert result.last_used_at > old_time
class TestGetUserSessionCount:
"""Tests for get_user_session_count method."""
@pytest.mark.asyncio
async def test_get_user_session_count_success(self, async_test_db, async_test_user):
"""Test getting user session count."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
for i in range(3):
sess = UserSession(
user_id=async_test_user.id,
refresh_token_jti=f"count_{i}",
device_name=f"Device {i}",
ip_address="192.168.1.1",
user_agent="Mozilla/5.0",
is_active=True,
expires_at=datetime.now(timezone.utc) + timedelta(days=7),
last_used_at=datetime.now(timezone.utc)
)
session.add(sess)
await session.commit()
async with AsyncTestingSessionLocal() as session:
count = await session_async.get_user_session_count(
session,
user_id=str(async_test_user.id)
)
assert count == 3
@pytest.mark.asyncio
async def test_get_user_session_count_empty(self, async_test_db):
"""Test getting session count for user with no sessions."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
count = await session_async.get_user_session_count(
session,
user_id=str(uuid4())
)
assert count == 0

View File

@@ -0,0 +1,644 @@
# tests/crud/test_user_async.py
"""
Comprehensive tests for async user CRUD operations.
"""
import pytest
from datetime import datetime, timezone
from uuid import uuid4
from app.crud.user_async import user_async
from app.models.user import User
from app.schemas.users import UserCreate, UserUpdate
class TestGetByEmail:
"""Tests for get_by_email method."""
@pytest.mark.asyncio
async def test_get_by_email_success(self, async_test_db, async_test_user):
"""Test getting user by email."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await user_async.get_by_email(session, email=async_test_user.email)
assert result is not None
assert result.email == async_test_user.email
assert result.id == async_test_user.id
@pytest.mark.asyncio
async def test_get_by_email_not_found(self, async_test_db):
"""Test getting non-existent email returns None."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
result = await user_async.get_by_email(session, email="nonexistent@example.com")
assert result is None
class TestCreate:
"""Tests for create method."""
@pytest.mark.asyncio
async def test_create_user_success(self, async_test_db):
"""Test successfully creating a user."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user_data = UserCreate(
email="newuser@example.com",
password="SecurePass123!",
first_name="New",
last_name="User",
phone_number="+1234567890"
)
result = await user_async.create(session, obj_in=user_data)
assert result.email == "newuser@example.com"
assert result.first_name == "New"
assert result.last_name == "User"
assert result.phone_number == "+1234567890"
assert result.is_active is True
assert result.is_superuser is False
assert result.password_hash is not None
assert result.password_hash != "SecurePass123!" # Password should be hashed
@pytest.mark.asyncio
async def test_create_superuser_success(self, async_test_db):
"""Test creating a superuser."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user_data = UserCreate(
email="superuser@example.com",
password="SuperPass123!",
first_name="Super",
last_name="User",
is_superuser=True
)
result = await user_async.create(session, obj_in=user_data)
assert result.is_superuser is True
assert result.email == "superuser@example.com"
@pytest.mark.asyncio
async def test_create_duplicate_email_fails(self, async_test_db, async_test_user):
"""Test creating user with duplicate email raises ValueError."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user_data = UserCreate(
email=async_test_user.email, # Duplicate email
password="AnotherPass123!",
first_name="Duplicate",
last_name="User"
)
with pytest.raises(ValueError) as exc_info:
await user_async.create(session, obj_in=user_data)
assert "already exists" in str(exc_info.value).lower()
class TestUpdate:
"""Tests for update method."""
@pytest.mark.asyncio
async def test_update_user_basic_fields(self, async_test_db, async_test_user):
"""Test updating basic user fields."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
# Get fresh copy of user
user = await user_async.get(session, id=str(async_test_user.id))
update_data = UserUpdate(
first_name="Updated",
last_name="Name",
phone_number="+9876543210"
)
result = await user_async.update(session, db_obj=user, obj_in=update_data)
assert result.first_name == "Updated"
assert result.last_name == "Name"
assert result.phone_number == "+9876543210"
@pytest.mark.asyncio
async def test_update_user_password(self, async_test_db):
"""Test updating user password."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create a fresh user for this test
async with AsyncTestingSessionLocal() as session:
user_data = UserCreate(
email="passwordtest@example.com",
password="OldPassword123!",
first_name="Pass",
last_name="Test"
)
user = await user_async.create(session, obj_in=user_data)
user_id = user.id
old_password_hash = user.password_hash
# Update the password
async with AsyncTestingSessionLocal() as session:
user = await user_async.get(session, id=str(user_id))
update_data = UserUpdate(password="NewDifferentPassword123!")
result = await user_async.update(session, db_obj=user, obj_in=update_data)
await session.refresh(result)
assert result.password_hash != old_password_hash
assert result.password_hash is not None
assert "NewDifferentPassword123!" not in result.password_hash # Should be hashed
@pytest.mark.asyncio
async def test_update_user_with_dict(self, async_test_db, async_test_user):
"""Test updating user with dictionary."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user = await user_async.get(session, id=str(async_test_user.id))
update_dict = {"first_name": "DictUpdate"}
result = await user_async.update(session, db_obj=user, obj_in=update_dict)
assert result.first_name == "DictUpdate"
class TestGetMultiWithTotal:
"""Tests for get_multi_with_total method."""
@pytest.mark.asyncio
async def test_get_multi_with_total_basic(self, async_test_db, async_test_user):
"""Test basic pagination."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
users, total = await user_async.get_multi_with_total(
session,
skip=0,
limit=10
)
assert total >= 1
assert len(users) >= 1
assert any(u.id == async_test_user.id for u in users)
@pytest.mark.asyncio
async def test_get_multi_with_total_sorting_asc(self, async_test_db):
"""Test sorting in ascending order."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create multiple users
async with AsyncTestingSessionLocal() as session:
for i in range(3):
user_data = UserCreate(
email=f"sort{i}@example.com",
password="SecurePass123!",
first_name=f"User{i}",
last_name="Test"
)
await user_async.create(session, obj_in=user_data)
async with AsyncTestingSessionLocal() as session:
users, total = await user_async.get_multi_with_total(
session,
skip=0,
limit=10,
sort_by="email",
sort_order="asc"
)
# Check if sorted (at least the test users)
test_users = [u for u in users if u.email.startswith("sort")]
if len(test_users) > 1:
assert test_users[0].email < test_users[1].email
@pytest.mark.asyncio
async def test_get_multi_with_total_sorting_desc(self, async_test_db):
"""Test sorting in descending order."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create multiple users
async with AsyncTestingSessionLocal() as session:
for i in range(3):
user_data = UserCreate(
email=f"desc{i}@example.com",
password="SecurePass123!",
first_name=f"User{i}",
last_name="Test"
)
await user_async.create(session, obj_in=user_data)
async with AsyncTestingSessionLocal() as session:
users, total = await user_async.get_multi_with_total(
session,
skip=0,
limit=10,
sort_by="email",
sort_order="desc"
)
# Check if sorted descending (at least the test users)
test_users = [u for u in users if u.email.startswith("desc")]
if len(test_users) > 1:
assert test_users[0].email > test_users[1].email
@pytest.mark.asyncio
async def test_get_multi_with_total_filtering(self, async_test_db):
"""Test filtering by field."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create active and inactive users
async with AsyncTestingSessionLocal() as session:
active_user = UserCreate(
email="active@example.com",
password="SecurePass123!",
first_name="Active",
last_name="User"
)
await user_async.create(session, obj_in=active_user)
inactive_user = UserCreate(
email="inactive@example.com",
password="SecurePass123!",
first_name="Inactive",
last_name="User"
)
created_inactive = await user_async.create(session, obj_in=inactive_user)
# Deactivate the user
await user_async.update(
session,
db_obj=created_inactive,
obj_in={"is_active": False}
)
async with AsyncTestingSessionLocal() as session:
users, total = await user_async.get_multi_with_total(
session,
skip=0,
limit=100,
filters={"is_active": True}
)
# All returned users should be active
assert all(u.is_active for u in users)
@pytest.mark.asyncio
async def test_get_multi_with_total_search(self, async_test_db):
"""Test search functionality."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create user with unique name
async with AsyncTestingSessionLocal() as session:
user_data = UserCreate(
email="searchable@example.com",
password="SecurePass123!",
first_name="Searchable",
last_name="UserName"
)
await user_async.create(session, obj_in=user_data)
async with AsyncTestingSessionLocal() as session:
users, total = await user_async.get_multi_with_total(
session,
skip=0,
limit=100,
search="Searchable"
)
assert total >= 1
assert any(u.first_name == "Searchable" for u in users)
@pytest.mark.asyncio
async def test_get_multi_with_total_pagination(self, async_test_db):
"""Test pagination with skip and limit."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create multiple users
async with AsyncTestingSessionLocal() as session:
for i in range(5):
user_data = UserCreate(
email=f"page{i}@example.com",
password="SecurePass123!",
first_name=f"Page{i}",
last_name="User"
)
await user_async.create(session, obj_in=user_data)
async with AsyncTestingSessionLocal() as session:
# Get first page
users_page1, total = await user_async.get_multi_with_total(
session,
skip=0,
limit=2
)
# Get second page
users_page2, total2 = await user_async.get_multi_with_total(
session,
skip=2,
limit=2
)
# Total should be same
assert total == total2
# Different users on different pages
assert users_page1[0].id != users_page2[0].id
@pytest.mark.asyncio
async def test_get_multi_with_total_validation_negative_skip(self, async_test_db):
"""Test validation fails for negative skip."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
with pytest.raises(ValueError) as exc_info:
await user_async.get_multi_with_total(session, skip=-1, limit=10)
assert "skip must be non-negative" in str(exc_info.value)
@pytest.mark.asyncio
async def test_get_multi_with_total_validation_negative_limit(self, async_test_db):
"""Test validation fails for negative limit."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
with pytest.raises(ValueError) as exc_info:
await user_async.get_multi_with_total(session, skip=0, limit=-1)
assert "limit must be non-negative" in str(exc_info.value)
@pytest.mark.asyncio
async def test_get_multi_with_total_validation_max_limit(self, async_test_db):
"""Test validation fails for limit > 1000."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
with pytest.raises(ValueError) as exc_info:
await user_async.get_multi_with_total(session, skip=0, limit=1001)
assert "Maximum limit is 1000" in str(exc_info.value)
class TestBulkUpdateStatus:
"""Tests for bulk_update_status method."""
@pytest.mark.asyncio
async def test_bulk_update_status_success(self, async_test_db):
"""Test bulk updating user status."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create multiple users
user_ids = []
async with AsyncTestingSessionLocal() as session:
for i in range(3):
user_data = UserCreate(
email=f"bulk{i}@example.com",
password="SecurePass123!",
first_name=f"Bulk{i}",
last_name="User"
)
user = await user_async.create(session, obj_in=user_data)
user_ids.append(user.id)
# Bulk deactivate
async with AsyncTestingSessionLocal() as session:
count = await user_async.bulk_update_status(
session,
user_ids=user_ids,
is_active=False
)
assert count == 3
# Verify all are inactive
async with AsyncTestingSessionLocal() as session:
for user_id in user_ids:
user = await user_async.get(session, id=str(user_id))
assert user.is_active is False
@pytest.mark.asyncio
async def test_bulk_update_status_empty_list(self, async_test_db):
"""Test bulk update with empty list returns 0."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
count = await user_async.bulk_update_status(
session,
user_ids=[],
is_active=False
)
assert count == 0
@pytest.mark.asyncio
async def test_bulk_update_status_reactivate(self, async_test_db):
"""Test bulk reactivating users."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create inactive user
async with AsyncTestingSessionLocal() as session:
user_data = UserCreate(
email="reactivate@example.com",
password="SecurePass123!",
first_name="Reactivate",
last_name="User"
)
user = await user_async.create(session, obj_in=user_data)
# Deactivate
await user_async.update(session, db_obj=user, obj_in={"is_active": False})
user_id = user.id
# Reactivate
async with AsyncTestingSessionLocal() as session:
count = await user_async.bulk_update_status(
session,
user_ids=[user_id],
is_active=True
)
assert count == 1
# Verify active
async with AsyncTestingSessionLocal() as session:
user = await user_async.get(session, id=str(user_id))
assert user.is_active is True
class TestBulkSoftDelete:
"""Tests for bulk_soft_delete method."""
@pytest.mark.asyncio
async def test_bulk_soft_delete_success(self, async_test_db):
"""Test bulk soft deleting users."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create multiple users
user_ids = []
async with AsyncTestingSessionLocal() as session:
for i in range(3):
user_data = UserCreate(
email=f"delete{i}@example.com",
password="SecurePass123!",
first_name=f"Delete{i}",
last_name="User"
)
user = await user_async.create(session, obj_in=user_data)
user_ids.append(user.id)
# Bulk delete
async with AsyncTestingSessionLocal() as session:
count = await user_async.bulk_soft_delete(
session,
user_ids=user_ids
)
assert count == 3
# Verify all are soft deleted
async with AsyncTestingSessionLocal() as session:
for user_id in user_ids:
user = await user_async.get(session, id=str(user_id))
assert user.deleted_at is not None
assert user.is_active is False
@pytest.mark.asyncio
async def test_bulk_soft_delete_with_exclusion(self, async_test_db):
"""Test bulk soft delete with excluded user."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create multiple users
user_ids = []
async with AsyncTestingSessionLocal() as session:
for i in range(3):
user_data = UserCreate(
email=f"exclude{i}@example.com",
password="SecurePass123!",
first_name=f"Exclude{i}",
last_name="User"
)
user = await user_async.create(session, obj_in=user_data)
user_ids.append(user.id)
# Bulk delete, excluding first user
exclude_id = user_ids[0]
async with AsyncTestingSessionLocal() as session:
count = await user_async.bulk_soft_delete(
session,
user_ids=user_ids,
exclude_user_id=exclude_id
)
assert count == 2 # Only 2 deleted
# Verify excluded user is NOT deleted
async with AsyncTestingSessionLocal() as session:
excluded_user = await user_async.get(session, id=str(exclude_id))
assert excluded_user.deleted_at is None
@pytest.mark.asyncio
async def test_bulk_soft_delete_empty_list(self, async_test_db):
"""Test bulk delete with empty list returns 0."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
count = await user_async.bulk_soft_delete(
session,
user_ids=[]
)
assert count == 0
@pytest.mark.asyncio
async def test_bulk_soft_delete_all_excluded(self, async_test_db):
"""Test bulk delete where all users are excluded."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create user
async with AsyncTestingSessionLocal() as session:
user_data = UserCreate(
email="onlyuser@example.com",
password="SecurePass123!",
first_name="Only",
last_name="User"
)
user = await user_async.create(session, obj_in=user_data)
user_id = user.id
# Try to delete but exclude
async with AsyncTestingSessionLocal() as session:
count = await user_async.bulk_soft_delete(
session,
user_ids=[user_id],
exclude_user_id=user_id
)
assert count == 0
@pytest.mark.asyncio
async def test_bulk_soft_delete_already_deleted(self, async_test_db):
"""Test bulk delete doesn't re-delete already deleted users."""
test_engine, AsyncTestingSessionLocal = async_test_db
# Create and delete user
async with AsyncTestingSessionLocal() as session:
user_data = UserCreate(
email="predeleted@example.com",
password="SecurePass123!",
first_name="PreDeleted",
last_name="User"
)
user = await user_async.create(session, obj_in=user_data)
user_id = user.id
# First deletion
await user_async.bulk_soft_delete(session, user_ids=[user_id])
# Try to delete again
async with AsyncTestingSessionLocal() as session:
count = await user_async.bulk_soft_delete(
session,
user_ids=[user_id]
)
assert count == 0 # Already deleted
class TestUtilityMethods:
"""Tests for utility methods."""
@pytest.mark.asyncio
async def test_is_active_true(self, async_test_db, async_test_user):
"""Test is_active returns True for active user."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user = await user_async.get(session, id=str(async_test_user.id))
assert user_async.is_active(user) is True
@pytest.mark.asyncio
async def test_is_active_false(self, async_test_db):
"""Test is_active returns False for inactive user."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user_data = UserCreate(
email="inactive2@example.com",
password="SecurePass123!",
first_name="Inactive",
last_name="User"
)
user = await user_async.create(session, obj_in=user_data)
await user_async.update(session, db_obj=user, obj_in={"is_active": False})
assert user_async.is_active(user) is False
@pytest.mark.asyncio
async def test_is_superuser_true(self, async_test_db, async_test_superuser):
"""Test is_superuser returns True for superuser."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user = await user_async.get(session, id=str(async_test_superuser.id))
assert user_async.is_superuser(user) is True
@pytest.mark.asyncio
async def test_is_superuser_false(self, async_test_db, async_test_user):
"""Test is_superuser returns False for regular user."""
test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
user = await user_async.get(session, id=str(async_test_user.id))
assert user_async.is_superuser(user) is False