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>
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 disabledbuffer_duration- time between events
Event (scheduling/schedule/models.py)
title,start_time,end_time,status- Links to resources/customers via
Participantmodel
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
- Default to mocks - Only hit DB when testing query logic
- One assertion focus - Each test verifies one behavior
- Descriptive names -
test_create_event_raises_when_tenant_lacks_permission - Arrange-Act-Assert - Clear test structure
- No test interdependence - Tests must run in any order
- Fast feedback - If a test takes >1 second, it probably shouldn't hit the DB