diff --git a/smoothschedule/pyproject.toml b/smoothschedule/pyproject.toml index 932b341..2ed55d2 100644 --- a/smoothschedule/pyproject.toml +++ b/smoothschedule/pyproject.toml @@ -146,6 +146,7 @@ dev = [ "pytest==9.0.1", "pytest-django==4.11.1", "pytest-sugar==1.1.1", + "pytest-xdist>=3.5.0", "ruff==0.14.6", "sphinx==8.2.3", "sphinx-autobuild==2025.8.25", diff --git a/smoothschedule/smoothschedule/conftest.py b/smoothschedule/smoothschedule/conftest.py index 3b606a2..a86d0ea 100644 --- a/smoothschedule/smoothschedule/conftest.py +++ b/smoothschedule/smoothschedule/conftest.py @@ -18,3 +18,56 @@ def user(db) -> User: For unit tests, use create_mock_user() from factories.py instead. """ return UserFactory() + + +# ============================================================================= +# Shared Tenant Fixtures (Session-scoped for performance) +# ============================================================================= +# Creating a tenant in django-tenants runs all migrations (~40 seconds). +# These fixtures are session-scoped to avoid recreating tenants for each test. + +_shared_tenant_cache = {} + + +@pytest.fixture(scope="session") +def shared_tenant(django_db_setup, django_db_blocker): + """ + Session-scoped tenant fixture for tests that need a real tenant. + + This tenant is created ONCE per test session and reused across all tests. + Use this instead of creating tenants in individual tests to avoid the + ~40 second migration overhead per tenant. + + Usage: + @pytest.mark.django_db + def test_something(shared_tenant): + user = User(tenant=shared_tenant, ...) + """ + from smoothschedule.identity.core.models import Tenant + + with django_db_blocker.unblock(): + # Check if tenant already exists from a previous run (--reuse-db) + tenant = Tenant.objects.filter(schema_name="sharedtest").first() + if not tenant: + tenant = Tenant.objects.create( + name="Shared Test Business", + schema_name="sharedtest" + ) + return tenant + + +@pytest.fixture(scope="session") +def second_shared_tenant(django_db_setup, django_db_blocker): + """ + Second session-scoped tenant for tests that need multiple tenants. + """ + from smoothschedule.identity.core.models import Tenant + + with django_db_blocker.unblock(): + tenant = Tenant.objects.filter(schema_name="sharedtest2").first() + if not tenant: + tenant = Tenant.objects.create( + name="Shared Test Business 2", + schema_name="sharedtest2" + ) + return tenant diff --git a/smoothschedule/smoothschedule/identity/users/tests/test_models.py b/smoothschedule/smoothschedule/identity/users/tests/test_models.py index 74935b7..3ced6dc 100644 --- a/smoothschedule/smoothschedule/identity/users/tests/test_models.py +++ b/smoothschedule/smoothschedule/identity/users/tests/test_models.py @@ -341,45 +341,32 @@ class TestGetAccessibleTenants: """ Test get_accessible_tenants() method. - Note: These tests use database access because the method accesses - ForeignKey relationships which trigger database queries even with mocking. + Uses shared tenant fixtures (session-scoped) to avoid ~40s migration overhead + per tenant creation. See conftest.py for fixture definitions. """ @pytest.mark.django_db - def test_returns_all_tenants_for_platform_user(self): - # Arrange - from smoothschedule.identity.core.models import Tenant - import uuid - - # Create a couple of tenants - unique_id1 = str(uuid.uuid4())[:8] - Tenant.objects.create(name=f"Tenant1 {unique_id1}", schema_name=f"tenant1{unique_id1}") - - unique_id2 = str(uuid.uuid4())[:8] - Tenant.objects.create(name=f"Tenant2 {unique_id2}", schema_name=f"tenant2{unique_id2}") - + def test_returns_all_tenants_for_platform_user(self, shared_tenant, second_shared_tenant): + # Arrange - use shared fixtures instead of creating new tenants user = create_user_instance(User.Role.PLATFORM_MANAGER) # Act result = user.get_accessible_tenants() - # Assert - assert result.count() >= 2 # At least the two we created + # Assert - at least our two shared tenants exist + assert result.count() >= 2 @pytest.mark.django_db - def test_returns_single_tenant_for_tenant_user(self): - # Arrange - from smoothschedule.identity.core.models import Tenant + def test_returns_single_tenant_for_tenant_user(self, shared_tenant): + # Arrange - use shared fixture import uuid - unique_id = str(uuid.uuid4())[:8] - tenant = Tenant.objects.create(name=f"My Business {unique_id}", schema_name=f"mybiz{unique_id}") user = User( username=f"owner{unique_id}", email=f"owner{unique_id}@test.com", role=User.Role.TENANT_OWNER, - tenant=tenant + tenant=shared_tenant ) user.save() @@ -388,7 +375,7 @@ class TestGetAccessibleTenants: # Assert assert result.count() == 1 - assert result.first() == tenant + assert result.first() == shared_tenant @pytest.mark.django_db def test_returns_empty_queryset_for_tenant_user_without_tenant(self): @@ -482,23 +469,16 @@ class TestSaveMethodValidation: assert user.is_superuser is True @pytest.mark.django_db - def test_clears_tenant_for_platform_users(self): - # Arrange - from smoothschedule.identity.core.models import Tenant + def test_clears_tenant_for_platform_users(self, shared_tenant): + # Arrange - use shared fixture to avoid ~40s migration overhead import uuid - - # Create a tenant first with unique schema_name unique_id = str(uuid.uuid4())[:8] - tenant = Tenant.objects.create( - name=f"Test Business {unique_id}", - schema_name=f"testbiz{unique_id}" - ) user = User( username=f"platformuser{unique_id}", email=f"platform{unique_id}@example.com", role=User.Role.PLATFORM_MANAGER, - tenant=tenant # Should be cleared + tenant=shared_tenant # Should be cleared ) # Act @@ -527,55 +507,43 @@ class TestSaveMethodValidation: assert "must be assigned to a tenant" in str(exc_info.value) @pytest.mark.django_db - def test_allows_tenant_user_with_tenant(self): - # Arrange - from smoothschedule.identity.core.models import Tenant + def test_allows_tenant_user_with_tenant(self, shared_tenant): + # Arrange - use shared fixture to avoid ~40s migration overhead import uuid - unique_id = str(uuid.uuid4())[:8] - tenant = Tenant.objects.create( - name=f"Test Business {unique_id}", - schema_name=f"testbiz{unique_id}" - ) user = User( username=f"owner{unique_id}", email=f"owner{unique_id}@testbiz.com", role=User.Role.TENANT_OWNER, - tenant=tenant + tenant=shared_tenant ) # Act user.save() # Assert - assert user.tenant == tenant + assert user.tenant == shared_tenant assert user.id is not None @pytest.mark.django_db - def test_allows_customer_with_tenant(self): - # Arrange - from smoothschedule.identity.core.models import Tenant + def test_allows_customer_with_tenant(self, shared_tenant): + # Arrange - use shared fixture to avoid ~40s migration overhead import uuid - unique_id = str(uuid.uuid4())[:8] - tenant = Tenant.objects.create( - name=f"Test Business {unique_id}", - schema_name=f"testbiz{unique_id}" - ) user = User( username=f"customer{unique_id}", email=f"customer{unique_id}@example.com", role=User.Role.CUSTOMER, - tenant=tenant + tenant=shared_tenant ) # Act user.save() # Assert - assert user.tenant == tenant + assert user.tenant == shared_tenant # ============================================================================= diff --git a/smoothschedule/smoothschedule/identity/users/tests/test_user_model.py b/smoothschedule/smoothschedule/identity/users/tests/test_user_model.py index 34cee0a..7e6b524 100644 --- a/smoothschedule/smoothschedule/identity/users/tests/test_user_model.py +++ b/smoothschedule/smoothschedule/identity/users/tests/test_user_model.py @@ -494,29 +494,18 @@ class TestGetAccessibleTenants: assert result == mock_queryset @pytest.mark.django_db - def test_returns_single_tenant_for_tenant_user(self): - # This test requires DB to create a real Tenant instance - # because Django's ForeignKey and ORM make mocking too complex - from smoothschedule.identity.core.models import Tenant - import uuid - - # Create a real tenant - unique_id = str(uuid.uuid4())[:8] - tenant = Tenant.objects.create( - name=f'Test Business {unique_id}', - schema_name=f'testbiz{unique_id}' - ) - + def test_returns_single_tenant_for_tenant_user(self, shared_tenant): + # Use shared fixture to avoid ~40s migration overhead per tenant # Create user with that tenant user = create_user_instance(User.Role.TENANT_OWNER) - user.tenant = tenant + user.tenant = shared_tenant # Act result = user.get_accessible_tenants() # Assert assert result.count() == 1 - assert list(result)[0] == tenant + assert list(result)[0] == shared_tenant def test_returns_empty_queryset_for_tenant_user_without_tenant(self): # Arrange @@ -633,22 +622,16 @@ class TestSaveMethodValidation: assert user.is_superuser is True @pytest.mark.django_db - def test_clears_tenant_for_platform_users(self): - from smoothschedule.identity.core.models import Tenant + def test_clears_tenant_for_platform_users(self, shared_tenant): + # Use shared fixture to avoid ~40s migration overhead per tenant import uuid - - # Create unique schema name to avoid collisions unique_id = str(uuid.uuid4())[:8] - tenant = Tenant.objects.create( - name=f'Test Business {unique_id}', - schema_name=f'testbiz{unique_id}' - ) user = User( - username='platformuser', - email='platform@example.com', + username=f'platformuser{unique_id}', + email=f'platform{unique_id}@example.com', role=User.Role.PLATFORM_MANAGER, - tenant=tenant # Should be cleared + tenant=shared_tenant # Should be cleared ) user.save() @@ -669,49 +652,37 @@ class TestSaveMethodValidation: assert 'must be assigned to a tenant' in str(exc_info.value) @pytest.mark.django_db - def test_allows_tenant_user_with_tenant(self): - from smoothschedule.identity.core.models import Tenant + def test_allows_tenant_user_with_tenant(self, shared_tenant): + # Use shared fixture to avoid ~40s migration overhead per tenant import uuid - - # Create unique schema name to avoid collisions unique_id = str(uuid.uuid4())[:8] - tenant = Tenant.objects.create( - name=f'Test Business {unique_id}', - schema_name=f'testbiz{unique_id}' - ) user = User( - username='owner', - email='owner@testbiz.com', + username=f'owner{unique_id}', + email=f'owner{unique_id}@testbiz.com', role=User.Role.TENANT_OWNER, - tenant=tenant + tenant=shared_tenant ) user.save() - assert user.tenant == tenant + assert user.tenant == shared_tenant assert user.id is not None @pytest.mark.django_db - def test_allows_customer_with_tenant(self): - from smoothschedule.identity.core.models import Tenant + def test_allows_customer_with_tenant(self, shared_tenant): + # Use shared fixture to avoid ~40s migration overhead per tenant import uuid - - # Create unique schema name to avoid collisions unique_id = str(uuid.uuid4())[:8] - tenant = Tenant.objects.create( - name=f'Test Business {unique_id}', - schema_name=f'testbiz{unique_id}' - ) user = User( - username='customer', - email='customer@example.com', + username=f'customer{unique_id}', + email=f'customer{unique_id}@example.com', role=User.Role.CUSTOMER, - tenant=tenant + tenant=shared_tenant ) user.save() - assert user.tenant == tenant + assert user.tenant == shared_tenant # =============================================================================