project initialization
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
This commit is contained in:
626
backend/tests/integration/test_healthcare_operations.py
Normal file
626
backend/tests/integration/test_healthcare_operations.py
Normal file
@@ -0,0 +1,626 @@
|
||||
"""
|
||||
Integration test for healthcare module operations.
|
||||
This test MUST fail before implementation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class HealthcareOperationsIntegrationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# Tenant authentication header
|
||||
self.tenant_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_token'}
|
||||
|
||||
# Test patient data
|
||||
self.patient_data = {
|
||||
'ic_number': '900101-10-1234',
|
||||
'name': 'Ahmad bin Hassan',
|
||||
'gender': 'MALE',
|
||||
'date_of_birth': '1990-01-01',
|
||||
'phone': '+60123456789',
|
||||
'email': 'ahmad.hassan@example.com',
|
||||
'address': {
|
||||
'street': '123 Jalan Healthcare',
|
||||
'city': 'Kuala Lumpur',
|
||||
'state': 'Wilayah Persekutuan',
|
||||
'postal_code': '50400',
|
||||
'country': 'Malaysia'
|
||||
},
|
||||
'blood_type': 'O+',
|
||||
'allergies': ['Penicillin'],
|
||||
'medications': ['Metformin 500mg']
|
||||
}
|
||||
|
||||
# Test doctor data
|
||||
self.doctor_data = {
|
||||
'name': 'Dr. Sarah Johnson',
|
||||
'specialization': 'General Practitioner',
|
||||
'license_number': 'L12345',
|
||||
'department': 'Primary Care',
|
||||
'phone': '+60312345678',
|
||||
'email': 'sarah.johnson@hospital.com'
|
||||
}
|
||||
|
||||
def test_complete_patient_workflow(self):
|
||||
"""Test complete patient workflow from registration to treatment."""
|
||||
# Step 1: Patient registration (should fail before implementation)
|
||||
patient_response = self.client.post(
|
||||
'/api/v1/healthcare/patients/',
|
||||
data=json.dumps(self.patient_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert patient_response.status_code == status.HTTP_201_CREATED
|
||||
patient_data = patient_response.json()
|
||||
|
||||
# Verify patient structure
|
||||
assert 'id' in patient_data
|
||||
assert patient_data['ic_number'] == self.patient_data['ic_number']
|
||||
assert patient_data['name'] == self.patient_data['name']
|
||||
assert patient_data['age'] == 34 # Calculated from DOB
|
||||
assert patient_data['status'] == 'ACTIVE'
|
||||
|
||||
# Step 2: Create doctor
|
||||
doctor_response = self.client.post(
|
||||
'/api/v1/healthcare/doctors/',
|
||||
data=json.dumps(self.doctor_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert doctor_response.status_code == status.HTTP_201_CREATED
|
||||
doctor_data = doctor_response.json()
|
||||
|
||||
# Step 3: Schedule appointment
|
||||
appointment_data = {
|
||||
'patient_id': patient_data['id'],
|
||||
'doctor_id': doctor_data['id'],
|
||||
'appointment_datetime': '2024-02-15T14:30:00+08:00',
|
||||
'duration': 30,
|
||||
'type': 'CONSULTATION',
|
||||
'reason': 'Regular checkup for diabetes management',
|
||||
'priority': 'NORMAL'
|
||||
}
|
||||
|
||||
appointment_response = self.client.post(
|
||||
'/api/v1/healthcare/appointments/',
|
||||
data=json.dumps(appointment_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert appointment_response.status_code == status.HTTP_201_CREATED
|
||||
appointment_data = appointment_response.json()
|
||||
|
||||
assert appointment_data['status'] == 'SCHEDULED'
|
||||
|
||||
# Step 4: Update appointment status to in-progress
|
||||
status_update_response = self.client.put(
|
||||
f'/api/v1/healthcare/appointments/{appointment_data["id"]}/status/',
|
||||
data=json.dumps({'status': 'IN_PROGRESS'}),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert status_update_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Step 5: Create medical record
|
||||
medical_record_data = {
|
||||
'patient_id': patient_data['id'],
|
||||
'appointment_id': appointment_data['id'],
|
||||
'doctor_id': doctor_data['id'],
|
||||
'diagnosis': 'Type 2 Diabetes - well controlled',
|
||||
'treatment': 'Continue current medication regimen',
|
||||
'prescriptions': [
|
||||
{
|
||||
'medication': 'Metformin',
|
||||
'dosage': '500mg',
|
||||
'frequency': 'Twice daily',
|
||||
'duration': '30 days',
|
||||
'instructions': 'Take with meals'
|
||||
}
|
||||
],
|
||||
'vitals': {
|
||||
'blood_pressure': '120/80',
|
||||
'heart_rate': 72,
|
||||
'temperature': 36.5,
|
||||
'weight': 75.5,
|
||||
'height': 175.0
|
||||
},
|
||||
'notes': 'Patient reports good compliance with medication. Blood sugar levels well controlled.'
|
||||
}
|
||||
|
||||
record_response = self.client.post(
|
||||
'/api/v1/healthcare/medical-records/',
|
||||
data=json.dumps(medical_record_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert record_response.status_code == status.HTTP_201_CREATED
|
||||
record_data = record_response.json()
|
||||
|
||||
# Step 6: Complete appointment
|
||||
complete_response = self.client.put(
|
||||
f'/api/v1/healthcare/appointments/{appointment_data["id"]}/status/',
|
||||
data=json.dumps({'status': 'COMPLETED'}),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert complete_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Step 7: Schedule follow-up appointment
|
||||
follow_up_data = {
|
||||
'patient_id': patient_data['id'],
|
||||
'doctor_id': doctor_data['id'],
|
||||
'appointment_datetime': '2024-03-15T14:30:00+08:00',
|
||||
'duration': 20,
|
||||
'type': 'FOLLOW_UP',
|
||||
'reason': 'Diabetes follow-up'
|
||||
}
|
||||
|
||||
follow_up_response = self.client.post(
|
||||
'/api/v1/healthcare/appointments/',
|
||||
data=json.dumps(follow_up_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert follow_up_response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
def test_medical_records_management(self):
|
||||
"""Test medical records management and history."""
|
||||
# Create patient first
|
||||
patient_response = self.client.post(
|
||||
'/api/v1/healthcare/patients/',
|
||||
data=json.dumps(self.patient_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert patient_response.status_code == status.HTTP_201_CREATED
|
||||
patient_data = patient_response.json()
|
||||
|
||||
# Create multiple medical records over time
|
||||
records_data = [
|
||||
{
|
||||
'diagnosis': 'Hypertension',
|
||||
'treatment': 'Lifestyle modifications',
|
||||
'prescriptions': [
|
||||
{
|
||||
'medication': 'Lisinopril',
|
||||
'dosage': '10mg',
|
||||
'frequency': 'Once daily'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'diagnosis': 'Annual checkup - normal',
|
||||
'treatment': 'Continue healthy lifestyle',
|
||||
'vitals': {
|
||||
'blood_pressure': '118/76',
|
||||
'heart_rate': 68,
|
||||
'cholesterol': 180
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
created_records = []
|
||||
for record_data in records_data:
|
||||
full_record_data = {
|
||||
'patient_id': patient_data['id'],
|
||||
'doctor_id': 'doctor-001',
|
||||
'diagnosis': record_data['diagnosis'],
|
||||
'treatment': record_data['treatment'],
|
||||
**{k: v for k, v in record_data.items() if k not in ['diagnosis', 'treatment']}
|
||||
}
|
||||
|
||||
record_response = self.client.post(
|
||||
'/api/v1/healthcare/medical-records/',
|
||||
data=json.dumps(full_record_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert record_response.status_code == status.HTTP_201_CREATED
|
||||
created_records.append(record_response.json())
|
||||
|
||||
# Test medical history retrieval
|
||||
history_response = self.client.get(
|
||||
f'/api/v1/healthcare/patients/{patient_data["id"]}/medical-history/',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert history_response.status_code == status.HTTP_200_OK
|
||||
history_data = history_response.json()
|
||||
|
||||
assert 'medical_records' in history_data
|
||||
assert 'conditions' in history_data
|
||||
assert 'medications' in history_data
|
||||
assert 'allergies' in history_data
|
||||
|
||||
# Verify records are chronological
|
||||
records = history_data['medical_records']
|
||||
assert len(records) == len(created_records)
|
||||
|
||||
# Test record search and filtering
|
||||
search_response = self.client.get(
|
||||
f'/api/v1/healthcare/medical-records/',
|
||||
data={'patient_id': patient_data['id'], 'diagnosis': 'Hypertension'},
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert search_response.status_code == status.HTTP_200_OK
|
||||
search_results = search_response.json()['records']
|
||||
|
||||
assert len(search_results) > 0
|
||||
assert any('Hypertension' in record['diagnosis'] for record in search_results)
|
||||
|
||||
def test_prescription_management(self):
|
||||
"""Test prescription management and dispensing."""
|
||||
# Create patient
|
||||
patient_response = self.client.post(
|
||||
'/api/v1/healthcare/patients/',
|
||||
data=json.dumps(self.patient_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert patient_response.status_code == status.HTTP_201_CREATED
|
||||
patient_data = patient_response.json()
|
||||
|
||||
# Create prescription
|
||||
prescription_data = {
|
||||
'patient_id': patient_data['id'],
|
||||
'doctor_id': 'doctor-001',
|
||||
'medications': [
|
||||
{
|
||||
'name': 'Amoxicillin',
|
||||
'dosage': '500mg',
|
||||
'frequency': 'Three times daily',
|
||||
'duration': '7 days',
|
||||
'quantity': 21,
|
||||
'instructions': 'Take after meals',
|
||||
'refills_allowed': 0
|
||||
},
|
||||
{
|
||||
'name': 'Ibuprofen',
|
||||
'dosage': '400mg',
|
||||
'frequency': 'As needed for pain',
|
||||
'duration': '3 days',
|
||||
'quantity': 9,
|
||||
'instructions': 'Take with food',
|
||||
'refills_allowed': 1
|
||||
}
|
||||
],
|
||||
'diagnosis': 'Bacterial infection',
|
||||
'notes': 'Complete full course of antibiotics'
|
||||
}
|
||||
|
||||
prescription_response = self.client.post(
|
||||
'/api/v1/healthcare/prescriptions/',
|
||||
data=json.dumps(prescription_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert prescription_response.status_code == status.HTTP_201_CREATED
|
||||
prescription_data = prescription_response.json()
|
||||
|
||||
# Test prescription status management
|
||||
dispense_data = {
|
||||
'dispensed_by': 'pharmacist-001',
|
||||
'dispensed_at': datetime.now().isoformat(),
|
||||
'notes': 'Patient counseled on medication use'
|
||||
}
|
||||
|
||||
dispense_response = self.client.post(
|
||||
f'/api/v1/healthcare/prescriptions/{prescription_data["id"]}/dispense/',
|
||||
data=json.dumps(dispense_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert dispense_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Test refill request
|
||||
refill_response = self.client.post(
|
||||
f'/api/v1/healthcare/prescriptions/{prescription_data["id"]}/refill/',
|
||||
data=json.dumps({}),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert refill_response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_laboratory_and_imaging_orders(self):
|
||||
"""Test laboratory and imaging order management."""
|
||||
# Create patient
|
||||
patient_response = self.client.post(
|
||||
'/api/v1/healthcare/patients/',
|
||||
data=json.dumps(self.patient_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert patient_response.status_code == status.HTTP_201_CREATED
|
||||
patient_data = patient_response.json()
|
||||
|
||||
# Create lab order
|
||||
lab_order_data = {
|
||||
'patient_id': patient_data['id'],
|
||||
'doctor_id': 'doctor-001',
|
||||
'tests': [
|
||||
{
|
||||
'test_code': 'CBC',
|
||||
'test_name': 'Complete Blood Count',
|
||||
'priority': 'ROUTINE',
|
||||
'clinical_indication': 'Annual checkup'
|
||||
},
|
||||
{
|
||||
'test_code': 'HBA1C',
|
||||
'test_name': 'Hemoglobin A1C',
|
||||
'priority': 'ROUTINE',
|
||||
'clinical_indication': 'Diabetes monitoring'
|
||||
}
|
||||
],
|
||||
'notes': 'Patient fasting for 12 hours'
|
||||
}
|
||||
|
||||
lab_order_response = self.client.post(
|
||||
'/api/v1/healthcare/laboratory-orders/',
|
||||
data=json.dumps(lab_order_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert lab_order_response.status_code == status.HTTP_201_CREATED
|
||||
lab_order = lab_order_response.json()
|
||||
|
||||
# Update lab results
|
||||
results_data = {
|
||||
'results': [
|
||||
{
|
||||
'test_code': 'CBC',
|
||||
'result_value': 'Normal',
|
||||
'reference_range': '4.5-5.5 x 10^12/L',
|
||||
'units': 'x 10^12/L',
|
||||
'status': 'NORMAL'
|
||||
},
|
||||
{
|
||||
'test_code': 'HBA1C',
|
||||
'result_value': '6.2',
|
||||
'reference_range': '< 5.7%',
|
||||
'units': '%',
|
||||
'status': 'ABNORMAL',
|
||||
'notes': 'Slightly elevated - monitor'
|
||||
}
|
||||
],
|
||||
'interpreted_by': 'Dr. Lab Specialist',
|
||||
'interpretation': 'HbA1c shows prediabetes range'
|
||||
}
|
||||
|
||||
results_response = self.client.post(
|
||||
f'/api/v1/healthcare/laboratory-orders/{lab_order["id"]}/results/',
|
||||
data=json.dumps(results_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert results_response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_billing_and_insurance_integration(self):
|
||||
"""Test billing and insurance claim processing."""
|
||||
# Create patient with insurance
|
||||
patient_with_insurance = self.patient_data.copy()
|
||||
patient_with_insurance['insurance'] = {
|
||||
'provider': 'Malaysia National Insurance',
|
||||
'policy_number': 'MNI-123456789',
|
||||
'coverage_details': 'Full coverage',
|
||||
'expiry_date': '2024-12-31'
|
||||
}
|
||||
|
||||
patient_response = self.client.post(
|
||||
'/api/v1/healthcare/patients/',
|
||||
data=json.dumps(patient_with_insurance),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert patient_response.status_code == status.HTTP_201_CREATED
|
||||
patient_data = patient_response.json()
|
||||
|
||||
# Create consultation and generate bill
|
||||
billing_data = {
|
||||
'patient_id': patient_data['id'],
|
||||
'services': [
|
||||
{
|
||||
'service_code': 'CONSULT_GP',
|
||||
'description': 'General Practitioner Consultation',
|
||||
'amount': 150.00,
|
||||
'quantity': 1
|
||||
},
|
||||
{
|
||||
'service_code': 'LAB_CBC',
|
||||
'description': 'Complete Blood Count',
|
||||
'amount': 50.00,
|
||||
'quantity': 1
|
||||
}
|
||||
],
|
||||
'insurance_claim': {
|
||||
'provider': patient_data['insurance']['provider'],
|
||||
'policy_number': patient_data['insurance']['policy_number'],
|
||||
'pre_authorization_code': 'PA-2024-001'
|
||||
}
|
||||
}
|
||||
|
||||
billing_response = self.client.post(
|
||||
'/api/v1/healthcare/billing/',
|
||||
data=json.dumps(billing_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert billing_response.status_code == status.HTTP_201_CREATED
|
||||
billing_data = billing_response.json()
|
||||
|
||||
# Verify insurance claim processing
|
||||
assert 'insurance_coverage' in billing_data
|
||||
assert 'patient_responsibility' in billing_data
|
||||
assert 'claim_status' in billing_data
|
||||
|
||||
def test_healthcare_compliance_and_reporting(self):
|
||||
"""Test healthcare compliance and reporting features."""
|
||||
# Test PDPA compliance (Personal Data Protection Act)
|
||||
compliance_response = self.client.get(
|
||||
'/api/v1/healthcare/compliance/data-protection/',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert compliance_response.status_code == status.HTTP_200_OK
|
||||
compliance_data = compliance_response.json()
|
||||
|
||||
assert 'consent_records' in compliance_data
|
||||
assert 'data_access_logs' in compliance_data
|
||||
assert 'retention_policies' in compliance_data
|
||||
|
||||
# Test clinical reporting
|
||||
clinical_report_response = self.client.get(
|
||||
'/api/v1/healthcare/reports/clinical/',
|
||||
data={
|
||||
'period': 'monthly',
|
||||
'year': 2024,
|
||||
'month': 1
|
||||
},
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert clinical_report_response.status_code == status.HTTP_200_OK
|
||||
clinical_report = clinical_report_response.json()
|
||||
|
||||
assert 'patient_visits' in clinical_report
|
||||
assert 'common_diagnoses' in clinical_report
|
||||
assert 'prescription_trends' in clinical_report
|
||||
|
||||
# Test adverse event reporting
|
||||
adverse_event_data = {
|
||||
'patient_id': 'patient-001',
|
||||
'event_type': 'MEDICATION_ERROR',
|
||||
'description': 'Wrong dosage administered',
|
||||
'severity': 'MINOR',
|
||||
'date_occurred': datetime.now().isoformat(),
|
||||
'reported_by': 'nurse-001',
|
||||
'actions_taken': 'Corrected dosage, patient monitored'
|
||||
}
|
||||
|
||||
adverse_response = self.client.post(
|
||||
'/api/v1/healthcare/adverse-events/',
|
||||
data=json.dumps(adverse_event_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert adverse_response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
def test_telemedicine_integration(self):
|
||||
"""Test telemedicine and virtual consultation features."""
|
||||
# Create virtual appointment
|
||||
virtual_appointment_data = {
|
||||
'patient_id': 'patient-001',
|
||||
'doctor_id': 'doctor-001',
|
||||
'appointment_datetime': '2024-02-15T15:00:00+08:00',
|
||||
'duration': 20,
|
||||
'type': 'CONSULTATION',
|
||||
'is_virtual': True,
|
||||
'virtual_consultation': {
|
||||
'platform': 'ZOOM',
|
||||
'link': 'https://zoom.us/j/123456789',
|
||||
'instructions': 'Join 5 minutes early, test audio/video',
|
||||
'meeting_id': '123456789',
|
||||
'password': 'health2024'
|
||||
},
|
||||
'reason': 'Follow-up consultation'
|
||||
}
|
||||
|
||||
virtual_response = self.client.post(
|
||||
'/api/v1/healthcare/appointments/',
|
||||
data=json.dumps(virtual_appointment_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert virtual_response.status_code == status.HTTP_201_CREATED
|
||||
virtual_appointment = virtual_response.json()
|
||||
|
||||
assert virtual_appointment['is_virtual'] is True
|
||||
assert 'virtual_consultation' in virtual_appointment
|
||||
|
||||
# Test telemedicine session logging
|
||||
session_log_data = {
|
||||
'appointment_id': virtual_appointment['id'],
|
||||
'start_time': '2024-02-15T15:00:00Z',
|
||||
'end_time': '2024-02-15T15:18:00Z',
|
||||
'duration_minutes': 18,
|
||||
'connection_quality': 'GOOD',
|
||||
'technical_issues': None,
|
||||
'notes': 'Successful virtual consultation'
|
||||
}
|
||||
|
||||
session_log_response = self.client.post(
|
||||
'/api/v1/healthcare/telemedicine/session-logs/',
|
||||
data=json.dumps(session_log_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert session_log_response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
def test_emergency_management(self):
|
||||
"""Test emergency case management and triage."""
|
||||
# Create emergency appointment
|
||||
emergency_data = {
|
||||
'patient_id': 'patient-001',
|
||||
'doctor_id': 'doctor-emergency',
|
||||
'appointment_datetime': datetime.now().isoformat(),
|
||||
'duration': 60,
|
||||
'type': 'EMERGENCY',
|
||||
'priority': 'URGENT',
|
||||
'reason': 'Chest pain and shortness of breath',
|
||||
'triage_level': 'YELLOW'
|
||||
}
|
||||
|
||||
emergency_response = self.client.post(
|
||||
'/api/v1/healthcare/appointments/',
|
||||
data=json.dumps(emergency_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert emergency_response.status_code == status.HTTP_201_CREATED
|
||||
emergency_appointment = emergency_response.json()
|
||||
|
||||
assert emergency_appointment['type'] == 'EMERGENCY'
|
||||
assert emergency_appointment['priority'] == 'URGENT'
|
||||
|
||||
# Test emergency response protocol
|
||||
protocol_response = self.client.get(
|
||||
f'/api/v1/healthcare/emergency/protocols/{emergency_appointment["triage_level"]}/',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert protocol_response.status_code == status.HTTP_200_OK
|
||||
protocol_data = protocol_response.json()
|
||||
|
||||
assert 'response_time_target' in protocol_data
|
||||
assert 'required_actions' in protocol_data
|
||||
assert 'staffing_requirements' in protocol_data
|
||||
579
backend/tests/integration/test_retail_operations.py
Normal file
579
backend/tests/integration/test_retail_operations.py
Normal file
@@ -0,0 +1,579 @@
|
||||
"""
|
||||
Integration test for retail module operations.
|
||||
This test MUST fail before implementation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class RetailOperationsIntegrationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# Tenant authentication header
|
||||
self.tenant_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_token'}
|
||||
|
||||
# Test product data
|
||||
self.product_data = {
|
||||
'sku': 'LPT-PRO-001',
|
||||
'name': 'Professional Laptop 15"',
|
||||
'description': 'High-performance laptop for business use',
|
||||
'category': 'ELECTRONICS',
|
||||
'price': 3499.99,
|
||||
'cost': 2800.00,
|
||||
'stock_quantity': 25,
|
||||
'barcode': '1234567890123',
|
||||
'brand': 'TechBrand',
|
||||
'model': 'PRO-15-2024',
|
||||
'tax_rate': 6.0
|
||||
}
|
||||
|
||||
# Test customer data
|
||||
self.customer_data = {
|
||||
'name': 'John Customer',
|
||||
'email': 'john.customer@example.com',
|
||||
'phone': '+60123456789',
|
||||
'address': {
|
||||
'street': '123 Customer Street',
|
||||
'city': 'Kuala Lumpur',
|
||||
'state': 'Wilayah Persekutuan',
|
||||
'postal_code': '50000',
|
||||
'country': 'Malaysia'
|
||||
}
|
||||
}
|
||||
|
||||
def test_complete_retail_workflow(self):
|
||||
"""Test complete retail workflow from product creation to sales reporting."""
|
||||
# Step 1: Create product (should fail before implementation)
|
||||
product_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(self.product_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert product_response.status_code == status.HTTP_201_CREATED
|
||||
product_data = product_response.json()
|
||||
|
||||
# Verify product structure
|
||||
assert 'id' in product_data
|
||||
assert product_data['sku'] == self.product_data['sku']
|
||||
assert product_data['stock_quantity'] == self.product_data['stock_quantity']
|
||||
assert product_data['status'] == 'ACTIVE'
|
||||
|
||||
# Step 2: Create additional products for inventory testing
|
||||
additional_products = [
|
||||
{
|
||||
'sku': 'MOU-WRL-001',
|
||||
'name': 'Wireless Mouse',
|
||||
'category': 'ELECTRONICS',
|
||||
'price': 89.99,
|
||||
'cost': 45.00,
|
||||
'stock_quantity': 50
|
||||
},
|
||||
{
|
||||
'sku': 'KEY-MEC-001',
|
||||
'name': 'Mechanical Keyboard',
|
||||
'category': 'ELECTRONICS',
|
||||
'price': 299.99,
|
||||
'cost': 180.00,
|
||||
'stock_quantity': 30
|
||||
}
|
||||
]
|
||||
|
||||
created_products = []
|
||||
for prod_data in additional_products:
|
||||
prod_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(prod_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
assert prod_response.status_code == status.HTTP_201_CREATED
|
||||
created_products.append(prod_response.json())
|
||||
|
||||
# Step 3: Process multiple sales transactions
|
||||
sales_transactions = [
|
||||
{
|
||||
'customer': self.customer_data,
|
||||
'items': [
|
||||
{
|
||||
'product_id': product_data['id'],
|
||||
'sku': product_data['sku'],
|
||||
'quantity': 2,
|
||||
'unit_price': product_data['price']
|
||||
},
|
||||
{
|
||||
'product_id': created_products[0]['id'],
|
||||
'sku': created_products[0]['sku'],
|
||||
'quantity': 1,
|
||||
'unit_price': created_products[0]['price']
|
||||
}
|
||||
],
|
||||
'payment': {
|
||||
'method': 'CARD',
|
||||
'amount_paid': 7290.00,
|
||||
'reference_number': 'CARD-001'
|
||||
}
|
||||
},
|
||||
{
|
||||
'customer': {
|
||||
'name': 'Jane Buyer',
|
||||
'email': 'jane@example.com',
|
||||
'phone': '+60198765432'
|
||||
},
|
||||
'items': [
|
||||
{
|
||||
'product_id': created_products[1]['id'],
|
||||
'sku': created_products[1]['sku'],
|
||||
'quantity': 3,
|
||||
'unit_price': created_products[1]['price']
|
||||
}
|
||||
],
|
||||
'payment': {
|
||||
'method': 'CASH',
|
||||
'amount_paid': 900.00,
|
||||
'reference_number': 'CASH-001'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
created_sales = []
|
||||
for sale_data in sales_transactions:
|
||||
sale_response = self.client.post(
|
||||
'/api/v1/retail/sales/',
|
||||
data=json.dumps(sale_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert sale_response.status_code == status.HTTP_201_CREATED
|
||||
created_sales.append(sale_response.json())
|
||||
|
||||
# Step 4: Verify inventory updates after sales
|
||||
inventory_check_response = self.client.get(
|
||||
f'/api/v1/retail/products/{product_data["id"]}/',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert inventory_check_response.status_code == status.HTTP_200_OK
|
||||
updated_product = inventory_check_response.json()
|
||||
|
||||
# Stock should be reduced by sold quantity
|
||||
expected_stock = self.product_data['stock_quantity'] - 2 # 2 laptops sold
|
||||
assert updated_product['stock_quantity'] == expected_stock
|
||||
|
||||
# Step 5: Test sales reporting
|
||||
sales_report_response = self.client.get(
|
||||
'/api/v1/retail/reports/sales/',
|
||||
data={
|
||||
'start_date': (datetime.now() - timedelta(days=7)).isoformat(),
|
||||
'end_date': datetime.now().isoformat()
|
||||
},
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert sales_report_response.status_code == status.HTTP_200_OK
|
||||
sales_report = sales_report_response.json()
|
||||
|
||||
assert 'total_sales' in sales_report
|
||||
assert 'total_revenue' in sales_report
|
||||
assert 'transactions_count' in sales_report
|
||||
assert 'top_products' in sales_report
|
||||
|
||||
# Verify report data
|
||||
assert sales_report['transactions_count'] == len(created_sales)
|
||||
assert sales_report['total_revenue'] > 0
|
||||
|
||||
# Step 6: Test inventory reporting
|
||||
inventory_report_response = self.client.get(
|
||||
'/api/v1/retail/reports/inventory/',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert inventory_report_response.status_code == status.HTTP_200_OK
|
||||
inventory_report = inventory_report_response.json()
|
||||
|
||||
assert 'total_products' in inventory_report
|
||||
assert 'low_stock_items' in inventory_report
|
||||
assert 'total_value' in inventory_report
|
||||
|
||||
# Step 7: Test product search and filtering
|
||||
search_response = self.client.get(
|
||||
'/api/v1/retail/products/',
|
||||
data={'search': 'laptop', 'category': 'ELECTRONICS'},
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert search_response.status_code == status.HTTP_200_OK
|
||||
search_results = search_response.json()['products']
|
||||
|
||||
# Should find the laptop product
|
||||
assert len(search_results) > 0
|
||||
assert any(product['id'] == product_data['id'] for product in search_results)
|
||||
|
||||
def test_inventory_management_operations(self):
|
||||
"""Test inventory management operations."""
|
||||
# Create product first
|
||||
product_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(self.product_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert product_response.status_code == status.HTTP_201_CREATED
|
||||
product_data = product_response.json()
|
||||
|
||||
# Step 1: Stock adjustment
|
||||
adjustment_data = {
|
||||
'type': 'ADDITION',
|
||||
'quantity': 10,
|
||||
'reason': 'New stock received',
|
||||
'reference': 'PO-2024-001',
|
||||
'unit_cost': 2750.00
|
||||
}
|
||||
|
||||
adjustment_response = self.client.post(
|
||||
f'/api/v1/retail/products/{product_data["id"]}/inventory/',
|
||||
data=json.dumps(adjustment_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert adjustment_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Verify stock was updated
|
||||
updated_product_response = self.client.get(
|
||||
f'/api/v1/retail/products/{product_data["id"]}/',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert updated_product_response.status_code == status.HTTP_200_OK
|
||||
updated_product = updated_product_response.json()
|
||||
|
||||
expected_stock = self.product_data['stock_quantity'] + 10
|
||||
assert updated_product['stock_quantity'] == expected_stock
|
||||
|
||||
# Step 2: Stock transfer
|
||||
transfer_data = {
|
||||
'quantity': 5,
|
||||
'from_location': 'Warehouse A',
|
||||
'to_location': 'Store Front',
|
||||
'reason': 'Restocking store'
|
||||
}
|
||||
|
||||
transfer_response = self.client.post(
|
||||
f'/api/v1/retail/products/{product_data["id"]}/transfer/',
|
||||
data=json.dumps(transfer_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert transfer_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Step 3: Low stock alerts
|
||||
# Create product with low stock
|
||||
low_stock_product = self.product_data.copy()
|
||||
low_stock_product['sku'] = 'LOW-STOCK-001'
|
||||
low_stock_product['stock_quantity'] = 2
|
||||
|
||||
low_stock_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(low_stock_product),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert low_stock_response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# Check low stock report
|
||||
low_stock_report_response = self.client.get(
|
||||
'/api/v1/retail/reports/low-stock/',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert low_stock_report_response.status_code == status.HTTP_200_OK
|
||||
low_stock_report = low_stock_report_response.json()
|
||||
|
||||
assert 'low_stock_items' in low_stock_report
|
||||
assert len(low_stock_report['low_stock_items']) > 0
|
||||
|
||||
def test_product_variant_management(self):
|
||||
"""Test product variant management."""
|
||||
# Create parent product with variants
|
||||
parent_product = self.product_data.copy()
|
||||
parent_product['variants'] = [
|
||||
{
|
||||
'sku': 'LPT-PRO-001-BLK',
|
||||
'name': 'Professional Laptop 15" - Black',
|
||||
'attributes': {'color': 'Black', 'storage': '512GB SSD'},
|
||||
'price_adjustment': 0,
|
||||
'stock_quantity': 10
|
||||
},
|
||||
{
|
||||
'sku': 'LPT-PRO-001-SLV',
|
||||
'name': 'Professional Laptop 15" - Silver',
|
||||
'attributes': {'color': 'Silver', 'storage': '1TB SSD'},
|
||||
'price_adjustment': 200,
|
||||
'stock_quantity': 8
|
||||
}
|
||||
]
|
||||
|
||||
product_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(parent_product),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert product_response.status_code == status.HTTP_201_CREATED
|
||||
created_product = product_response.json()
|
||||
|
||||
# Verify variants were created
|
||||
assert 'variants' in created_product
|
||||
assert len(created_product['variants']) == 2
|
||||
|
||||
# Test variant operations
|
||||
variant = created_product['variants'][0]
|
||||
|
||||
# Update variant stock
|
||||
variant_stock_update = {
|
||||
'stock_quantity': 15,
|
||||
'reason': 'New stock received'
|
||||
}
|
||||
|
||||
variant_update_response = self.client.put(
|
||||
f'/api/v1/retail/products/{created_product["id"]}/variants/{variant["sku"]}/',
|
||||
data=json.dumps(variant_stock_update),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert variant_update_response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_customer_management(self):
|
||||
"""Test customer management operations."""
|
||||
# Create customer
|
||||
customer_response = self.client.post(
|
||||
'/api/v1/retail/customers/',
|
||||
data=json.dumps(self.customer_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert customer_response.status_code == status.HTTP_201_CREATED
|
||||
customer_data = customer_response.json()
|
||||
|
||||
# Step 1: Customer purchase history
|
||||
# Create a sale for this customer
|
||||
sale_data = {
|
||||
'customer_id': customer_data['id'],
|
||||
'items': [
|
||||
{
|
||||
'product_id': 'product-001',
|
||||
'sku': 'TEST-001',
|
||||
'quantity': 1,
|
||||
'unit_price': 99.99
|
||||
}
|
||||
],
|
||||
'payment': {
|
||||
'method': 'CASH',
|
||||
'amount_paid': 99.99
|
||||
}
|
||||
}
|
||||
|
||||
sale_response = self.client.post(
|
||||
'/api/v1/retail/sales/',
|
||||
data=json.dumps(sale_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert sale_response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# Get customer purchase history
|
||||
history_response = self.client.get(
|
||||
f'/api/v1/retail/customers/{customer_data["id"]}/history/',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert history_response.status_code == status.HTTP_200_OK
|
||||
history_data = history_response.json()
|
||||
|
||||
assert 'purchases' in history_data
|
||||
assert 'total_spent' in history_data
|
||||
assert 'loyalty_points' in history_data
|
||||
|
||||
# Step 2: Customer loyalty program
|
||||
loyalty_data = {
|
||||
'points_earned': 100,
|
||||
'notes': 'Purchase bonus'
|
||||
}
|
||||
|
||||
loyalty_response = self.client.post(
|
||||
f'/api/v1/retail/customers/{customer_data["id"]}/loyalty/',
|
||||
data=json.dumps(loyalty_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert loyalty_response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_discount_and_promotion_management(self):
|
||||
"""Test discount and promotion management."""
|
||||
# Create promotion
|
||||
promotion_data = {
|
||||
'name': 'New Year Sale',
|
||||
'type': 'PERCENTAGE',
|
||||
'value': 20,
|
||||
'start_date': (datetime.now() - timedelta(days=1)).isoformat(),
|
||||
'end_date': (datetime.now() + timedelta(days=30)).isoformat(),
|
||||
'applicable_products': ['product-001', 'product-002'],
|
||||
'minimum_purchase': 100,
|
||||
'usage_limit': 100
|
||||
}
|
||||
|
||||
promotion_response = self.client.post(
|
||||
'/api/v1/retail/promotions/',
|
||||
data=json.dumps(promotion_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert promotion_response.status_code == status.HTTP_201_CREATED
|
||||
created_promotion = promotion_response.json()
|
||||
|
||||
# Test promotion application in sale
|
||||
sale_with_promotion = {
|
||||
'customer': self.customer_data,
|
||||
'items': [
|
||||
{
|
||||
'product_id': 'product-001',
|
||||
'sku': 'TEST-001',
|
||||
'quantity': 2,
|
||||
'unit_price': 100.00
|
||||
}
|
||||
],
|
||||
'promotion_code': created_promotion['code'],
|
||||
'payment': {
|
||||
'method': 'CARD',
|
||||
'amount_paid': 160.00 # 20% discount on 200
|
||||
}
|
||||
}
|
||||
|
||||
sale_response = self.client.post(
|
||||
'/api/v1/retail/sales/',
|
||||
data=json.dumps(sale_with_promotion),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert sale_response.status_code == status.HTTP_201_CREATED
|
||||
sale_data = sale_response.json()
|
||||
|
||||
# Verify discount was applied
|
||||
assert 'discount_amount' in sale_data['totals']
|
||||
assert sale_data['totals']['discount_amount'] == 40.00
|
||||
|
||||
def test_return_and_refund_operations(self):
|
||||
"""Test return and refund operations."""
|
||||
# Create a sale first
|
||||
sale_data = {
|
||||
'customer': self.customer_data,
|
||||
'items': [
|
||||
{
|
||||
'product_id': 'product-001',
|
||||
'sku': 'TEST-001',
|
||||
'quantity': 2,
|
||||
'unit_price': 100.00
|
||||
}
|
||||
],
|
||||
'payment': {
|
||||
'method': 'CARD',
|
||||
'amount_paid': 200.00
|
||||
}
|
||||
}
|
||||
|
||||
sale_response = self.client.post(
|
||||
'/api/v1/retail/sales/',
|
||||
data=json.dumps(sale_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert sale_response.status_code == status.HTTP_201_CREATED
|
||||
created_sale = sale_response.json()
|
||||
|
||||
# Process return
|
||||
return_data = {
|
||||
'sale_id': created_sale['id'],
|
||||
'items': [
|
||||
{
|
||||
'product_id': 'product-001',
|
||||
'quantity': 1,
|
||||
'reason': 'Defective product',
|
||||
'condition': 'DAMAGED'
|
||||
}
|
||||
],
|
||||
'refund_method': 'ORIGINAL',
|
||||
'notes': 'Customer reported defective item'
|
||||
}
|
||||
|
||||
return_response = self.client.post(
|
||||
'/api/v1/retail/returns/',
|
||||
data=json.dumps(return_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert return_response.status_code == status.HTTP_201_CREATED
|
||||
return_data = return_response.json()
|
||||
|
||||
# Verify inventory was updated (returned to stock)
|
||||
# Verify refund was processed
|
||||
assert 'refund_amount' in return_data
|
||||
assert return_data['refund_amount'] == 100.00
|
||||
|
||||
def test_retail_analytics_and_reporting(self):
|
||||
"""Test retail analytics and reporting."""
|
||||
# Generate some test data first
|
||||
# This would involve creating multiple products and sales
|
||||
|
||||
# Test sales analytics
|
||||
analytics_response = self.client.get(
|
||||
'/api/v1/retail/analytics/',
|
||||
data={
|
||||
'period': 'monthly',
|
||||
'year': 2024,
|
||||
'month': 1
|
||||
},
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert analytics_response.status_code == status.HTTP_200_OK
|
||||
analytics_data = analytics_response.json()
|
||||
|
||||
assert 'revenue' in analytics_data
|
||||
assert 'profit' in analytics_data
|
||||
assert 'top_products' in analytics_data
|
||||
assert 'customer_metrics' in analytics_data
|
||||
|
||||
# Test product performance
|
||||
performance_response = self.client.get(
|
||||
'/api/v1/retail/reports/product-performance/',
|
||||
**self.tenant_auth
|
||||
)
|
||||
|
||||
assert performance_response.status_code == status.HTTP_200_OK
|
||||
performance_data = performance_response.json()
|
||||
|
||||
assert 'products' in performance_data
|
||||
assert 'best_sellers' in performance_data
|
||||
assert 'low_performers' in performance_data
|
||||
390
backend/tests/integration/test_subscription_management.py
Normal file
390
backend/tests/integration/test_subscription_management.py
Normal file
@@ -0,0 +1,390 @@
|
||||
"""
|
||||
Integration test for subscription management.
|
||||
This test MUST fail before implementation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class SubscriptionManagementIntegrationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# Admin authentication header
|
||||
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer super_admin_token'}
|
||||
|
||||
# Tenant admin authentication header
|
||||
self.tenant_admin_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_admin_token'}
|
||||
|
||||
# Test subscription data
|
||||
self.subscription_data = {
|
||||
'plan': 'GROWTH',
|
||||
'pricing_model': 'SUBSCRIPTION',
|
||||
'billing_cycle': 'MONTHLY',
|
||||
'payment_method': {
|
||||
'type': 'CARD',
|
||||
'card_last4': '4242',
|
||||
'expiry_month': 12,
|
||||
'expiry_year': 2025,
|
||||
'brand': 'visa'
|
||||
},
|
||||
'modules': ['retail', 'inventory'],
|
||||
'trial_days': 14
|
||||
}
|
||||
|
||||
def test_subscription_lifecycle_management(self):
|
||||
"""Test complete subscription lifecycle from trial to cancellation."""
|
||||
# Step 1: Create subscription with trial (should fail before implementation)
|
||||
create_response = self.client.post(
|
||||
'/api/v1/subscriptions/',
|
||||
data=json.dumps(self.subscription_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert create_response.status_code == status.HTTP_201_CREATED
|
||||
subscription_data = create_response.json()
|
||||
|
||||
# Verify subscription structure
|
||||
assert 'id' in subscription_data
|
||||
assert subscription_data['plan'] == self.subscription_data['plan']
|
||||
assert subscription_data['status'] == 'TRIAL'
|
||||
assert subscription_data['billing_cycle'] == self.subscription_data['billing_cycle']
|
||||
|
||||
# Verify billing period
|
||||
assert 'current_period_start' in subscription_data
|
||||
assert 'current_period_end' in subscription_data
|
||||
assert 'trial_end' in subscription_data
|
||||
|
||||
# Step 2: Test subscription upgrades during trial
|
||||
upgrade_data = {
|
||||
'plan': 'PRO',
|
||||
'reason': 'Business growth requires more features'
|
||||
}
|
||||
|
||||
upgrade_response = self.client.post(
|
||||
f'/api/v1/subscriptions/{subscription_data["id"]}/upgrade/',
|
||||
data=json.dumps(upgrade_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert upgrade_response.status_code == status.HTTP_200_OK
|
||||
upgraded_data = upgrade_response.json()
|
||||
|
||||
assert upgraded_data['plan'] == 'PRO'
|
||||
assert upgraded_data['status'] == 'TRIAL' # Still in trial period
|
||||
|
||||
# Step 3: Simulate trial end and activation
|
||||
# In real implementation, this would be handled by a background job
|
||||
activate_response = self.client.post(
|
||||
f'/api/v1/subscriptions/{subscription_data["id"]}/activate/',
|
||||
data=json.dumps({}),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert activate_response.status_code == status.HTTP_200_OK
|
||||
activated_data = activate_response.json()
|
||||
|
||||
assert activated_data['status'] == 'ACTIVE'
|
||||
assert activated_data['plan'] == 'PRO'
|
||||
|
||||
# Step 4: Test subscription usage tracking
|
||||
usage_response = self.client.get(
|
||||
f'/api/v1/subscriptions/{subscription_data["id"]}/usage/',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert usage_response.status_code == status.HTTP_200_OK
|
||||
usage_data = usage_response.json()
|
||||
|
||||
assert 'usage' in usage_data
|
||||
assert 'limits' in usage_data
|
||||
assert 'users_count' in usage_data['usage']
|
||||
assert 'storage_used' in usage_data['usage']
|
||||
|
||||
# Step 5: Test subscription downgrade
|
||||
downgrade_data = {
|
||||
'plan': 'GROWTH',
|
||||
'effective_date': (datetime.now() + timedelta(days=30)).isoformat()
|
||||
}
|
||||
|
||||
downgrade_response = self.client.post(
|
||||
f'/api/v1/subscriptions/{subscription_data["id"]}/downgrade/',
|
||||
data=json.dumps(downgrade_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert downgrade_response.status_code == status.HTTP_200_OK
|
||||
downgraded_data = downgrade_response.json()
|
||||
|
||||
assert downgraded_data['pending_plan'] == 'GROWTH'
|
||||
assert downgraded_data['plan_change_effective_date'] == downgrade_data['effective_date']
|
||||
|
||||
# Step 6: Test subscription cancellation
|
||||
cancel_data = {
|
||||
'reason': 'Business closure',
|
||||
'feedback': 'Closing down operations',
|
||||
'immediate': False
|
||||
}
|
||||
|
||||
cancel_response = self.client.post(
|
||||
f'/api/v1/subscriptions/{subscription_data["id"]}/cancel/',
|
||||
data=json.dumps(cancel_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert cancel_response.status_code == status.HTTP_200_OK
|
||||
cancelled_data = cancel_response.json()
|
||||
|
||||
assert cancelled_data['status'] == 'ACTIVE' # Still active until end of period
|
||||
assert cancelled_data['cancel_at_period_end'] is True
|
||||
|
||||
def test_subscription_billing_and_payments(self):
|
||||
"""Test subscription billing and payment processing."""
|
||||
# Create subscription
|
||||
create_response = self.client.post(
|
||||
'/api/v1/subscriptions/',
|
||||
data=json.dumps(self.subscription_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert create_response.status_code == status.HTTP_201_CREATED
|
||||
subscription_id = create_response.json()['id']
|
||||
|
||||
# Test billing history
|
||||
billing_response = self.client.get(
|
||||
f'/api/v1/subscriptions/{subscription_id}/billing/',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert billing_response.status_code == status.HTTP_200_OK
|
||||
billing_data = billing_response.json()
|
||||
|
||||
assert 'invoices' in billing_data
|
||||
assert 'payments' in billing_data
|
||||
assert 'upcoming_invoice' in billing_data
|
||||
|
||||
# Test payment method management
|
||||
payment_method_data = {
|
||||
'type': 'CARD',
|
||||
'card_number': '4242424242424242',
|
||||
'expiry_month': 12,
|
||||
'expiry_year': 2025,
|
||||
'cvv': '123',
|
||||
'cardholder_name': 'Test User'
|
||||
}
|
||||
|
||||
add_payment_response = self.client.post(
|
||||
f'/api/v1/subscriptions/{subscription_id}/payment-methods/',
|
||||
data=json.dumps(payment_method_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert add_payment_response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
def test_subscription_plan_changes_validation(self):
|
||||
"""Test validation of subscription plan changes."""
|
||||
# Create subscription
|
||||
create_response = self.client.post(
|
||||
'/api/v1/subscriptions/',
|
||||
data=json.dumps(self.subscription_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert create_response.status_code == status.HTTP_201_CREATED
|
||||
subscription_id = create_response.json()['id']
|
||||
|
||||
# Test invalid plan upgrade
|
||||
invalid_upgrade_data = {
|
||||
'plan': 'INVALID_PLAN'
|
||||
}
|
||||
|
||||
invalid_upgrade_response = self.client.post(
|
||||
f'/api/v1/subscriptions/{subscription_id}/upgrade/',
|
||||
data=json.dumps(invalid_upgrade_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert invalid_upgrade_response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
# Test downgrade to same plan
|
||||
same_plan_data = {
|
||||
'plan': self.subscription_data['plan']
|
||||
}
|
||||
|
||||
same_plan_response = self.client.post(
|
||||
f'/api/v1/subscriptions/{subscription_id}/downgrade/',
|
||||
data=json.dumps(same_plan_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert same_plan_response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_subscription_module_management(self):
|
||||
"""Test subscription module add-ons and management."""
|
||||
# Create base subscription
|
||||
base_subscription = self.subscription_data.copy()
|
||||
base_subscription['modules'] = ['retail']
|
||||
|
||||
create_response = self.client.post(
|
||||
'/api/v1/subscriptions/',
|
||||
data=json.dumps(base_subscription),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert create_response.status_code == status.HTTP_201_CREATED
|
||||
subscription_id = create_response.json()['id']
|
||||
|
||||
# Add module
|
||||
add_module_data = {
|
||||
'module': 'inventory',
|
||||
'pricing_model': 'PER_MODULE',
|
||||
'billing_cycle': 'MONTHLY'
|
||||
}
|
||||
|
||||
add_module_response = self.client.post(
|
||||
f'/api/v1/subscriptions/{subscription_id}/modules/',
|
||||
data=json.dumps(add_module_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert add_module_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Remove module
|
||||
remove_module_response = self.client.delete(
|
||||
f'/api/v1/subscriptions/{subscription_id}/modules/inventory/',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert remove_module_response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_subscription_usage_limits(self):
|
||||
"""Test subscription usage limits and overage handling."""
|
||||
# Create subscription with specific limits
|
||||
limited_subscription = self.subscription_data.copy()
|
||||
limited_subscription['usage_limits'] = {
|
||||
'users': 5,
|
||||
'storage_gb': 10,
|
||||
'api_calls_per_month': 10000
|
||||
}
|
||||
|
||||
create_response = self.client.post(
|
||||
'/api/v1/subscriptions/',
|
||||
data=json.dumps(limited_subscription),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert create_response.status_code == status.HTTP_201_CREATED
|
||||
subscription_id = create_response.json()['id']
|
||||
|
||||
# Check usage limits
|
||||
limits_response = self.client.get(
|
||||
f'/api/v1/subscriptions/{subscription_id}/limits/',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert limits_response.status_code == status.HTTP_200_OK
|
||||
limits_data = limits_response.json()
|
||||
|
||||
assert 'limits' in limits_data
|
||||
assert 'current_usage' in limits_data
|
||||
assert 'overage_charges' in limits_data
|
||||
|
||||
def test_subscription_discounts_and_promotions(self):
|
||||
"""Test subscription discounts and promotional codes."""
|
||||
# Create subscription with promo code
|
||||
promo_subscription = self.subscription_data.copy()
|
||||
promo_subscription['promo_code'] = 'WELCOME20'
|
||||
|
||||
create_response = self.client.post(
|
||||
'/api/v1/subscriptions/',
|
||||
data=json.dumps(promo_subscription),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert create_response.status_code == status.HTTP_201_CREATED
|
||||
subscription_data = create_response.json()
|
||||
|
||||
# Check discount was applied
|
||||
assert 'discount' in subscription_data
|
||||
assert subscription_data['promo_code'] == 'WELCOME20'
|
||||
|
||||
def test_subscription_notifications_and_reminders(self):
|
||||
"""Test subscription notifications and renewal reminders."""
|
||||
# Create subscription
|
||||
create_response = self.client.post(
|
||||
'/api/v1/subscriptions/',
|
||||
data=json.dumps(self.subscription_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert create_response.status_code == status.HTTP_201_CREATED
|
||||
subscription_id = create_response.json()['id']
|
||||
|
||||
# Test notification settings
|
||||
notification_settings = {
|
||||
'email_notifications': True,
|
||||
'renewal_reminders': True,
|
||||
'usage_alerts': True,
|
||||
'billing_notifications': True
|
||||
}
|
||||
|
||||
settings_response = self.client.put(
|
||||
f'/api/v1/subscriptions/{subscription_id}/notifications/',
|
||||
data=json.dumps(notification_settings),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert settings_response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_subscription_audit_trail(self):
|
||||
"""Test subscription changes audit trail."""
|
||||
# Create subscription
|
||||
create_response = self.client.post(
|
||||
'/api/v1/subscriptions/',
|
||||
data=json.dumps(self.subscription_data),
|
||||
content_type='application/json',
|
||||
**self.tenant_admin_auth
|
||||
)
|
||||
|
||||
assert create_response.status_code == status.HTTP_201_CREATED
|
||||
subscription_id = create_response.json()['id']
|
||||
|
||||
# Get audit trail
|
||||
audit_response = self.client.get(
|
||||
f'/api/v1/subscriptions/{subscription_id}/audit/',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert audit_response.status_code == status.HTTP_200_OK
|
||||
audit_data = audit_response.json()
|
||||
|
||||
assert 'changes' in audit_data
|
||||
assert isinstance(audit_data['changes'], list)
|
||||
assert len(audit_data['changes']) > 0
|
||||
|
||||
# First change should be subscription creation
|
||||
first_change = audit_data['changes'][0]
|
||||
assert first_change['action'] == 'CREATE'
|
||||
assert first_change['user_id'] is not None
|
||||
404
backend/tests/integration/test_tenant_isolation.py
Normal file
404
backend/tests/integration/test_tenant_isolation.py
Normal file
@@ -0,0 +1,404 @@
|
||||
"""
|
||||
Integration test for multi-tenant data isolation.
|
||||
This test MUST fail before implementation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
import json
|
||||
|
||||
|
||||
class TenantIsolationIntegrationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# Super admin authentication header
|
||||
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer super_admin_token'}
|
||||
|
||||
# Tenant 1 authentication header
|
||||
self.tenant1_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant1_admin_token'}
|
||||
|
||||
# Tenant 2 authentication header
|
||||
self.tenant2_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant2_admin_token'}
|
||||
|
||||
# Test data for different tenants
|
||||
self.tenant1_user_data = {
|
||||
'email': 'user1@tenant1.com',
|
||||
'name': 'User One',
|
||||
'role': 'MANAGER',
|
||||
'department': 'Sales'
|
||||
}
|
||||
|
||||
self.tenant2_user_data = {
|
||||
'email': 'user1@tenant2.com',
|
||||
'name': 'User One Duplicate',
|
||||
'role': 'MANAGER',
|
||||
'department': 'Marketing'
|
||||
}
|
||||
|
||||
self.tenant1_product_data = {
|
||||
'sku': 'PROD-001',
|
||||
'name': 'Product A',
|
||||
'category': 'ELECTRONICS',
|
||||
'price': 999.99
|
||||
}
|
||||
|
||||
self.tenant2_product_data = {
|
||||
'sku': 'PROD-001', # Same SKU as tenant1
|
||||
'name': 'Product A Different',
|
||||
'category': 'ELECTRONICS',
|
||||
'price': 899.99
|
||||
}
|
||||
|
||||
def test_user_data_isolation(self):
|
||||
"""Test that user data is properly isolated between tenants."""
|
||||
# Step 1: Create users in different tenants with same email structure
|
||||
tenant1_user_response = self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_user_response.status_code == status.HTTP_201_CREATED
|
||||
tenant1_user = tenant1_user_response.json()
|
||||
|
||||
tenant2_user_response = self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant2_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_user_response.status_code == status.HTTP_201_CREATED
|
||||
tenant2_user = tenant2_user_response.json()
|
||||
|
||||
# Step 2: Verify tenant isolation - each tenant should only see their own users
|
||||
tenant1_users_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_users_response.status_code == status.HTTP_200_OK
|
||||
tenant1_users = tenant1_users_response.json()['users']
|
||||
|
||||
# Should only see users from tenant1
|
||||
assert len(tenant1_users) == 1
|
||||
assert tenant1_users[0]['email'] == self.tenant1_user_data['email']
|
||||
assert tenant1_users[0]['tenant_id'] == tenant1_user['tenant_id']
|
||||
|
||||
tenant2_users_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_users_response.status_code == status.HTTP_200_OK
|
||||
tenant2_users = tenant2_users_response.json()['users']
|
||||
|
||||
# Should only see users from tenant2
|
||||
assert len(tenant2_users) == 1
|
||||
assert tenant2_users[0]['email'] == self.tenant2_user_data['email']
|
||||
assert tenant2_users[0]['tenant_id'] == tenant2_user['tenant_id']
|
||||
|
||||
# Step 3: Super admin should see all users
|
||||
admin_users_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert admin_users_response.status_code == status.HTTP_200_OK
|
||||
admin_users = admin_users_response.json()['users']
|
||||
|
||||
# Should see users from both tenants
|
||||
assert len(admin_users) >= 2
|
||||
user_emails = [user['email'] for user in admin_users]
|
||||
assert self.tenant1_user_data['email'] in user_emails
|
||||
assert self.tenant2_user_data['email'] in user_emails
|
||||
|
||||
def test_product_data_isolation(self):
|
||||
"""Test that product data is properly isolated between tenants."""
|
||||
# Step 1: Create products with same SKU in different tenants
|
||||
tenant1_product_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(self.tenant1_product_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_product_response.status_code == status.HTTP_201_CREATED
|
||||
tenant1_product = tenant1_product_response.json()
|
||||
|
||||
tenant2_product_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(self.tenant2_product_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_product_response.status_code == status.HTTP_201_CREATED
|
||||
tenant2_product = tenant2_product_response.json()
|
||||
|
||||
# Step 2: Verify SKU isolation - same SKU allowed in different tenants
|
||||
assert tenant1_product['sku'] == tenant2_product['sku']
|
||||
assert tenant1_product['id'] != tenant2_product['id']
|
||||
assert tenant1_product['tenant_id'] != tenant2_product['tenant_id']
|
||||
|
||||
# Step 3: Test product retrieval isolation
|
||||
tenant1_products_response = self.client.get(
|
||||
'/api/v1/retail/products/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_products_response.status_code == status.HTTP_200_OK
|
||||
tenant1_products = tenant1_products_response.json()['products']
|
||||
|
||||
# Should only see products from tenant1
|
||||
assert len(tenant1_products) == 1
|
||||
assert tenant1_products[0]['name'] == self.tenant1_product_data['name']
|
||||
assert tenant1_products[0]['tenant_id'] == tenant1_product['tenant_id']
|
||||
|
||||
tenant2_products_response = self.client.get(
|
||||
'/api/v1/retail/products/',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_products_response.status_code == status.HTTP_200_OK
|
||||
tenant2_products = tenant2_products_response.json()['products']
|
||||
|
||||
# Should only see products from tenant2
|
||||
assert len(tenant2_products) == 1
|
||||
assert tenant2_products[0]['name'] == self.tenant2_product_data['name']
|
||||
assert tenant2_products[0]['tenant_id'] == tenant2_product['tenant_id']
|
||||
|
||||
def test_healthcare_data_isolation(self):
|
||||
"""Test that healthcare patient data is properly isolated."""
|
||||
# Patient data for different tenants
|
||||
tenant1_patient_data = {
|
||||
'ic_number': '900101-10-1234',
|
||||
'name': 'Ahmad bin Hassan',
|
||||
'gender': 'MALE',
|
||||
'date_of_birth': '1990-01-01'
|
||||
}
|
||||
|
||||
tenant2_patient_data = {
|
||||
'ic_number': '900101-10-1234', # Same IC number
|
||||
'name': 'Ahmad bin Ali', # Different name
|
||||
'gender': 'MALE',
|
||||
'date_of_birth': '1990-01-01'
|
||||
}
|
||||
|
||||
# Create patients in different tenants
|
||||
tenant1_patient_response = self.client.post(
|
||||
'/api/v1/healthcare/patients/',
|
||||
data=json.dumps(tenant1_patient_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_patient_response.status_code == status.HTTP_201_CREATED
|
||||
tenant1_patient = tenant1_patient_response.json()
|
||||
|
||||
tenant2_patient_response = self.client.post(
|
||||
'/api/v1/healthcare/patients/',
|
||||
data=json.dumps(tenant2_patient_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_patient_response.status_code == status.HTTP_201_CREATED
|
||||
tenant2_patient = tenant2_patient_response.json()
|
||||
|
||||
# Verify same IC number allowed in different tenants (healthcare compliance)
|
||||
assert tenant1_patient['ic_number'] == tenant2_patient['ic_number']
|
||||
assert tenant1_patient['id'] != tenant2_patient['id']
|
||||
|
||||
# Test patient data isolation
|
||||
tenant1_patients_response = self.client.get(
|
||||
'/api/v1/healthcare/patients/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_patients_response.status_code == status.HTTP_200_OK
|
||||
tenant1_patients = tenant1_patients_response.json()['patients']
|
||||
|
||||
# Should only see patients from tenant1
|
||||
assert len(tenant1_patients) == 1
|
||||
assert tenant1_patients[0]['name'] == tenant1_patient_data['name']
|
||||
|
||||
def test_cross_tenant_access_prevention(self):
|
||||
"""Test that cross-tenant access is properly prevented."""
|
||||
# Step 1: Create a user in tenant1
|
||||
tenant1_user_response = self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_user_response.status_code == status.HTTP_201_CREATED
|
||||
created_user = tenant1_user_response.json()
|
||||
user_id = created_user['id']
|
||||
|
||||
# Step 2: Try to access tenant1 user from tenant2 (should fail)
|
||||
tenant2_access_response = self.client.get(
|
||||
f'/api/v1/users/{user_id}/',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_access_response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
# Step 3: Try to modify tenant1 user from tenant2 (should fail)
|
||||
modify_data = {'name': 'Hacked Name'}
|
||||
|
||||
tenant2_modify_response = self.client.put(
|
||||
f'/api/v1/users/{user_id}/',
|
||||
data=json.dumps(modify_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
assert tenant2_modify_response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
# Step 4: Verify user data is unchanged
|
||||
verify_response = self.client.get(
|
||||
f'/api/v1/users/{user_id}/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert verify_response.status_code == status.HTTP_200_OK
|
||||
verified_user = verify_response.json()
|
||||
assert verified_user['name'] == self.tenant1_user_data['name']
|
||||
|
||||
def test_database_row_level_security(self):
|
||||
"""Test that database row-level security is working."""
|
||||
# This test verifies that data isolation is enforced at the database level
|
||||
|
||||
# Create test data in both tenants
|
||||
self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant2_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
# Test direct database queries would be isolated
|
||||
# This is more of an integration test that would require actual database setup
|
||||
pass
|
||||
|
||||
def test_file_storage_isolation(self):
|
||||
"""Test that file storage is properly isolated between tenants."""
|
||||
# Upload files for different tenants
|
||||
# This would test file storage isolation mechanisms
|
||||
pass
|
||||
|
||||
def test_cache_isolation(self):
|
||||
"""Test that cache keys are properly isolated between tenants."""
|
||||
# Test that cache keys include tenant information
|
||||
# This ensures cache data doesn't leak between tenants
|
||||
pass
|
||||
|
||||
def test_tenant_context_propagation(self):
|
||||
"""Test that tenant context is properly propagated through the system."""
|
||||
# Create a user and verify tenant context is maintained across operations
|
||||
user_response = self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert user_response.status_code == status.HTTP_201_CREATED
|
||||
created_user = user_response.json()
|
||||
|
||||
# Verify tenant ID is consistently set
|
||||
assert 'tenant_id' in created_user
|
||||
tenant_id = created_user['tenant_id']
|
||||
|
||||
# Create a product and verify same tenant context
|
||||
product_response = self.client.post(
|
||||
'/api/v1/retail/products/',
|
||||
data=json.dumps(self.tenant1_product_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert product_response.status_code == status.HTTP_201_CREATED
|
||||
created_product = product_response.json()
|
||||
|
||||
assert created_product['tenant_id'] == tenant_id
|
||||
|
||||
def test_tenant_configuration_isolation(self):
|
||||
"""Test that tenant configurations are properly isolated."""
|
||||
# Set tenant-specific configurations
|
||||
tenant1_config = {
|
||||
'timezone': 'Asia/Kuala_Lumpur',
|
||||
'currency': 'MYR',
|
||||
'date_format': 'DD/MM/YYYY'
|
||||
}
|
||||
|
||||
tenant2_config = {
|
||||
'timezone': 'Asia/Singapore',
|
||||
'currency': 'SGD',
|
||||
'date_format': 'MM/DD/YYYY'
|
||||
}
|
||||
|
||||
# Apply configurations (would need actual config endpoints)
|
||||
# Verify configurations don't interfere
|
||||
pass
|
||||
|
||||
def test_tenant_performance_isolation(self):
|
||||
"""Test that one tenant's performance doesn't affect others."""
|
||||
# This would test resource limits and performance isolation
|
||||
pass
|
||||
|
||||
def test_audit_log_tenant_isolation(self):
|
||||
"""Test that audit logs are properly isolated by tenant."""
|
||||
# Perform actions in different tenants
|
||||
self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant1_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
self.client.post(
|
||||
'/api/v1/users/',
|
||||
data=json.dumps(self.tenant2_user_data),
|
||||
content_type='application/json',
|
||||
**self.tenant2_auth
|
||||
)
|
||||
|
||||
# Check that each tenant only sees their own audit logs
|
||||
tenant1_audit_response = self.client.get(
|
||||
'/api/v1/audit/logs/',
|
||||
**self.tenant1_auth
|
||||
)
|
||||
|
||||
assert tenant1_audit_response.status_code == status.HTTP_200_OK
|
||||
tenant1_logs = tenant1_audit_response.json()['logs']
|
||||
|
||||
# Should only see logs from tenant1 operations
|
||||
for log in tenant1_logs:
|
||||
assert log['tenant_id'] is not None
|
||||
|
||||
# Super admin should see all logs
|
||||
admin_audit_response = self.client.get(
|
||||
'/api/v1/audit/logs/',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert admin_audit_response.status_code == status.HTTP_200_OK
|
||||
admin_logs = admin_audit_response.json()['logs']
|
||||
|
||||
# Should see logs from both tenants
|
||||
assert len(admin_logs) >= len(tenant1_logs)
|
||||
322
backend/tests/integration/test_tenant_registration.py
Normal file
322
backend/tests/integration/test_tenant_registration.py
Normal file
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
Integration test for tenant registration flow.
|
||||
This test MUST fail before implementation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
import json
|
||||
|
||||
|
||||
class TenantRegistrationIntegrationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# Super admin authentication header
|
||||
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer super_admin_token'}
|
||||
|
||||
# Test tenant data
|
||||
self.tenant_data = {
|
||||
'name': 'Test Healthcare Sdn Bhd',
|
||||
'email': 'info@testhealthcare.com',
|
||||
'phone': '+60312345678',
|
||||
'address': {
|
||||
'street': '123 Medical Street',
|
||||
'city': 'Kuala Lumpur',
|
||||
'state': 'Wilayah Persekutuan',
|
||||
'postal_code': '50400',
|
||||
'country': 'Malaysia'
|
||||
},
|
||||
'business_type': 'HEALTHCARE',
|
||||
'subscription_plan': 'GROWTH',
|
||||
'pricing_model': 'SUBSCRIPTION',
|
||||
'admin_user': {
|
||||
'name': 'Dr. Sarah Johnson',
|
||||
'email': 'sarah.johnson@testhealthcare.com',
|
||||
'password': 'SecurePassword123!',
|
||||
'role': 'TENANT_ADMIN',
|
||||
'phone': '+60123456789'
|
||||
}
|
||||
}
|
||||
|
||||
def test_complete_tenant_registration_flow(self):
|
||||
"""Test complete tenant registration from creation to admin setup."""
|
||||
# Step 1: Create tenant (should fail before implementation)
|
||||
tenant_response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(self.tenant_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert tenant_response.status_code == status.HTTP_201_CREATED
|
||||
tenant_data = tenant_response.json()
|
||||
|
||||
# Verify tenant structure
|
||||
assert 'id' in tenant_data
|
||||
assert tenant_data['name'] == self.tenant_data['name']
|
||||
assert tenant_data['email'] == self.tenant_data['email']
|
||||
assert tenant_data['business_type'] == self.tenant_data['business_type']
|
||||
assert tenant_data['status'] == 'PENDING'
|
||||
|
||||
# Step 2: Verify tenant admin user was created
|
||||
# First, authenticate as super admin to get user list
|
||||
users_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert users_response.status_code == status.HTTP_200_OK
|
||||
users_data = users_response.json()
|
||||
|
||||
# Find the newly created admin user
|
||||
admin_user = None
|
||||
for user in users_data['users']:
|
||||
if user['email'] == self.tenant_data['admin_user']['email']:
|
||||
admin_user = user
|
||||
break
|
||||
|
||||
assert admin_user is not None
|
||||
assert admin_user['name'] == self.tenant_data['admin_user']['name']
|
||||
assert admin_user['role'] == 'TENANT_ADMIN'
|
||||
assert admin_user['tenant_id'] == tenant_data['id']
|
||||
|
||||
# Step 3: Verify subscription was created for tenant
|
||||
subscription_response = self.client.get(
|
||||
'/api/v1/subscriptions/',
|
||||
data={'tenant_id': tenant_data['id']},
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert subscription_response.status_code == status.HTTP_200_OK
|
||||
subscriptions_data = subscription_response.json()
|
||||
|
||||
assert len(subscriptions_data['subscriptions']) == 1
|
||||
subscription = subscriptions_data['subscriptions'][0]
|
||||
assert subscription['tenant_id'] == tenant_data['id']
|
||||
assert subscription['plan'] == self.tenant_data['subscription_plan']
|
||||
assert subscription['status'] == 'TRIAL'
|
||||
|
||||
# Step 4: Test tenant admin authentication
|
||||
# Login as tenant admin
|
||||
login_data = {
|
||||
'email': self.tenant_data['admin_user']['email'],
|
||||
'password': self.tenant_data['admin_user']['password']
|
||||
}
|
||||
|
||||
auth_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps(login_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert auth_response.status_code == status.HTTP_200_OK
|
||||
auth_data = auth_response.json()
|
||||
|
||||
assert 'access_token' in auth_data
|
||||
assert 'refresh_token' in auth_data
|
||||
assert 'user' in auth_data
|
||||
|
||||
# Verify user info in token
|
||||
user_info = auth_data['user']
|
||||
assert user_info['email'] == self.tenant_data['admin_user']['email']
|
||||
assert user_info['tenant_id'] == tenant_data['id']
|
||||
|
||||
# Step 5: Test tenant admin can access their tenant data
|
||||
tenant_admin_auth = {'HTTP_AUTHORIZATION': f'Bearer {auth_data["access_token"]}'}
|
||||
|
||||
tenant_own_response = self.client.get(
|
||||
'/api/v1/tenants/',
|
||||
**tenant_admin_auth
|
||||
)
|
||||
|
||||
assert tenant_own_response.status_code == status.HTTP_200_OK
|
||||
tenant_own_data = tenant_own_response.json()
|
||||
|
||||
# Should only see their own tenant
|
||||
assert len(tenant_own_data['tenants']) == 1
|
||||
assert tenant_own_data['tenants'][0]['id'] == tenant_data['id']
|
||||
|
||||
# Step 6: Test tenant isolation - cannot see other tenants
|
||||
# Create another tenant as super admin
|
||||
other_tenant_data = self.tenant_data.copy()
|
||||
other_tenant_data['name'] = 'Other Healthcare Sdn Bhd'
|
||||
other_tenant_data['email'] = 'info@otherhealthcare.com'
|
||||
other_tenant_data['admin_user']['email'] = 'admin@otherhealthcare.com'
|
||||
|
||||
other_tenant_response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(other_tenant_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert other_tenant_response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# First tenant admin should still only see their own tenant
|
||||
tenant_still_own_response = self.client.get(
|
||||
'/api/v1/tenants/',
|
||||
**tenant_admin_auth
|
||||
)
|
||||
|
||||
assert tenant_still_own_response.status_code == status.HTTP_200_OK
|
||||
tenant_still_own_data = tenant_still_own_response.json()
|
||||
|
||||
# Should still only see their own tenant
|
||||
assert len(tenant_still_own_data['tenants']) == 1
|
||||
assert tenant_still_own_data['tenants'][0]['id'] == tenant_data['id']
|
||||
|
||||
def test_tenant_registration_invalid_business_type(self):
|
||||
"""Test tenant registration with invalid business type."""
|
||||
invalid_data = self.tenant_data.copy()
|
||||
invalid_data['business_type'] = 'INVALID_TYPE'
|
||||
|
||||
response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(invalid_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_tenant_registration_missing_admin_user(self):
|
||||
"""Test tenant registration without admin user data."""
|
||||
invalid_data = self.tenant_data.copy()
|
||||
del invalid_data['admin_user']
|
||||
|
||||
response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(invalid_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_tenant_registration_duplicate_email(self):
|
||||
"""Test tenant registration with duplicate email."""
|
||||
# Create first tenant
|
||||
first_response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(self.tenant_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert first_response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
# Try to create second tenant with same email
|
||||
second_response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(self.tenant_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert second_response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_tenant_registration_unauthorized(self):
|
||||
"""Test tenant registration without admin authentication."""
|
||||
response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(self.tenant_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_tenant_registration_weak_admin_password(self):
|
||||
"""Test tenant registration with weak admin password."""
|
||||
invalid_data = self.tenant_data.copy()
|
||||
invalid_data['admin_user']['password'] = '123'
|
||||
|
||||
response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(invalid_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_tenant_registration_with_modules_configuration(self):
|
||||
"""Test tenant registration with specific modules configuration."""
|
||||
modules_data = self.tenant_data.copy()
|
||||
modules_data['modules'] = ['healthcare', 'appointments', 'billing']
|
||||
modules_data['modules_config'] = {
|
||||
'healthcare': {
|
||||
'features': ['patient_management', 'appointment_scheduling', 'medical_records'],
|
||||
'settings': {
|
||||
'enable_telemedicine': True,
|
||||
'appointment_reminders': True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(modules_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
if response.status_code == status.HTTP_201_CREATED:
|
||||
tenant_data = response.json()
|
||||
# Should have modules configuration
|
||||
assert 'modules' in tenant_data
|
||||
assert 'modules_config' in tenant_data
|
||||
|
||||
def test_tenant_registration_with_branding(self):
|
||||
"""Test tenant registration with branding information."""
|
||||
branding_data = self.tenant_data.copy()
|
||||
branding_data['branding'] = {
|
||||
'logo_url': 'https://example.com/logo.png',
|
||||
'primary_color': '#2563eb',
|
||||
'secondary_color': '#64748b',
|
||||
'company_website': 'https://testhealthcare.com',
|
||||
'social_media': {
|
||||
'facebook': 'testhealthcare',
|
||||
'instagram': 'testhealthcare_my'
|
||||
}
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(branding_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
if response.status_code == status.HTTP_201_CREATED:
|
||||
tenant_data = response.json()
|
||||
# Should have branding information
|
||||
assert 'branding' in tenant_data
|
||||
assert tenant_data['branding']['primary_color'] == '#2563eb'
|
||||
|
||||
def test_tenant_registration_domain_setup(self):
|
||||
"""Test tenant registration with custom domain setup."""
|
||||
domain_data = self.tenant_data.copy()
|
||||
domain_data['domain'] = 'portal.testhealthcare.com'
|
||||
domain_data['settings'] = {
|
||||
'custom_domain_enabled': True,
|
||||
'ssl_enabled': True,
|
||||
'email_domain': 'testhealthcare.com'
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
'/api/v1/tenants/',
|
||||
data=json.dumps(domain_data),
|
||||
content_type='application/json',
|
||||
**self.admin_auth
|
||||
)
|
||||
|
||||
if response.status_code == status.HTTP_201_CREATED:
|
||||
tenant_data = response.json()
|
||||
# Should have domain configuration
|
||||
assert 'domain' in tenant_data
|
||||
assert 'settings' in tenant_data
|
||||
assert tenant_data['domain'] == 'portal.testhealthcare.com'
|
||||
391
backend/tests/integration/test_user_authentication.py
Normal file
391
backend/tests/integration/test_user_authentication.py
Normal file
@@ -0,0 +1,391 @@
|
||||
"""
|
||||
Integration test for user authentication flow.
|
||||
This test MUST fail before implementation.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
class UserAuthenticationIntegrationTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# Test user credentials
|
||||
self.test_user = {
|
||||
'email': 'test.user@example.com',
|
||||
'password': 'SecurePassword123!',
|
||||
'name': 'Test User',
|
||||
'role': 'TENANT_ADMIN'
|
||||
}
|
||||
|
||||
def test_complete_authentication_flow(self):
|
||||
"""Test complete authentication flow from login to logout."""
|
||||
# Step 1: User login (should fail before implementation)
|
||||
login_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': self.test_user['email'],
|
||||
'password': self.test_user['password']
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert login_response.status_code == status.HTTP_200_OK
|
||||
login_data = login_response.json()
|
||||
|
||||
# Verify token structure
|
||||
assert 'access_token' in login_data
|
||||
assert 'refresh_token' in login_data
|
||||
assert 'user' in login_data
|
||||
assert 'expires_in' in login_data
|
||||
|
||||
access_token = login_data['access_token']
|
||||
refresh_token = login_data['refresh_token']
|
||||
user_info = login_data['user']
|
||||
|
||||
# Verify user information
|
||||
assert user_info['email'] == self.test_user['email']
|
||||
assert user_info['name'] == self.test_user['name']
|
||||
assert user_info['role'] == self.test_user['role']
|
||||
assert 'tenant_id' in user_info
|
||||
|
||||
# Step 2: Use access token for authenticated requests
|
||||
auth_header = {'HTTP_AUTHORIZATION': f'Bearer {access_token}'}
|
||||
|
||||
# Test accessing protected resource
|
||||
protected_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**auth_header
|
||||
)
|
||||
|
||||
assert protected_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Step 3: Test token refresh
|
||||
refresh_response = self.client.post(
|
||||
'/api/v1/auth/refresh/',
|
||||
data=json.dumps({
|
||||
'refresh_token': refresh_token
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert refresh_response.status_code == status.HTTP_200_OK
|
||||
refresh_data = refresh_response.json()
|
||||
|
||||
# Verify new tokens
|
||||
assert 'access_token' in refresh_data
|
||||
assert 'refresh_token' in refresh_data
|
||||
|
||||
# New access token should be different (rotation)
|
||||
new_access_token = refresh_data['access_token']
|
||||
assert new_access_token != access_token
|
||||
|
||||
# New refresh token should also be different (rotation)
|
||||
new_refresh_token = refresh_data['refresh_token']
|
||||
assert new_refresh_token != refresh_token
|
||||
|
||||
# Step 4: Test new access token works
|
||||
new_auth_header = {'HTTP_AUTHORIZATION': f'Bearer {new_access_token}'}
|
||||
|
||||
new_protected_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**new_auth_header
|
||||
)
|
||||
|
||||
assert new_protected_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Step 5: Test old refresh token is invalidated
|
||||
old_refresh_response = self.client.post(
|
||||
'/api/v1/auth/refresh/',
|
||||
data=json.dumps({
|
||||
'refresh_token': refresh_token # Old token
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert old_refresh_response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
# Step 6: Test logout/blacklist tokens
|
||||
logout_response = self.client.post(
|
||||
'/api/v1/auth/logout/',
|
||||
**new_auth_header
|
||||
)
|
||||
|
||||
assert logout_response.status_code == status.HTTP_200_OK
|
||||
logout_data = logout_response.json()
|
||||
|
||||
assert 'message' in logout_data
|
||||
assert logout_data['message'] == 'Successfully logged out'
|
||||
|
||||
# Step 7: Test token is blacklisted (cannot be used after logout)
|
||||
blacklisted_response = self.client.get(
|
||||
'/api/v1/users/',
|
||||
**new_auth_header
|
||||
)
|
||||
|
||||
assert blacklisted_response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_multi_factor_authentication_flow(self):
|
||||
"""Test multi-factor authentication flow."""
|
||||
# Step 1: Initial login with MFA enabled user
|
||||
mfa_login_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': 'mfa.user@example.com',
|
||||
'password': 'SecurePassword123!'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
# Should return MFA challenge instead of full token
|
||||
assert mfa_login_response.status_code == status.HTTP_200_OK
|
||||
mfa_data = mfa_login_response.json()
|
||||
|
||||
assert 'mfa_required' in mfa_data
|
||||
assert mfa_data['mfa_required'] is True
|
||||
assert 'mfa_methods' in mfa_data
|
||||
assert 'temp_token' in mfa_data
|
||||
|
||||
# Step 2: Complete MFA with TOTP
|
||||
mfa_verify_response = self.client.post(
|
||||
'/api/v1/auth/mfa/verify/',
|
||||
data=json.dumps({
|
||||
'temp_token': mfa_data['temp_token'],
|
||||
'method': 'TOTP',
|
||||
'code': '123456' # Mock TOTP code
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert mfa_verify_response.status_code == status.HTTP_200_OK
|
||||
mfa_verify_data = mfa_verify_response.json()
|
||||
|
||||
assert 'access_token' in mfa_verify_data
|
||||
assert 'refresh_token' in mfa_verify_data
|
||||
|
||||
def test_authentication_error_scenarios(self):
|
||||
"""Test various authentication error scenarios."""
|
||||
# Test invalid credentials
|
||||
invalid_credentials_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': self.test_user['email'],
|
||||
'password': 'wrongpassword'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert invalid_credentials_response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
# Test missing credentials
|
||||
missing_credentials_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': self.test_user['email']
|
||||
# Missing password
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert missing_credentials_response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
# Test invalid refresh token
|
||||
invalid_refresh_response = self.client.post(
|
||||
'/api/v1/auth/refresh/',
|
||||
data=json.dumps({
|
||||
'refresh_token': 'invalid_refresh_token'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert invalid_refresh_response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
# Test missing refresh token
|
||||
missing_refresh_response = self.client.post(
|
||||
'/api/v1/auth/refresh/',
|
||||
data=json.dumps({}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert missing_refresh_response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_token_expiry_handling(self):
|
||||
"""Test handling of expired tokens."""
|
||||
# This test would need to simulate token expiration
|
||||
# For now, we'll test the structure
|
||||
pass
|
||||
|
||||
def test_concurrent_session_management(self):
|
||||
"""Test concurrent session management."""
|
||||
# Login first device
|
||||
device1_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': self.test_user['email'],
|
||||
'password': self.test_user['password']
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert device1_response.status_code == status.HTTP_200_OK
|
||||
device1_token = device1_response.json()['access_token']
|
||||
|
||||
# Login second device
|
||||
device2_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': self.test_user['email'],
|
||||
'password': self.test_user['password']
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert device2_response.status_code == status.HTTP_200_OK
|
||||
device2_token = device2_response.json()['access_token']
|
||||
|
||||
# Both tokens should work (assuming concurrent sessions are allowed)
|
||||
device1_auth = {'HTTP_AUTHORIZATION': f'Bearer {device1_token}'}
|
||||
device2_auth = {'HTTP_AUTHORIZATION': f'Bearer {device2_token}'}
|
||||
|
||||
device1_protected = self.client.get('/api/v1/users/', **device1_auth)
|
||||
device2_protected = self.client.get('/api/v1/users/', **device2_auth)
|
||||
|
||||
assert device1_protected.status_code == status.HTTP_200_OK
|
||||
assert device2_protected.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_permission_based_access_control(self):
|
||||
"""Test permission-based access control."""
|
||||
# Login as regular user
|
||||
user_login_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': 'regular.user@example.com',
|
||||
'password': 'SecurePassword123!'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert user_login_response.status_code == status.HTTP_200_OK
|
||||
user_token = user_login_response.json()['access_token']
|
||||
user_auth = {'HTTP_AUTHORIZATION': f'Bearer {user_token}'}
|
||||
|
||||
# Regular user should not be able to access admin-only endpoints
|
||||
admin_endpoint_response = self.client.get('/api/v1/tenants/', **user_auth)
|
||||
assert admin_endpoint_response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
# But should be able to access user endpoints
|
||||
user_endpoint_response = self.client.get('/api/v1/users/', **user_auth)
|
||||
assert user_endpoint_response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_tenant_isolation_in_authentication(self):
|
||||
"""Test that authentication tokens include tenant isolation."""
|
||||
# Login as tenant admin
|
||||
tenant_admin_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': 'tenant.admin@tenant1.com',
|
||||
'password': 'SecurePassword123!'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert tenant_admin_response.status_code == status.HTTP_200_OK
|
||||
tenant_admin_data = tenant_admin_response.json()
|
||||
|
||||
# Token should include tenant information
|
||||
assert 'tenant_id' in tenant_admin_data['user']
|
||||
tenant1_id = tenant_admin_data['user']['tenant_id']
|
||||
|
||||
# Login as different tenant admin
|
||||
tenant2_admin_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': 'tenant.admin@tenant2.com',
|
||||
'password': 'SecurePassword123!'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert tenant2_admin_response.status_code == status.HTTP_200_OK
|
||||
tenant2_admin_data = tenant2_admin_response.json()
|
||||
|
||||
# Should have different tenant ID
|
||||
assert 'tenant_id' in tenant2_admin_data['user']
|
||||
tenant2_id = tenant2_admin_data['user']['tenant_id']
|
||||
|
||||
assert tenant1_id != tenant2_id
|
||||
|
||||
def test_authentication_rate_limiting(self):
|
||||
"""Test authentication rate limiting."""
|
||||
# Test multiple failed login attempts
|
||||
for i in range(5):
|
||||
failed_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': self.test_user['email'],
|
||||
'password': 'wrongpassword'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
# Should still allow attempts but may implement rate limiting
|
||||
assert failed_response.status_code in [status.HTTP_401_UNAUTHORIZED, status.HTTP_429_TOO_MANY_REQUESTS]
|
||||
|
||||
def test_password_change_flow(self):
|
||||
"""Test password change flow with authentication."""
|
||||
# Login first
|
||||
login_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': self.test_user['email'],
|
||||
'password': self.test_user['password']
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert login_response.status_code == status.HTTP_200_OK
|
||||
access_token = login_response.json()['access_token']
|
||||
auth_header = {'HTTP_AUTHORIZATION': f'Bearer {access_token}'}
|
||||
|
||||
# Change password
|
||||
password_change_response = self.client.post(
|
||||
'/api/v1/auth/change-password/',
|
||||
data=json.dumps({
|
||||
'current_password': self.test_user['password'],
|
||||
'new_password': 'NewSecurePassword456!'
|
||||
}),
|
||||
content_type='application/json',
|
||||
**auth_header
|
||||
)
|
||||
|
||||
assert password_change_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Test login with new password
|
||||
new_login_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': self.test_user['email'],
|
||||
'password': 'NewSecurePassword456!'
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert new_login_response.status_code == status.HTTP_200_OK
|
||||
|
||||
# Test old password no longer works
|
||||
old_login_response = self.client.post(
|
||||
'/api/v1/auth/login/',
|
||||
data=json.dumps({
|
||||
'email': self.test_user['email'],
|
||||
'password': self.test_user['password']
|
||||
}),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
assert old_login_response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
Reference in New Issue
Block a user