Files
smoothschedule/smoothschedule/CLAUDE.md
poduck b9e90e6f46 docs: Add comprehensive testing guidelines to CLAUDE.md
Add testing documentation emphasizing mocked unit tests over slow
database-hitting integration tests due to django-tenants overhead.

Guidelines include:
- Testing pyramid philosophy (prefer unit tests)
- Unit test examples with mocks
- Serializer and ViewSet testing patterns
- When to use integration tests (sparingly)
- Repository pattern for testable code
- Dependency injection examples
- Test file structure conventions
- Commands for running tests with coverage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-07 19:12:01 -05:00

14 KiB

SmoothSchedule Backend Development Guide

Docker-Based Development

IMPORTANT: This project runs in Docker containers. Do NOT try to run Django commands directly on the host machine - they will fail due to missing environment variables and database connection.

Running Django Commands

Always use Docker Compose to execute commands:

# Navigate to the smoothschedule directory first
cd /home/poduck/Desktop/smoothschedule2/smoothschedule

# Run migrations
docker compose -f docker-compose.local.yml exec django python manage.py migrate

# Run migrations for a specific app
docker compose -f docker-compose.local.yml exec django python manage.py migrate schedule

# Make migrations
docker compose -f docker-compose.local.yml exec django python manage.py makemigrations

# Create superuser
docker compose -f docker-compose.local.yml exec django python manage.py createsuperuser

# Run management commands
docker compose -f docker-compose.local.yml exec django python manage.py <command>

# Access Django shell
docker compose -f docker-compose.local.yml exec django python manage.py shell

# Run tests
docker compose -f docker-compose.local.yml exec django pytest

Multi-Tenant Migrations

This is a multi-tenant app using django-tenants. To run migrations on a specific tenant schema:

# Run on a specific tenant (e.g., 'demo')
docker compose -f docker-compose.local.yml exec django python manage.py tenant_command migrate --schema=demo

# Run on public schema
docker compose -f docker-compose.local.yml exec django python manage.py migrate_schemas --shared

Docker Services

# Start all services
docker compose -f docker-compose.local.yml up -d

# View logs
docker compose -f docker-compose.local.yml logs -f django

# Restart Django after code changes (usually auto-reloads)
docker compose -f docker-compose.local.yml restart django

# Rebuild after dependency changes
docker compose -f docker-compose.local.yml up -d --build

Project Structure

Critical Configuration Files

smoothschedule/
├── docker-compose.local.yml          # Local development Docker config
├── docker-compose.production.yml     # Production Docker config
├── .envs/
│   ├── .local/
│   │   ├── .django                   # Django env vars (SECRET_KEY, DEBUG, etc.)
│   │   └── .postgres                 # Database credentials
│   └── .production/
│       ├── .django
│       └── .postgres
├── config/
│   ├── settings/
│   │   ├── base.py                   # Base settings (shared)
│   │   ├── local.py                  # Local dev settings (imports multitenancy.py)
│   │   ├── production.py             # Production settings
│   │   ├── multitenancy.py           # Multi-tenant configuration
│   │   └── test.py                   # Test settings
│   └── urls.py                       # Main URL configuration
├── compose/
│   ├── local/django/
│   │   ├── Dockerfile                # Local Django container
│   │   └── start                     # Startup script
│   └── production/
│       ├── django/
│       ├── postgres/
│       └── traefik/

Django Apps (Domain-Based Organization)

smoothschedule/smoothschedule/
├── identity/                         # Identity Domain
│   ├── core/                         # Tenant, Domain, middleware, mixins
│   │   ├── models.py                 # Tenant, Domain, PermissionGrant
│   │   ├── middleware.py             # TenantHeader, Sandbox, Masquerade
│   │   └── mixins.py                 # Base classes for views/viewsets
│   └── users/                        # User management, authentication
│       ├── models.py                 # User model with roles
│       ├── api_views.py              # Auth endpoints
│       └── mfa_api_views.py          # MFA endpoints
├── scheduling/                       # Scheduling Domain
│   ├── schedule/                     # Core scheduling functionality
│   │   ├── models.py                 # Resource, Event, Service, Participant
│   │   ├── serializers.py            # DRF serializers
│   │   ├── views.py                  # ViewSets for API
│   │   └── services.py               # AvailabilityService
│   ├── contracts/                    # E-signature system
│   └── analytics/                    # Business analytics
├── communication/                    # Communication Domain
│   ├── notifications/                # Notification system
│   ├── credits/                      # SMS/calling credits
│   ├── mobile/                       # Field employee mobile app
│   └── messaging/                    # Email templates
├── commerce/                         # Commerce Domain
│   ├── payments/                     # Stripe Connect integration
│   └── tickets/                      # Support tickets
└── platform/                         # Platform Domain
    ├── admin/                        # Platform administration
    └── api/                          # Public API v1

