Files
smoothschedule/smoothschedule/schedule/test_export.py
poduck e4ad7fca87 feat: Plan-based feature permissions and quota enforcement
Backend:
- Add HasQuota() permission factory for quota limits (resources, users, services, appointments, email templates, automated tasks)
- Add HasFeaturePermission() factory for feature-based permissions (SMS, masked calling, custom domains, white label, plugins, webhooks, calendar sync, analytics)
- Add has_feature() method to Tenant model for flexible permission checking
- Add new tenant permission fields: can_create_plugins, can_use_webhooks, can_use_calendar_sync, can_export_data
- Create Data Export API with CSV/JSON support for appointments, customers, resources, services
- Create Analytics API with dashboard, appointments, revenue endpoints
- Add calendar sync views and URL configuration

Frontend:
- Add usePlanFeatures hook for checking feature availability
- Add UpgradePrompt components (inline, banner, overlay variants)
- Add LockedSection wrapper and LockedButton for feature gating
- Update settings pages with permission checks

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 11:21:11 -05:00

227 lines
7.7 KiB
Python

"""
Tests for Data Export API
Run with:
docker compose -f docker-compose.local.yml exec django python manage.py test schedule.test_export
"""
from django.test import TestCase, Client
from django.contrib.auth import get_user_model
from django.utils import timezone
from datetime import timedelta
from core.models import Tenant, Domain
from schedule.models import Event, Resource, Service
from smoothschedule.users.models import User as CustomUser
User = get_user_model()
class DataExportAPITestCase(TestCase):
"""Test suite for data export API endpoints"""
def setUp(self):
"""Set up test fixtures"""
# Create tenant with export permission
self.tenant = Tenant.objects.create(
name="Test Business",
schema_name="test_business",
can_export_data=True, # Enable export permission
)
# Create domain for tenant
self.domain = Domain.objects.create(
tenant=self.tenant,
domain="test.lvh.me",
is_primary=True
)
# Create test user (owner)
self.user = CustomUser.objects.create_user(
username="testowner",
email="owner@test.com",
password="testpass123",
role=CustomUser.Role.TENANT_OWNER,
tenant=self.tenant
)
# Create test customer
self.customer = CustomUser.objects.create_user(
username="customer1",
email="customer@test.com",
first_name="John",
last_name="Doe",
role=CustomUser.Role.CUSTOMER,
tenant=self.tenant
)
# Create test resource
self.resource = Resource.objects.create(
name="Test Resource",
type=Resource.Type.STAFF,
max_concurrent_events=1
)
# Create test service
self.service = Service.objects.create(
name="Test Service",
description="Test service description",
duration=60,
price=50.00
)
# Create test event
now = timezone.now()
self.event = Event.objects.create(
title="Test Appointment",
start_time=now,
end_time=now + timedelta(hours=1),
status=Event.Status.SCHEDULED,
notes="Test notes",
created_by=self.user
)
# Set up authenticated client
self.client = Client()
self.client.force_login(self.user)
def test_appointments_export_json(self):
"""Test exporting appointments in JSON format"""
response = self.client.get('/export/appointments/?format=json')
self.assertEqual(response.status_code, 200)
self.assertIn('application/json', response['Content-Type'])
# Check response structure
data = response.json()
self.assertIn('count', data)
self.assertIn('data', data)
self.assertIn('exported_at', data)
self.assertIn('filters', data)
# Verify data
self.assertEqual(data['count'], 1)
self.assertEqual(len(data['data']), 1)
appointment = data['data'][0]
self.assertEqual(appointment['title'], 'Test Appointment')
self.assertEqual(appointment['status'], 'SCHEDULED')
def test_appointments_export_csv(self):
"""Test exporting appointments in CSV format"""
response = self.client.get('/export/appointments/?format=csv')
self.assertEqual(response.status_code, 200)
self.assertIn('text/csv', response['Content-Type'])
self.assertIn('attachment', response['Content-Disposition'])
# Check CSV content
content = response.content.decode('utf-8')
self.assertIn('id,title,start_time', content)
self.assertIn('Test Appointment', content)
def test_customers_export_json(self):
"""Test exporting customers in JSON format"""
response = self.client.get('/export/customers/?format=json')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['count'], 1)
customer = data['data'][0]
self.assertEqual(customer['email'], 'customer@test.com')
self.assertEqual(customer['first_name'], 'John')
self.assertEqual(customer['last_name'], 'Doe')
def test_customers_export_csv(self):
"""Test exporting customers in CSV format"""
response = self.client.get('/export/customers/?format=csv')
self.assertEqual(response.status_code, 200)
self.assertIn('text/csv', response['Content-Type'])
content = response.content.decode('utf-8')
self.assertIn('customer@test.com', content)
self.assertIn('John', content)
def test_resources_export_json(self):
"""Test exporting resources in JSON format"""
response = self.client.get('/export/resources/?format=json')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['count'], 1)
resource = data['data'][0]
self.assertEqual(resource['name'], 'Test Resource')
self.assertEqual(resource['type'], 'STAFF')
def test_services_export_json(self):
"""Test exporting services in JSON format"""
response = self.client.get('/export/services/?format=json')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['count'], 1)
service = data['data'][0]
self.assertEqual(service['name'], 'Test Service')
self.assertEqual(service['duration'], 60)
self.assertEqual(service['price'], '50.00')
def test_date_range_filter(self):
"""Test filtering appointments by date range"""
# Create appointment in the past
past_time = timezone.now() - timedelta(days=30)
Event.objects.create(
title="Past Appointment",
start_time=past_time,
end_time=past_time + timedelta(hours=1),
status=Event.Status.COMPLETED,
created_by=self.user
)
# Filter for recent appointments only
start_date = (timezone.now() - timedelta(days=7)).isoformat()
response = self.client.get(f'/export/appointments/?format=json&start_date={start_date}')
data = response.json()
# Should only get the recent appointment, not the past one
self.assertEqual(data['count'], 1)
self.assertEqual(data['data'][0]['title'], 'Test Appointment')
def test_no_permission_denied(self):
"""Test that export fails when tenant doesn't have permission"""
# Disable export permission
self.tenant.can_export_data = False
self.tenant.save()
response = self.client.get('/export/appointments/?format=json')
self.assertEqual(response.status_code, 403)
self.assertIn('not available', response.json()['detail'])
def test_unauthenticated_denied(self):
"""Test that unauthenticated requests are denied"""
client = Client() # Not authenticated
response = client.get('/export/appointments/?format=json')
self.assertEqual(response.status_code, 401)
self.assertIn('Authentication', response.json()['detail'])
def test_active_filter(self):
"""Test filtering by active status"""
# Create inactive service
Service.objects.create(
name="Inactive Service",
duration=30,
price=25.00,
is_active=False
)
# Export only active services
response = self.client.get('/export/services/?format=json&is_active=true')
data = response.json()
# Should only get the active service
self.assertEqual(data['count'], 1)
self.assertEqual(data['data'][0]['name'], 'Test Service')