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>
227 lines
7.7 KiB
Python
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')
|