API Endpoints

Base URL: http://lvh.me:8000/api/

  • /api/resources/ - Resource CRUD
  • /api/events/ - Event/Appointment CRUD
  • /api/services/ - Service CRUD
  • /api/customers/ - Customer listing
  • /api/auth/login/ - Authentication
  • /api/auth/logout/ - Logout
  • /api/users/me/ - Current user info
  • /api/business/ - Business settings

Local Development URLs

  • Backend API: http://lvh.me:8000
  • Frontend: http://demo.lvh.me:5173 (business subdomain)
  • Platform Frontend: http://platform.lvh.me:5173

Note: lvh.me resolves to 127.0.0.1 and allows subdomain-based multi-tenancy with cookies.

Database

  • Type: PostgreSQL with django-tenants
  • Public Schema: Shared tables (tenants, domains, platform users)
  • Tenant Schemas: Per-business data (resources, events, customers)

Common Issues

500 Error with No CORS Headers

When Django crashes (500 error), CORS headers aren't sent. Check Django logs:

docker compose -f docker-compose.local.yml logs django --tail=100

Missing Column/Table Errors

Run migrations:

docker compose -f docker-compose.local.yml exec django python manage.py migrate

ModuleNotFoundError / ImportError

You're trying to run Python directly instead of through Docker. Use docker compose exec.

Key Models

Resource (scheduling/schedule/models.py)

  • name, type (STAFF/ROOM/EQUIPMENT)
  • max_concurrent_events - concurrency limit (1=exclusive, >1=multilane, 0=unlimited)
  • saved_lane_count - remembers lane count when multilane disabled
  • buffer_duration - time between events

Event (scheduling/schedule/models.py)

  • title, start_time, end_time, status
  • Links to resources/customers via Participant model

User (identity/users/models.py)

  • Roles: superuser, platform_manager, platform_support, owner, manager, staff, resource, customer
  • business_subdomain - which tenant they belong to

Tenant (identity/core/models.py)

  • name, subdomain, schema_name
  • Multi-tenancy via django-tenants

Testing Guidelines

Testing Philosophy

Follow the Testing Pyramid:

        /\
       /E2E\        <- Few (real browser, full stack) - AVOID
      /------\
     /Integr- \     <- Some (real DB, verify ORM/queries)
    /---ation--\
   /   Unit     \   <- Many (mocked, fast, isolated) - PREFER
  /--------------\

Why: Django-tenants requires full PostgreSQL migrations per test, making DB-based tests extremely slow (~30-60 seconds each). Unit tests with mocks run in milliseconds.

Unit Tests (Preferred)

Write fast, isolated unit tests using mocks. Do NOT use @pytest.mark.django_db for unit tests.

# GOOD: Fast unit test with mocks
from unittest.mock import Mock, patch

def test_event_service_creates_event():
    # Arrange - mock dependencies
    mock_repo = Mock()
    mock_repo.save.return_value = Mock(id=1, title="Test Event")

    mock_tenant = Mock()
    mock_tenant.can_use_feature.return_value = True

    service = EventService(repository=mock_repo, tenant=mock_tenant)

    # Act
    result = service.create_event(title="Test Event", start_time=datetime.now())

    # Assert
    mock_repo.save.assert_called_once()
    assert result.title == "Test Event"


def test_event_service_denies_without_permission():
    mock_tenant = Mock()
    mock_tenant.can_use_feature.return_value = False

    service = EventService(repository=Mock(), tenant=mock_tenant)

    with pytest.raises(PermissionDenied):
        service.create_event(title="Test")
# BAD: Slow test hitting real database
@pytest.mark.django_db
def test_event_creation(tenant, user):
    # This takes 30+ seconds due to tenant schema setup!
    event = Event.objects.create(tenant=tenant, title="Test")
    assert event.id is not None

