Files
leocrm/tests/test_permission_performance.py

289 lines
11 KiB
Python

"""Performance tests for permission system — visibility filter, caching.
Covers:
- Visibility filter performance with 1000 mock contacts
- Cache hit vs miss performance comparison
"""
from __future__ import annotations
import time
import uuid
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.contact import Contact
from app.services import entity_permission_service as eps
from tests.conftest import seed_tenant_and_users
@pytest.mark.asyncio
class TestPermissionPerformance:
"""Performance tests for permission system."""
async def test_visibility_filter_performance(self, db_session: AsyncSession):
"""get_visible_ids with 1000 contacts completes in reasonable time."""
seed = await seed_tenant_and_users(db_session)
tenant_id = seed["tenant_a"].id
user_id = seed["admin_a"].id
# Create 1000 contacts owned by the user
contacts = []
for i in range(1000):
contacts.append(Contact(
tenant_id=tenant_id,
type="person",
firstname=f"Perf{i}",
surname=f"Test{i}",
owner_id=user_id,
created_by=user_id,
updated_by=user_id,
))
db_session.add_all(contacts)
await db_session.commit()
# Measure time for get_visible_ids
start = time.perf_counter()
visible, access_map = await eps.get_visible_ids(
db_session, tenant_id, user_id, "contact"
)
elapsed = time.perf_counter() - start
# Should complete in under 2 seconds for 1000 contacts
assert elapsed < 2.0, f"get_visible_ids took {elapsed:.3f}s (expected <2.0s)"
assert len(visible) >= 1000, f"Expected >=1000 visible, got {len(visible)}"
# All should be at 'owner' level
for eid, level in access_map.items():
if eid in [c.id for c in contacts]:
assert level == "owner", f"Expected 'owner', got '{level}'"
async def test_cache_hit_vs_miss(self, db_session: AsyncSession, redis_client):
"""Cache hit is significantly faster than cache miss."""
seed = await seed_tenant_and_users(db_session)
tenant_id = seed["tenant_a"].id
user_id = seed["admin_a"].id
# Create 100 contacts owned by the user
contacts = []
for i in range(100):
contacts.append(Contact(
tenant_id=tenant_id,
type="person",
firstname=f"Cache{i}",
surname=f"Test{i}",
owner_id=user_id,
created_by=user_id,
updated_by=user_id,
))
db_session.add_all(contacts)
await db_session.commit()
# First call — cache miss (populates cache)
miss_start = time.perf_counter()
visible_miss, access_map_miss = await eps.get_cached_visible_ids(
db_session, redis_client, tenant_id, user_id, "contact"
)
miss_elapsed = time.perf_counter() - miss_start
assert len(visible_miss) >= 100, f"Expected >=100 visible, got {len(visible_miss)}"
# Second call — cache hit
hit_start = time.perf_counter()
visible_hit, access_map_hit = await eps.get_cached_visible_ids(
db_session, redis_client, tenant_id, user_id, "contact"
)
hit_elapsed = time.perf_counter() - hit_start
# Cache hit should be faster (at least 2x speedup)
assert hit_elapsed < miss_elapsed, f"Cache hit {hit_elapsed:.4f}s should be faster than miss {miss_elapsed:.4f}s"
assert len(visible_hit) == len(visible_miss), "Cache hit should return same results"
async def test_batch_resolution_performance(self, db_session: AsyncSession):
"""batch_get_effective_access with 100 entities completes quickly."""
seed = await seed_tenant_and_users(db_session)
tenant_id = seed["tenant_a"].id
user_id = seed["admin_a"].id
# Create 100 contacts
contacts = []
for i in range(100):
contacts.append(Contact(
tenant_id=tenant_id,
type="person",
firstname=f"Batch{i}",
surname=f"Test{i}",
owner_id=user_id,
created_by=user_id,
updated_by=user_id,
))
db_session.add_all(contacts)
await db_session.commit()
entity_ids = [c.id for c in contacts]
start = time.perf_counter()
result = await eps.batch_get_effective_access(
db_session, tenant_id, user_id, "contact", entity_ids
)
elapsed = time.perf_counter() - start
assert elapsed < 1.0, f"batch_get_effective_access took {elapsed:.3f}s (expected <1.0s)"
assert len(result) == 100, f"Expected 100 results, got {len(result)}"
# All should be 'owner'
for eid, level in result.items():
assert level == "owner", f"Expected 'owner', got '{level}'"
async def test_cache_invalidation_performance(self, db_session: AsyncSession, redis_client):
"""Cache invalidation for all entity types completes quickly."""
seed = await seed_tenant_and_users(db_session)
tenant_id = seed["tenant_a"].id
user_id = seed["admin_a"].id
# Populate cache for multiple entity types
for entity_type in ["contact", "company", "attachment", "entity_attachment", "workflow"]:
await eps.get_cached_visible_ids(
db_session, redis_client, tenant_id, user_id, entity_type
)
# Measure invalidation time
start = time.perf_counter()
await eps.invalidate_all_user_entity_cache(redis_client, tenant_id, user_id)
elapsed = time.perf_counter() - start
assert elapsed < 1.0, f"Cache invalidation took {elapsed:.3f}s (expected <1.0s)"
# Verify cache is cleared
for entity_type in ["contact", "company", "attachment", "entity_attachment", "workflow"]:
cache_key = f"ep_vis:{user_id}:{tenant_id}:{entity_type}"
cached = await redis_client.get(cache_key)
assert cached is None, f"Cache for {entity_type} should be cleared"
async def test_get_effective_access_performance(self, db_session: AsyncSession):
"""get_effective_access for a single entity completes quickly."""
seed = await seed_tenant_and_users(db_session)
tenant_id = seed["tenant_a"].id
user_id = seed["admin_a"].id
contact_id = seed["company_a"].id
# Set owner_id
contact = await db_session.get(Contact, contact_id)
contact.owner_id = user_id
await db_session.commit()
# Measure single access check
start = time.perf_counter()
for _ in range(100):
access = await eps.get_effective_access(
db_session, tenant_id, user_id, "contact", contact_id
)
elapsed = time.perf_counter() - start
avg_ms = (elapsed / 100) * 1000
assert avg_ms < 50, f"Average get_effective_access took {avg_ms:.2f}ms (expected <50ms)"
assert access == "owner"
async def test_check_entity_access_performance(self, db_session: AsyncSession):
"""check_entity_access for a single entity completes quickly."""
seed = await seed_tenant_and_users(db_session)
tenant_id = seed["tenant_a"].id
user_id = seed["admin_a"].id
contact_id = seed["company_a"].id
# Set owner_id
contact = await db_session.get(Contact, contact_id)
contact.owner_id = user_id
await db_session.commit()
# Measure single access check
start = time.perf_counter()
for _ in range(100):
result = await eps.check_entity_access(
db_session, tenant_id, user_id, "contact", contact_id, "read"
)
elapsed = time.perf_counter() - start
avg_ms = (elapsed / 100) * 1000
assert avg_ms < 50, f"Average check_entity_access took {avg_ms:.2f}ms (expected <50ms)"
assert result is True
async def test_permission_creation_performance(self, db_session: AsyncSession):
"""Creating permissions in bulk completes quickly."""
seed = await seed_tenant_and_users(db_session)
tenant_id = seed["tenant_a"].id
owner_id = seed["admin_a"].id
viewer_id = seed["viewer_a"].id
contact_id = seed["company_a"].id
# Set owner_id
contact = await db_session.get(Contact, contact_id)
contact.owner_id = owner_id
await db_session.commit()
# Measure bulk permission creation
start = time.perf_counter()
for level in ["read", "write", "admin", "delete"]:
await eps.create_permission(
db_session,
tenant_id=tenant_id,
entity_type="contact",
entity_id=str(contact_id),
principal_type="user",
principal_id=str(viewer_id),
permission_level=level,
created_by=owner_id,
)
elapsed = time.perf_counter() - start
avg_ms = (elapsed / 4) * 1000
assert avg_ms < 200, f"Average permission creation took {avg_ms:.2f}ms (expected <200ms)"
async def test_visible_ids_with_mixed_ownership(self, db_session: AsyncSession):
"""get_visible_ids with mixed ownership (owned + tenant + shared) performs well."""
seed = await seed_tenant_and_users(db_session)
tenant_id = seed["tenant_a"].id
owner_id = seed["admin_a"].id
viewer_id = seed["viewer_a"].id
# Create 500 owned, 300 tenant-owned, 200 shared contacts
contacts = []
for i in range(500):
contacts.append(Contact(
tenant_id=tenant_id, type="person",
firstname=f"Owned{i}", surname=f"Test{i}",
owner_id=owner_id, created_by=owner_id, updated_by=owner_id,
))
for i in range(300):
contacts.append(Contact(
tenant_id=tenant_id, type="person",
firstname=f"Tenant{i}", surname=f"Test{i}",
owner_id=None, created_by=owner_id, updated_by=owner_id,
))
db_session.add_all(contacts)
await db_session.commit()
# Grant viewer access to 200 contacts
shared_ids = [c.id for c in contacts[:200]]
for cid in shared_ids:
await eps.create_permission(
db_session, tenant_id=tenant_id,
entity_type="contact", entity_id=str(cid),
principal_type="user", principal_id=str(viewer_id),
permission_level="read", created_by=owner_id,
)
# Measure performance for viewer
start = time.perf_counter()
visible, access_map = await eps.get_visible_ids(
db_session, tenant_id, viewer_id, "contact"
)
elapsed = time.perf_counter() - start
assert elapsed < 3.0, f"get_visible_ids with mixed ownership took {elapsed:.3f}s (expected <3.0s)"
# Viewer should see: 200 shared + 300 tenant-owned = 500
assert len(visible) >= 500, f"Expected >=500 visible, got {len(visible)}"