Testing Serializers

def test_event_serializer_validates_end_after_start():
    data = {
        'title': 'Test',
        'start_time': '2024-01-01T10:00:00Z',
        'end_time': '2024-01-01T09:00:00Z',  # Before start!
    }
    serializer = EventSerializer(data=data)

    assert not serializer.is_valid()
    assert 'end_time' in serializer.errors

Testing Views/ViewSets

Use APIRequestFactory with mocked authentication:

from rest_framework.test import APIRequestFactory
from unittest.mock import Mock, patch

def test_resource_list_returns_filtered_resources():
    factory = APIRequestFactory()
    request = factory.get('/api/resources/')

    # Mock the user and tenant
    request.user = Mock(is_authenticated=True, role='manager')
    request.tenant = Mock(id=1)

    # Mock the queryset
    with patch.object(ResourceViewSet, 'get_queryset') as mock_qs:
        mock_qs.return_value = [Mock(id=1, name='Room A')]

        view = ResourceViewSet.as_view({'get': 'list'})
        response = view(request)

    assert response.status_code == 200

Integration Tests (Use Sparingly)

Only use @pytest.mark.django_db when you MUST verify:

  • Complex ORM queries with joins/aggregations
  • Database constraints and triggers
  • Migration correctness
# Integration test - use only when necessary
@pytest.mark.django_db
class TestEventQueryIntegration:
    """Verify complex queries work correctly with real DB."""

    def test_overlapping_events_query(self, tenant_with_events):
        # This tests the actual SQL query behavior
        overlapping = Event.objects.filter_overlapping(
            start=datetime(2024, 1, 1, 10),
            end=datetime(2024, 1, 1, 11)
        )
        assert overlapping.count() == 2

Writing Testable Code

Design code for testability using dependency injection:

# BAD: Hard to test - direct imports and global state
class EventService:
    def create_event(self, data):
        tenant = get_current_tenant()  # Global state!
        event = Event.objects.create(**data)  # Direct ORM call
        send_notification(event)  # Direct import
        return event

# GOOD: Easy to test - dependencies injected
class EventService:
    def __init__(self, repository, tenant, notifier):
        self.repository = repository
        self.tenant = tenant
        self.notifier = notifier

    def create_event(self, data):
        if not self.tenant.can_use_feature('events'):
            raise PermissionDenied()
        event = self.repository.save(Event(**data))
        self.notifier.send(event)
        return event

Repository Pattern

Abstract database access for easier mocking:

# In repositories.py
class EventRepository:
    def save(self, event):
        event.save()
        return event

    def get_by_id(self, event_id):
        return Event.objects.get(id=event_id)

    def filter_by_date_range(self, start, end):
        return Event.objects.filter(start_time__gte=start, end_time__lte=end)

# In tests - mock the repository
def test_something():
    mock_repo = Mock(spec=EventRepository)
    mock_repo.get_by_id.return_value = Mock(id=1, title="Test")
    # ... test with mock_repo

Test File Structure

Each app should have:

app/
├── tests/
│   ├── __init__.py
│   ├── test_services.py      # Unit tests for business logic
│   ├── test_serializers.py   # Serializer validation tests
│   ├── test_views.py         # View/ViewSet tests (mocked)
│   └── test_integration.py   # DB tests (sparingly)

Running Tests

# Run all tests
docker compose -f docker-compose.local.yml exec django pytest

# Run specific test file
docker compose -f docker-compose.local.yml exec django pytest smoothschedule/scheduling/schedule/tests/test_services.py

# Run with coverage
docker compose -f docker-compose.local.yml exec django coverage run -m pytest
docker compose -f docker-compose.local.yml exec django coverage report

# Run only unit tests (fast)
docker compose -f docker-compose.local.yml exec django pytest -m "not django_db"

# Run only integration tests
docker compose -f docker-compose.local.yml exec django pytest -m django_db

Key Testing Rules

  1. Default to mocks - Only hit DB when testing query logic
  2. One assertion focus - Each test verifies one behavior
  3. Descriptive names - test_create_event_raises_when_tenant_lacks_permission
  4. Arrange-Act-Assert - Clear test structure
  5. No test interdependence - Tests must run in any order
  6. Fast feedback - If a test takes >1 second, it probably shouldn't hit the DB