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

This commit is contained in:
2025-10-05 02:37:33 +08:00
parent 2cbb6d5fa1
commit b3fff546e9
226 changed files with 97805 additions and 35 deletions

View File

@@ -0,0 +1,115 @@
"""
Contract test for POST /auth/login endpoint.
This test MUST fail before implementation.
"""
import pytest
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from rest_framework import status
import json
User = get_user_model()
class AuthLoginContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.login_url = '/api/v1/auth/login/'
# Create test user
self.user_data = {
'email': 'test@example.com',
'password': 'testpass123',
'first_name': 'Test',
'last_name': 'User'
}
def test_login_success(self):
"""Test successful login with valid credentials."""
response = self.client.post(
self.login_url,
data=json.dumps(self.user_data),
content_type='application/json'
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'access_token' in data
assert 'refresh_token' in data
assert 'user' in data
user_data = data['user']
assert user_data['email'] == self.user_data['email']
assert user_data['first_name'] == self.user_data['first_name']
assert user_data['last_name'] == self.user_data['last_name']
def test_login_invalid_credentials(self):
"""Test login failure with invalid credentials."""
invalid_data = self.user_data.copy()
invalid_data['password'] = 'wrongpassword'
response = self.client.post(
self.login_url,
data=json.dumps(invalid_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_login_missing_email(self):
"""Test login failure with missing email."""
incomplete_data = {
'password': self.user_data['password']
}
response = self.client.post(
self.login_url,
data=json.dumps(incomplete_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_login_missing_password(self):
"""Test login failure with missing password."""
incomplete_data = {
'email': self.user_data['email']
}
response = self.client.post(
self.login_url,
data=json.dumps(incomplete_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_login_invalid_content_type(self):
"""Test login failure with invalid content type."""
response = self.client.post(
self.login_url,
data=json.dumps(self.user_data)
)
assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
def test_login_tenant_specific(self):
"""Test login with tenant-specific URL."""
# This test will check multi-tenant authentication
tenant_login_url = '/api/v1/auth/login/'
response = self.client.post(
tenant_login_url,
data=json.dumps(self.user_data),
content_type='application/json'
)
# Should return tenant-specific information
if response.status_code == status.HTTP_200_OK:
data = response.json()
assert 'tenant' in data

View File

@@ -0,0 +1,78 @@
"""
Contract test for POST /auth/logout endpoint.
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 AuthLogoutContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.logout_url = '/api/v1/auth/logout/'
# Mock authentication token
self.auth_header = {'HTTP_AUTHORIZATION': 'Bearer mock_token'}
def test_logout_success(self):
"""Test successful logout with valid token."""
response = self.client.post(
self.logout_url,
**self.auth_header
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'message' in data
assert data['message'] == 'Successfully logged out'
def test_logout_no_token(self):
"""Test logout failure without authentication token."""
response = self.client.post(self.logout_url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_logout_invalid_token(self):
"""Test logout failure with invalid token."""
invalid_auth_header = {'HTTP_AUTHORIZATION': 'Bearer invalid_token'}
response = self.client.post(
self.logout_url,
**invalid_auth_header
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_logout_expired_token(self):
"""Test logout failure with expired token."""
expired_auth_header = {'HTTP_AUTHORIZATION': 'Bearer expired_token'}
response = self.client.post(
self.logout_url,
**expired_auth_header
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_logout_token_blacklisting(self):
"""Test that logout token is blacklisted."""
# This test verifies that the token is added to blacklist
response = self.client.post(
self.logout_url,
**self.auth_header
)
if response.status_code == status.HTTP_200_OK:
# Token should be blacklisted and cannot be used again
second_response = self.client.post(
self.logout_url,
**self.auth_header
)
assert second_response.status_code == status.HTTP_401_UNAUTHORIZED

View File

@@ -0,0 +1,108 @@
"""
Contract test for POST /auth/refresh endpoint.
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 AuthRefreshContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.refresh_url = '/api/v1/auth/refresh/'
# Mock refresh token
self.refresh_data = {
'refresh_token': 'mock_refresh_token'
}
def test_refresh_success(self):
"""Test successful token refresh with valid refresh token."""
response = self.client.post(
self.refresh_url,
data=json.dumps(self.refresh_data),
content_type='application/json'
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'access_token' in data
assert 'refresh_token' in data
# New refresh token should be different (rotation enabled)
assert data['refresh_token'] != self.refresh_data['refresh_token']
def test_refresh_invalid_token(self):
"""Test refresh failure with invalid refresh token."""
invalid_data = {
'refresh_token': 'invalid_refresh_token'
}
response = self.client.post(
self.refresh_url,
data=json.dumps(invalid_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_refresh_missing_token(self):
"""Test refresh failure with missing refresh token."""
incomplete_data = {}
response = self.client.post(
self.refresh_url,
data=json.dumps(incomplete_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_refresh_blacklisted_token(self):
"""Test refresh failure with blacklisted token."""
blacklisted_data = {
'refresh_token': 'blacklisted_refresh_token'
}
response = self.client.post(
self.refresh_url,
data=json.dumps(blacklisted_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_refresh_expired_token(self):
"""Test refresh failure with expired refresh token."""
expired_data = {
'refresh_token': 'expired_refresh_token'
}
response = self.client.post(
self.refresh_url,
data=json.dumps(expired_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_refresh_tenant_isolation(self):
"""Test that refresh token respects tenant isolation."""
# This test ensures refresh tokens are tenant-specific
response = self.client.post(
self.refresh_url,
data=json.dumps(self.refresh_data),
content_type='application/json'
)
if response.status_code == status.HTTP_200_OK:
data = response.json()
# Tenant information should be included in token
assert 'tenant_id' in data or 'tenant_slug' in data

View File

@@ -0,0 +1,336 @@
"""
Contract test for GET /healthcare/appointments endpoint.
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 HealthcareAppointmentsGetContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.appointments_url = '/api/v1/healthcare/appointments/'
# Tenant authentication header
self.tenant_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_token'}
def test_get_appointments_success(self):
"""Test successful retrieval of appointments list."""
response = self.client.get(
self.appointments_url,
**self.tenant_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'appointments' in data
assert isinstance(data['appointments'], list)
# Check pagination structure
assert 'pagination' in data
pagination = data['pagination']
assert 'page' in pagination
assert 'limit' in pagination
assert 'total' in pagination
assert 'pages' in pagination
def test_get_appointments_unauthorized(self):
"""Test appointments list retrieval without authentication."""
response = self.client.get(self.appointments_url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_get_appointments_with_pagination(self):
"""Test appointments list retrieval with pagination parameters."""
params = {
'page': 2,
'limit': 20
}
response = self.client.get(
self.appointments_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data['pagination']['page'] == 2
assert data['pagination']['limit'] == 20
def test_get_appointments_with_search(self):
"""Test appointments list retrieval with search parameter."""
params = {
'search': 'ahmad'
}
response = self.client.get(
self.appointments_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned appointments should match search criteria
for appointment in data['appointments']:
search_match = (
'ahmad' in appointment['patient_name'].lower() or
'ahmad' in appointment['doctor_name'].lower() or
'ahmad' in appointment['notes'].lower()
)
assert search_match
def test_get_appointments_filter_by_date_range(self):
"""Test appointments list retrieval filtered by date range."""
params = {
'start_date': '2024-01-01',
'end_date': '2024-01-31'
}
response = self.client.get(
self.appointments_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned appointments should be within the date range
for appointment in data['appointments']:
appointment_date = appointment['appointment_datetime'].split('T')[0]
assert '2024-01-01' <= appointment_date <= '2024-01-31'
def test_get_appointments_filter_by_status(self):
"""Test appointments list retrieval filtered by status."""
params = {
'status': 'CONFIRMED'
}
response = self.client.get(
self.appointments_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned appointments should have the specified status
for appointment in data['appointments']:
assert appointment['status'] == 'CONFIRMED'
def test_get_appointments_filter_by_doctor(self):
"""Test appointments list retrieval filtered by doctor."""
params = {
'doctor_id': 'doctor-001'
}
response = self.client.get(
self.appointments_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned appointments should be with the specified doctor
for appointment in data['appointments']:
assert appointment['doctor_id'] == 'doctor-001'
def test_get_appointments_filter_by_patient(self):
"""Test appointments list retrieval filtered by patient."""
params = {
'patient_id': 'patient-001'
}
response = self.client.get(
self.appointments_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned appointments should be for the specified patient
for appointment in data['appointments']:
assert appointment['patient_id'] == 'patient-001'
def test_get_appointments_data_structure(self):
"""Test that appointment data structure matches the contract."""
response = self.client.get(
self.appointments_url,
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['appointments']) > 0:
appointment = response.json()['appointments'][0]
# Required fields according to contract
required_fields = [
'id', 'patient_id', 'patient_name', 'doctor_id', 'doctor_name',
'appointment_datetime', 'duration', 'status', 'type',
'reason', 'notes', 'tenant_id', 'created_at', 'updated_at'
]
for field in required_fields:
assert field in appointment
# Field types and enums
assert isinstance(appointment['id'], str)
assert isinstance(appointment['patient_id'], str)
assert isinstance(appointment['patient_name'], str)
assert isinstance(appointment['doctor_id'], str)
assert isinstance(appointment['doctor_name'], str)
assert isinstance(appointment['appointment_datetime'], str)
assert isinstance(appointment['duration'], int)
assert appointment['status'] in ['SCHEDULED', 'CONFIRMED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'NO_SHOW']
assert appointment['type'] in ['CONSULTATION', 'FOLLOW_UP', 'PROCEDURE', 'EMERGENCY', 'CHECKUP']
def test_get_appointments_with_patient_details(self):
"""Test that appointment data includes patient details."""
response = self.client.get(
self.appointments_url,
data={'include_patient_details': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['appointments']) > 0:
appointment = response.json()['appointments'][0]
# Should include patient details
assert 'patient_details' in appointment
patient_details = appointment['patient_details']
# Patient details should include relevant fields
expected_patient_fields = ['ic_number', 'phone', 'email', 'age', 'gender']
for field in expected_patient_fields:
assert field in patient_details
def test_get_appointments_with_doctor_details(self):
"""Test that appointment data includes doctor details."""
response = self.client.get(
self.appointments_url,
data={'include_doctor_details': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['appointments']) > 0:
appointment = response.json()['appointments'][0]
# Should include doctor details
assert 'doctor_details' in appointment
doctor_details = appointment['doctor_details']
# Doctor details should include relevant fields
expected_doctor_fields = ['specialization', 'license_number', 'department']
for field in expected_doctor_fields:
assert field in doctor_details
def test_get_appointments_sorting(self):
"""Test appointments list retrieval with sorting."""
params = {
'sort_by': 'appointment_datetime',
'sort_order': 'asc'
}
response = self.client.get(
self.appointments_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Appointments should be sorted by datetime in ascending order
appointment_datetimes = [appointment['appointment_datetime'] for appointment in data['appointments']]
assert appointment_datetimes == sorted(appointment_datetimes)
def test_get_appointments_tenant_isolation(self):
"""Test that appointments are isolated by tenant."""
response = self.client.get(
self.appointments_url,
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK:
data = response.json()
# All returned appointments should belong to the authenticated tenant
for appointment in data['appointments']:
assert 'tenant_id' in appointment
# This will be validated once implementation exists
def test_get_appointments_upcoming_only(self):
"""Test appointments list retrieval for upcoming appointments only."""
params = {
'upcoming_only': 'true'
}
response = self.client.get(
self.appointments_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned appointments should be in the future
# This will be validated once implementation exists
pass
def test_get_appointments_with_reminders(self):
"""Test that appointment data includes reminder information."""
response = self.client.get(
self.appointments_url,
data={'include_reminders': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['appointments']) > 0:
appointment = response.json()['appointments'][0]
# Should include reminder information
assert 'reminders' in appointment
reminders = appointment['reminders']
# Should be a list
assert isinstance(reminders, list)
if len(reminders) > 0:
reminder = reminders[0]
expected_reminder_fields = ['type', 'sent_at', 'status']
for field in expected_reminder_fields:
assert field in reminder
def test_get_appointments_with_virtual_info(self):
"""Test that appointment data includes virtual consultation information."""
response = self.client.get(
self.appointments_url,
data={'include_virtual_info': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['appointments']) > 0:
appointment = response.json()['appointments'][0]
# Should include virtual consultation info if applicable
if appointment.get('is_virtual', False):
assert 'virtual_consultation' in appointment
virtual_info = appointment['virtual_consultation']
expected_virtual_fields = ['platform', 'link', 'instructions']
for field in expected_virtual_fields:
assert field in virtual_info

View File

@@ -0,0 +1,392 @@
"""
Contract test for POST /healthcare/appointments endpoint.
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 HealthcareAppointmentsPostContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.appointments_url = '/api/v1/healthcare/appointments/'
# Tenant authentication header
self.tenant_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_token'}
# Valid appointment data
self.appointment_data = {
'patient_id': 'patient-001',
'doctor_id': 'doctor-001',
'appointment_datetime': '2024-02-15T14:30:00+08:00',
'duration': 30,
'type': 'CONSULTATION',
'reason': 'Regular checkup for diabetes management',
'notes': 'Patient reports occasional dizziness. Need to review medication dosage.',
'priority': 'NORMAL',
'is_virtual': False,
'location': {
'room': 'Consultation Room A',
'floor': '2nd Floor',
'building': 'Main Medical Center'
},
'reminders': [
{
'type': 'SMS',
'time_before': 1440, # 24 hours
'message': 'Reminder: Your appointment is tomorrow at 2:30 PM'
},
{
'type': 'EMAIL',
'time_before': 60, # 1 hour
'message': 'Your appointment is in 1 hour'
}
],
'follow_up': {
'required': True,
'interval_days': 30,
'notes': 'Follow up to check medication effectiveness'
}
}
def test_create_appointment_success(self):
"""Test successful appointment creation."""
response = self.client.post(
self.appointments_url,
data=json.dumps(self.appointment_data),
content_type='application/json',
**self.tenant_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert 'id' in data
assert data['patient_id'] == self.appointment_data['patient_id']
assert data['doctor_id'] == self.appointment_data['doctor_id']
assert data['appointment_datetime'] == self.appointment_data['appointment_datetime']
assert data['duration'] == self.appointment_data['duration']
assert data['type'] == self.appointment_data['type']
assert data['reason'] == self.appointment_data['reason']
assert data['status'] == 'SCHEDULED' # Default status
# Should have timestamps
assert 'created_at' in data
assert 'updated_at' in data
# Should have tenant_id from context
assert 'tenant_id' in data
# Should include location information
assert 'location' in data
assert data['location']['room'] == self.appointment_data['location']['room']
def test_create_appointment_unauthorized(self):
"""Test appointment creation without authentication."""
response = self.client.post(
self.appointments_url,
data=json.dumps(self.appointment_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_appointment_missing_required_fields(self):
"""Test appointment creation with missing required fields."""
incomplete_data = self.appointment_data.copy()
del incomplete_data['patient_id']
response = self.client.post(
self.appointments_url,
data=json.dumps(incomplete_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
data = response.json()
assert 'patient_id' in data.get('errors', {})
def test_create_appointment_invalid_datetime(self):
"""Test appointment creation with invalid datetime format."""
invalid_data = self.appointment_data.copy()
invalid_data['appointment_datetime'] = 'invalid-datetime-format'
response = self.client.post(
self.appointments_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_appointment_past_datetime(self):
"""Test appointment creation with past datetime."""
invalid_data = self.appointment_data.copy()
invalid_data['appointment_datetime'] = '2020-01-01T10:00:00+08:00' # Past date
response = self.client.post(
self.appointments_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_appointment_invalid_type(self):
"""Test appointment creation with invalid type."""
invalid_data = self.appointment_data.copy()
invalid_data['type'] = 'INVALID_TYPE'
response = self.client.post(
self.appointments_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_appointment_negative_duration(self):
"""Test appointment creation with negative duration."""
invalid_data = self.appointment_data.copy()
invalid_data['duration'] = -30
response = self.client.post(
self.appointments_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_appointment_doctor_availability_conflict(self):
"""Test appointment creation with doctor availability conflict."""
# First request should succeed (if implemented)
first_response = self.client.post(
self.appointments_url,
data=json.dumps(self.appointment_data),
content_type='application/json',
**self.tenant_auth
)
if first_response.status_code == status.HTTP_201_CREATED:
# Second request with same doctor and overlapping time should fail
conflicting_data = self.appointment_data.copy()
conflicting_data['patient_id'] = 'patient-002' # Different patient
conflicting_data['appointment_datetime'] = '2024-02-15T14:45:00+08:00' # Overlapping time
second_response = self.client.post(
self.appointments_url,
data=json.dumps(conflicting_data),
content_type='application/json',
**self.tenant_auth
)
assert second_response.status_code == status.HTTP_409_CONFLICT
def test_create_appointment_virtual_consultation(self):
"""Test appointment creation with virtual consultation."""
virtual_data = self.appointment_data.copy()
virtual_data['is_virtual'] = True
virtual_data['virtual_consultation'] = {
'platform': 'ZOOM',
'link': 'https://zoom.us/j/123456789',
'instructions': 'Please join 5 minutes early. Test your audio and video.',
'meeting_id': '123456789',
'password': 'health2024'
}
response = self.client.post(
self.appointments_url,
data=json.dumps(virtual_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert data['is_virtual'] is True
assert 'virtual_consultation' in data
virtual_info = data['virtual_consultation']
assert virtual_info['platform'] == 'ZOOM'
def test_create_appointment_emergency(self):
"""Test emergency appointment creation."""
emergency_data = self.appointment_data.copy()
emergency_data['type'] = 'EMERGENCY'
emergency_data['priority'] = 'URGENT'
emergency_data['reason'] = 'Chest pain and shortness of breath'
response = self.client.post(
self.appointments_url,
data=json.dumps(emergency_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert data['type'] == 'EMERGENCY'
assert data['priority'] == 'URGENT'
def test_create_appointment_with_attachments(self):
"""Test appointment creation with attachments."""
attachment_data = self.appointment_data.copy()
attachment_data['attachments'] = [
{
'type': 'MEDICAL_REPORT',
'name': 'Blood Test Results.pdf',
'url': 'https://storage.example.com/blood-test-123.pdf',
'uploaded_at': '2024-02-10T10:00:00Z'
},
{
'type': 'PRESCRIPTION',
'name': 'Previous Prescription.jpg',
'url': 'https://storage.example.com/prescription-456.jpg',
'uploaded_at': '2024-02-08T14:30:00Z'
}
]
response = self.client.post(
self.appointments_url,
data=json.dumps(attachment_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'attachments' in data
assert len(data['attachments']) == 2
assert data['attachments'][0]['type'] == 'MEDICAL_REPORT'
def test_create_appointment_insurance_verification(self):
"""Test appointment creation with insurance verification."""
insurance_data = self.appointment_data.copy()
insurance_data['insurance'] = {
'provider': 'Malaysia National Insurance',
'policy_number': 'MNI-123456789',
'verification_required': True,
'pre_authorization_code': 'PA-2024-001'
}
response = self.client.post(
self.appointments_url,
data=json.dumps(insurance_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'insurance' in data
assert data['insurance']['verification_required'] is True
def test_create_appointment_with_cancellation_policy(self):
"""Test appointment creation with cancellation policy."""
policy_data = self.appointment_data.copy()
policy_data['cancellation_policy'] = {
'can_cancel_until': '2024-02-14T14:30:00+08:00', # 24 hours before
'cancellation_fee': 50.00,
'fee_applies_after': '2024-02-14T14:30:00+08:00'
}
response = self.client.post(
self.appointments_url,
data=json.dumps(policy_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'cancellation_policy' in data
def test_create_appointment_malformed_reminders(self):
"""Test appointment creation with malformed reminders JSON."""
invalid_data = self.appointment_data.copy()
invalid_data['reminders'] = 'invalid reminders format'
response = self.client.post(
self.appointments_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_appointment_tenant_isolation(self):
"""Test that appointment creation respects tenant isolation."""
response = self.client.post(
self.appointments_url,
data=json.dumps(self.appointment_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
# Appointment should be created in the authenticated tenant's context
assert 'tenant_id' in data
# This will be validated once implementation exists
def test_create_appointment_scheduling_validation(self):
"""Test that appointment creation validates business hours and scheduling rules."""
# Test with off-hours appointment
off_hours_data = self.appointment_data.copy()
off_hours_data['appointment_datetime'] = '2024-02-15T22:00:00+08:00' # 10 PM
response = self.client.post(
self.appointments_url,
data=json.dumps(off_hours_data),
content_type='application/json',
**self.tenant_auth
)
# This should fail if clinic hours are enforced
# This will be validated once implementation exists
if response.status_code == status.HTTP_400_BAD_REQUEST:
pass # Expected behavior
elif response.status_code == status.HTTP_201_CREATED:
pass # Also acceptable if 24/7 appointments are allowed
def test_create_appointment_with_consent(self):
"""Test appointment creation with patient consent."""
consent_data = self.appointment_data.copy()
consent_data['consents'] = [
{
'type': 'TREATMENT',
'given_at': '2024-02-10T10:00:00Z',
'expires_at': None,
'scope': 'This appointment only'
},
{
'type': 'TELEMEDICINE',
'given_at': '2024-02-10T10:00:00Z',
'expires_at': '2024-02-15T16:30:00Z',
'scope': 'Virtual consultation if needed'
}
]
response = self.client.post(
self.appointments_url,
data=json.dumps(consent_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'consents' in data
assert len(data['consents']) == 2

View File

@@ -0,0 +1,326 @@
"""
Contract test for GET /healthcare/patients endpoint.
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 HealthcarePatientsGetContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.patients_url = '/api/v1/healthcare/patients/'
# Tenant authentication header
self.tenant_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_token'}
def test_get_patients_success(self):
"""Test successful retrieval of patients list."""
response = self.client.get(
self.patients_url,
**self.tenant_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'patients' in data
assert isinstance(data['patients'], list)
# Check pagination structure
assert 'pagination' in data
pagination = data['pagination']
assert 'page' in pagination
assert 'limit' in pagination
assert 'total' in pagination
assert 'pages' in pagination
def test_get_patients_unauthorized(self):
"""Test patients list retrieval without authentication."""
response = self.client.get(self.patients_url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_get_patients_with_pagination(self):
"""Test patients list retrieval with pagination parameters."""
params = {
'page': 2,
'limit': 15
}
response = self.client.get(
self.patients_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data['pagination']['page'] == 2
assert data['pagination']['limit'] == 15
def test_get_patients_with_search(self):
"""Test patients list retrieval with search parameter."""
params = {
'search': 'tan'
}
response = self.client.get(
self.patients_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned patients should match search criteria
for patient in data['patients']:
search_match = (
'tan' in patient['name'].lower() or
'tan' in patient['ic_number'].lower() or
'tan' in patient['email'].lower()
)
assert search_match
def test_get_patients_filter_by_gender(self):
"""Test patients list retrieval filtered by gender."""
params = {
'gender': 'FEMALE'
}
response = self.client.get(
self.patients_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned patients should have the specified gender
for patient in data['patients']:
assert patient['gender'] == 'FEMALE'
def test_get_patients_filter_by_status(self):
"""Test patients list retrieval filtered by status."""
params = {
'status': 'ACTIVE'
}
response = self.client.get(
self.patients_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned patients should have the specified status
for patient in data['patients']:
assert patient['status'] == 'ACTIVE'
def test_get_patients_filter_by_age_range(self):
"""Test patients list retrieval filtered by age range."""
params = {
'min_age': 25,
'max_age': 65
}
response = self.client.get(
self.patients_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned patients should be within the age range
for patient in data['patients']:
assert 25 <= patient['age'] <= 65
def test_get_patients_data_structure(self):
"""Test that patient data structure matches the contract."""
response = self.client.get(
self.patients_url,
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['patients']) > 0:
patient = response.json()['patients'][0]
# Required fields according to contract
required_fields = [
'id', 'ic_number', 'name', 'gender', 'date_of_birth',
'age', 'phone', 'email', 'address', 'blood_type',
'allergies', 'medications', 'status', 'tenant_id',
'created_at', 'updated_at'
]
for field in required_fields:
assert field in patient
# Field types and enums
assert isinstance(patient['id'], str)
assert isinstance(patient['ic_number'], str)
assert isinstance(patient['name'], str)
assert patient['gender'] in ['MALE', 'FEMALE', 'OTHER']
assert isinstance(patient['date_of_birth'], str)
assert isinstance(patient['age'], int)
assert isinstance(patient['phone'], str)
assert isinstance(patient['email'], str)
assert patient['blood_type'] in ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-', 'UNKNOWN']
assert patient['status'] in ['ACTIVE', 'INACTIVE', 'DECEASED']
def test_get_patients_with_medical_history(self):
"""Test that patient data includes medical history."""
response = self.client.get(
self.patients_url,
data={'include_medical_history': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['patients']) > 0:
patient = response.json()['patients'][0]
# Should include medical history
assert 'medical_history' in patient
medical_history = patient['medical_history']
# Medical history should include relevant fields
expected_medical_fields = ['conditions', 'surgeries', 'family_history', 'immunizations']
for field in expected_medical_fields:
assert field in medical_history
def test_get_patients_with_emergency_contacts(self):
"""Test that patient data includes emergency contacts."""
response = self.client.get(
self.patients_url,
data={'include_emergency_contacts': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['patients']) > 0:
patient = response.json()['patients'][0]
# Should include emergency contacts
assert 'emergency_contacts' in patient
emergency_contacts = patient['emergency_contacts']
# Should be a list
assert isinstance(emergency_contacts, list)
if len(emergency_contacts) > 0:
contact = emergency_contacts[0]
expected_contact_fields = ['name', 'relationship', 'phone', 'email']
for field in expected_contact_fields:
assert field in contact
def test_get_patients_with_insurance_info(self):
"""Test that patient data includes insurance information."""
response = self.client.get(
self.patients_url,
data={'include_insurance': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['patients']) > 0:
patient = response.json()['patients'][0]
# Should include insurance information
assert 'insurance' in patient
insurance = patient['insurance']
# Insurance should include relevant fields
expected_insurance_fields = ['provider', 'policy_number', 'coverage_details', 'expiry_date']
for field in expected_insurance_fields:
assert field in insurance
def test_get_patients_sorting(self):
"""Test patients list retrieval with sorting."""
params = {
'sort_by': 'name',
'sort_order': 'asc'
}
response = self.client.get(
self.patients_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Patients should be sorted by name in ascending order
patient_names = [patient['name'] for patient in data['patients']]
assert patient_names == sorted(patient_names)
def test_get_patients_tenant_isolation(self):
"""Test that patients are isolated by tenant."""
response = self.client.get(
self.patients_url,
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK:
data = response.json()
# All returned patients should belong to the authenticated tenant
for patient in data['patients']:
assert 'tenant_id' in patient
# This will be validated once implementation exists
def test_get_patients_with_visit_history(self):
"""Test that patient data includes visit history."""
response = self.client.get(
self.patients_url,
data={'include_visits': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['patients']) > 0:
patient = response.json()['patients'][0]
# Should include visit history
assert 'visit_history' in patient
visit_history = patient['visit_history']
# Should be a list
assert isinstance(visit_history, list)
if len(visit_history) > 0:
visit = visit_history[0]
expected_visit_fields = ['date', 'doctor_id', 'diagnosis', 'treatment', 'notes']
for field in expected_visit_fields:
assert field in visit
def test_get_patients_data_compliance(self):
"""Test that patient data complies with healthcare data protection."""
response = self.client.get(
self.patients_url,
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['patients']) > 0:
patient = response.json()['patients'][0]
# Sensitive medical data should be properly handled
# Allergies and medications should be present for healthcare compliance
assert 'allergies' in patient
assert 'medications' in patient
# Address should be structured for privacy compliance
assert 'address' in patient
address = patient['address']
expected_address_fields = ['street', 'city', 'state', 'postal_code', 'country']
for field in expected_address_fields:
assert field in address

View File

@@ -0,0 +1,362 @@
"""
Contract test for POST /healthcare/patients endpoint.
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 HealthcarePatientsPostContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.patients_url = '/api/v1/healthcare/patients/'
# Tenant authentication header
self.tenant_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_token'}
# Valid 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', 'Peanuts'],
'medications': ['Metformin 500mg', 'Lisinopril 10mg'],
'emergency_contacts': [
{
'name': 'Siti binti Ibrahim',
'relationship': 'Spouse',
'phone': '+60198765432',
'email': 'siti.ibrahim@example.com'
}
],
'insurance': {
'provider': 'Malaysia National Insurance',
'policy_number': 'MNI-123456789',
'coverage_details': 'Full coverage',
'expiry_date': '2024-12-31'
},
'medical_history': {
'conditions': ['Type 2 Diabetes', 'Hypertension'],
'surgeries': ['Appendectomy (2015)'],
'family_history': ['Diabetes (paternal)', 'Hypertension (maternal)'],
'immunizations': ['COVID-19 Vaccine (2023)', 'Flu Vaccine (2023)']
}
}
def test_create_patient_success(self):
"""Test successful patient creation."""
response = self.client.post(
self.patients_url,
data=json.dumps(self.patient_data),
content_type='application/json',
**self.tenant_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert 'id' in data
assert data['ic_number'] == self.patient_data['ic_number']
assert data['name'] == self.patient_data['name']
assert data['gender'] == self.patient_data['gender']
assert data['date_of_birth'] == self.patient_data['date_of_birth']
assert data['age'] == 34 # Calculated from DOB
assert data['status'] == 'ACTIVE' # Default status
# Should have timestamps
assert 'created_at' in data
assert 'updated_at' in data
# Should have tenant_id from context
assert 'tenant_id' in data
# Should include medical information
assert data['blood_type'] == self.patient_data['blood_type']
assert data['allergies'] == self.patient_data['allergies']
assert data['medications'] == self.patient_data['medications']
def test_create_patient_unauthorized(self):
"""Test patient creation without authentication."""
response = self.client.post(
self.patients_url,
data=json.dumps(self.patient_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_patient_missing_required_fields(self):
"""Test patient creation with missing required fields."""
incomplete_data = self.patient_data.copy()
del incomplete_data['ic_number']
response = self.client.post(
self.patients_url,
data=json.dumps(incomplete_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
data = response.json()
assert 'ic_number' in data.get('errors', {})
def test_create_patient_invalid_ic_number(self):
"""Test patient creation with invalid IC number format."""
invalid_data = self.patient_data.copy()
invalid_data['ic_number'] = 'invalid-ic-format'
response = self.client.post(
self.patients_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_patient_invalid_gender(self):
"""Test patient creation with invalid gender."""
invalid_data = self.patient_data.copy()
invalid_data['gender'] = 'INVALID_GENDER'
response = self.client.post(
self.patients_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_patient_invalid_blood_type(self):
"""Test patient creation with invalid blood type."""
invalid_data = self.patient_data.copy()
invalid_data['blood_type'] = 'INVALID_BLOOD_TYPE'
response = self.client.post(
self.patients_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_patient_future_birth_date(self):
"""Test patient creation with future birth date."""
invalid_data = self.patient_data.copy()
invalid_data['date_of_birth'] = '2050-01-01' # Future date
response = self.client.post(
self.patients_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_patient_duplicate_ic_number(self):
"""Test patient creation with duplicate IC number."""
# First request should succeed (if implemented)
first_response = self.client.post(
self.patients_url,
data=json.dumps(self.patient_data),
content_type='application/json',
**self.tenant_auth
)
if first_response.status_code == status.HTTP_201_CREATED:
# Second request with same IC number should fail
second_response = self.client.post(
self.patients_url,
data=json.dumps(self.patient_data),
content_type='application/json',
**self.tenant_auth
)
assert second_response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_patient_with_minimal_data(self):
"""Test patient creation with minimal required data."""
minimal_data = {
'ic_number': '950505-05-5678',
'name': 'Lee Mei Lin',
'gender': 'FEMALE',
'date_of_birth': '1995-05-05'
}
response = self.client.post(
self.patients_url,
data=json.dumps(minimal_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert data['ic_number'] == minimal_data['ic_number']
assert data['name'] == minimal_data['name']
# Optional fields should have default values
assert data['blood_type'] == 'UNKNOWN'
assert data['allergies'] == []
def test_create_patient_invalid_email(self):
"""Test patient creation with invalid email format."""
invalid_data = self.patient_data.copy()
invalid_data['email'] = 'invalid-email-format'
response = self.client.post(
self.patients_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_patient_malformed_address(self):
"""Test patient creation with malformed address JSON."""
invalid_data = self.patient_data.copy()
invalid_data['address'] = 'invalid address format'
response = self.client.post(
self.patients_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_patient_missing_address_fields(self):
"""Test patient creation with missing address fields."""
invalid_data = self.patient_data.copy()
invalid_data['address'] = {'street': '123 Street'} # Missing required fields
response = self.client.post(
self.patients_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_patient_invalid_emergency_contact(self):
"""Test patient creation with invalid emergency contact."""
invalid_data = self.patient_data.copy()
invalid_data['emergency_contacts'] = [
{
'name': 'Emergency Contact',
# Missing required relationship and phone
}
]
response = self.client.post(
self.patients_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_patient_age_calculation(self):
"""Test that age is calculated correctly from date of birth."""
response = self.client.post(
self.patients_url,
data=json.dumps(self.patient_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
# Age should be calculated based on current date and birth date
# This will be validated once implementation exists
assert isinstance(data['age'], int)
assert data['age'] > 0
def test_create_patient_tenant_isolation(self):
"""Test that patient creation respects tenant isolation."""
response = self.client.post(
self.patients_url,
data=json.dumps(self.patient_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
# Patient should be created in the authenticated tenant's context
assert 'tenant_id' in data
# This will be validated once implementation exists
def test_create_patient_data_privacy_compliance(self):
"""Test that patient creation handles sensitive data according to PDPA."""
response = self.client.post(
self.patients_url,
data=json.dumps(self.patient_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
# Sensitive medical data should be stored and handled properly
assert 'allergies' in data
assert 'medications' in data
assert 'medical_history' in data
# IC number should be handled with special care for privacy
assert data['ic_number'] == self.patient_data['ic_number']
def test_create_patient_with_consent_info(self):
"""Test patient creation with consent information."""
consent_data = self.patient_data.copy()
consent_data['consents'] = [
{
'type': 'TREATMENT',
'given_at': '2024-01-15T10:00:00Z',
'expires_at': '2025-01-15T10:00:00Z',
'notes': 'Consent for general treatment'
},
{
'type': 'DATA_SHARING',
'given_at': '2024-01-15T10:00:00Z',
'expires_at': None,
'notes': 'Consent to share data with insurance provider'
}
]
response = self.client.post(
self.patients_url,
data=json.dumps(consent_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'consents' in data
assert len(data['consents']) == 2
assert data['consents'][0]['type'] == 'TREATMENT'

View File

@@ -0,0 +1,280 @@
"""
Contract test for GET /modules endpoint.
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 ModulesGetContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.modules_url = '/api/v1/modules/'
# Admin authentication header
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer admin_token'}
# Tenant admin authentication header
self.tenant_admin_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_admin_token'}
def test_get_modules_success_admin(self):
"""Test successful retrieval of modules list by admin."""
response = self.client.get(
self.modules_url,
**self.admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'modules' in data
assert isinstance(data['modules'], list)
# Should return all available modules
expected_modules = ['retail', 'healthcare', 'education', 'logistics', 'beauty']
returned_modules = [module['key'] for module in data['modules']]
for expected_module in expected_modules:
assert expected_module in returned_modules
def test_get_modules_success_tenant_admin(self):
"""Test successful retrieval of modules list by tenant admin."""
response = self.client.get(
self.modules_url,
**self.tenant_admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'modules' in data
assert isinstance(data['modules'], list)
# Tenant admin should see modules available to their subscription
# This will be validated once implementation exists
def test_get_modules_unauthorized(self):
"""Test modules list retrieval without authentication."""
response = self.client.get(self.modules_url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_get_modules_filter_by_category(self):
"""Test modules list retrieval filtered by category."""
params = {
'category': 'core'
}
response = self.client.get(
self.modules_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned modules should have the specified category
for module in data['modules']:
assert module['category'] == 'core'
def test_get_modules_filter_by_status(self):
"""Test modules list retrieval filtered by status."""
params = {
'status': 'ACTIVE'
}
response = self.client.get(
self.modules_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned modules should have the specified status
for module in data['modules']:
assert module['status'] == 'ACTIVE'
def test_get_modules_filter_by_tenant(self):
"""Test modules list retrieval filtered by tenant subscription."""
params = {
'tenant_id': 'test-tenant-id',
'only_subscribed': 'true'
}
response = self.client.get(
self.modules_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned modules should be subscribed by the tenant
# This will be validated once implementation exists
def test_get_modules_with_details(self):
"""Test modules list retrieval with detailed information."""
params = {
'include_details': 'true'
}
response = self.client.get(
self.modules_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
if len(data['modules']) > 0:
module = data['modules'][0]
# Should include detailed information
assert 'description' in module
assert 'features' in module
assert 'pricing' in module
assert 'requirements' in module
# Features should be a list
assert isinstance(module['features'], list)
# Pricing should include relevant information
pricing = module['pricing']
assert 'base_price' in pricing
assert 'currency' in pricing
assert 'billing_cycle' in pricing
def test_get_modules_tenant_specific_view(self):
"""Test that tenant admin sees modules available to their subscription."""
response = self.client.get(
self.modules_url,
**self.tenant_admin_auth
)
if response.status_code == status.HTTP_200_OK:
data = response.json()
# Tenant admin should see which modules are available and which are subscribed
# This will be validated once implementation exists
pass
def test_get_modules_data_structure(self):
"""Test that module data structure matches the contract."""
response = self.client.get(
self.modules_url,
**self.admin_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['modules']) > 0:
module = response.json()['modules'][0]
# Required fields according to contract
required_fields = [
'id', 'key', 'name', 'category', 'status',
'version', 'created_at', 'updated_at'
]
for field in required_fields:
assert field in module
# Field types and enums
assert isinstance(module['id'], str)
assert isinstance(module['key'], str)
assert isinstance(module['name'], str)
assert module['category'] in ['core', 'industry', 'integration']
assert module['status'] in ['ACTIVE', 'INACTIVE', 'DEPRECATED']
assert isinstance(module['version'], str)
def test_get_modules_with_compatibility_info(self):
"""Test that module data includes compatibility information."""
response = self.client.get(
self.modules_url,
data={'include_compatibility': 'true'},
**self.admin_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['modules']) > 0:
module = response.json()['modules'][0]
# Should include compatibility information
assert 'compatibility' in module
compatibility = module['compatibility']
# Compatibility should include relevant fields
expected_compatibility_fields = ['min_platform_version', 'required_modules', 'conflicts']
for field in expected_compatibility_fields:
assert field in compatibility
def test_get_modules_with_usage_stats(self):
"""Test that module data includes usage statistics for admin."""
response = self.client.get(
self.modules_url,
data={'include_stats': 'true'},
**self.admin_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['modules']) > 0:
module = response.json()['modules'][0]
# Should include usage statistics
assert 'usage_stats' in module
stats = module['usage_stats']
# Stats should include relevant fields
expected_stats_fields = ['active_tenants', 'total_users', 'api_calls_today', 'storage_used']
for field in expected_stats_fields:
assert field in stats
def test_get_modules_search(self):
"""Test modules list retrieval with search functionality."""
params = {
'search': 'retail'
}
response = self.client.get(
self.modules_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned modules should match search criteria
for module in data['modules']:
search_match = (
'retail' in module['name'].lower() or
'retail' in module['key'].lower() or
'retail' in module['description'].lower()
)
assert search_match
def test_get_modules_sorting(self):
"""Test modules list retrieval with sorting."""
params = {
'sort_by': 'name',
'sort_order': 'asc'
}
response = self.client.get(
self.modules_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Modules should be sorted by name in ascending order
module_names = [module['name'] for module in data['modules']]
assert module_names == sorted(module_names)

View File

@@ -0,0 +1,273 @@
"""
Contract test for GET /retail/products endpoint.
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 RetailProductsGetContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.products_url = '/api/v1/retail/products/'
# Tenant authentication header
self.tenant_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_token'}
def test_get_products_success(self):
"""Test successful retrieval of products list."""
response = self.client.get(
self.products_url,
**self.tenant_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'products' in data
assert isinstance(data['products'], list)
# Check pagination structure
assert 'pagination' in data
pagination = data['pagination']
assert 'page' in pagination
assert 'limit' in pagination
assert 'total' in pagination
assert 'pages' in pagination
def test_get_products_unauthorized(self):
"""Test products list retrieval without authentication."""
response = self.client.get(self.products_url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_get_products_with_pagination(self):
"""Test products list retrieval with pagination parameters."""
params = {
'page': 2,
'limit': 20
}
response = self.client.get(
self.products_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data['pagination']['page'] == 2
assert data['pagination']['limit'] == 20
def test_get_products_with_search(self):
"""Test products list retrieval with search parameter."""
params = {
'search': 'laptop'
}
response = self.client.get(
self.products_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned products should match search criteria
for product in data['products']:
search_match = (
'laptop' in product['name'].lower() or
'laptop' in product['description'].lower() or
'laptop' in product['sku'].lower()
)
assert search_match
def test_get_products_filter_by_category(self):
"""Test products list retrieval filtered by category."""
params = {
'category': 'ELECTRONICS'
}
response = self.client.get(
self.products_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned products should have the specified category
for product in data['products']:
assert product['category'] == 'ELECTRONICS'
def test_get_products_filter_by_status(self):
"""Test products list retrieval filtered by status."""
params = {
'status': 'ACTIVE'
}
response = self.client.get(
self.products_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned products should have the specified status
for product in data['products']:
assert product['status'] == 'ACTIVE'
def test_get_products_filter_by_price_range(self):
"""Test products list retrieval filtered by price range."""
params = {
'min_price': 100,
'max_price': 1000
}
response = self.client.get(
self.products_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned products should be within the price range
for product in data['products']:
assert 100 <= product['price'] <= 1000
def test_get_products_filter_by_stock(self):
"""Test products list retrieval filtered by stock availability."""
params = {
'in_stock': 'true'
}
response = self.client.get(
self.products_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned products should have stock available
for product in data['products']:
assert product['stock_quantity'] > 0
def test_get_products_data_structure(self):
"""Test that product data structure matches the contract."""
response = self.client.get(
self.products_url,
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['products']) > 0:
product = response.json()['products'][0]
# Required fields according to contract
required_fields = [
'id', 'sku', 'name', 'description', 'category',
'price', 'cost', 'stock_quantity', 'status',
'tenant_id', 'created_at', 'updated_at'
]
for field in required_fields:
assert field in product
# Field types and enums
assert isinstance(product['id'], str)
assert isinstance(product['sku'], str)
assert isinstance(product['name'], str)
assert isinstance(product['description'], str)
assert product['category'] in ['ELECTRONICS', 'CLOTHING', 'FOOD', 'BOOKS', 'OTHER']
assert isinstance(product['price'], (int, float))
assert isinstance(product['cost'], (int, float))
assert isinstance(product['stock_quantity'], int)
assert product['status'] in ['ACTIVE', 'INACTIVE', 'DISCONTINUED']
def test_get_products_with_inventory_info(self):
"""Test that product data includes inventory information."""
response = self.client.get(
self.products_url,
data={'include_inventory': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['products']) > 0:
product = response.json()['products'][0]
# Should include inventory information
assert 'inventory' in product
inventory = product['inventory']
# Inventory should include relevant fields
expected_inventory_fields = ['location', 'reorder_point', 'supplier', 'last_restocked']
for field in expected_inventory_fields:
assert field in inventory
def test_get_products_with_pricing_info(self):
"""Test that product data includes detailed pricing information."""
response = self.client.get(
self.products_url,
data={'include_pricing': 'true'},
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['products']) > 0:
product = response.json()['products'][0]
# Should include detailed pricing information
assert 'pricing' in product
pricing = product['pricing']
# Pricing should include relevant fields
expected_pricing_fields = ['base_price', 'tax_rate', 'discount_price', 'currency']
for field in expected_pricing_fields:
assert field in pricing
def test_get_products_sorting(self):
"""Test products list retrieval with sorting."""
params = {
'sort_by': 'name',
'sort_order': 'asc'
}
response = self.client.get(
self.products_url,
data=params,
**self.tenant_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Products should be sorted by name in ascending order
product_names = [product['name'] for product in data['products']]
assert product_names == sorted(product_names)
def test_get_products_tenant_isolation(self):
"""Test that products are isolated by tenant."""
response = self.client.get(
self.products_url,
**self.tenant_auth
)
if response.status_code == status.HTTP_200_OK:
data = response.json()
# All returned products should belong to the authenticated tenant
for product in data['products']:
assert 'tenant_id' in product
# This will be validated once implementation exists

View File

@@ -0,0 +1,314 @@
"""
Contract test for POST /retail/products endpoint.
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 RetailProductsPostContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.products_url = '/api/v1/retail/products/'
# Tenant authentication header
self.tenant_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_token'}
# Valid product data
self.product_data = {
'sku': 'LPT-001',
'name': 'Laptop Pro 15"',
'description': 'High-performance laptop for business use',
'category': 'ELECTRONICS',
'price': 2499.99,
'cost': 1800.00,
'stock_quantity': 50,
'barcode': '1234567890123',
'brand': 'TechBrand',
'model': 'PRO-15-2024',
'weight': 2.5,
'dimensions': {
'length': 35.5,
'width': 25.0,
'height': 2.0,
'unit': 'cm'
},
'tax_rate': 6.0,
'inventory': {
'location': 'Warehouse A',
'reorder_point': 10,
'supplier': 'TechSupplier Inc.',
'lead_time_days': 7
},
'attributes': {
'color': 'Space Gray',
'processor': 'Intel i7',
'ram': '16GB',
'storage': '512GB SSD'
}
}
def test_create_product_success(self):
"""Test successful product creation."""
response = self.client.post(
self.products_url,
data=json.dumps(self.product_data),
content_type='application/json',
**self.tenant_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert 'id' in data
assert data['sku'] == self.product_data['sku']
assert data['name'] == self.product_data['name']
assert data['description'] == self.product_data['description']
assert data['category'] == self.product_data['category']
assert data['price'] == self.product_data['price']
assert data['cost'] == self.product_data['cost']
assert data['stock_quantity'] == self.product_data['stock_quantity']
assert data['status'] == 'ACTIVE' # Default status
# Should have timestamps
assert 'created_at' in data
assert 'updated_at' in data
# Should have tenant_id from context
assert 'tenant_id' in data
# Should include attributes
assert 'attributes' in data
assert data['attributes']['color'] == self.product_data['attributes']['color']
def test_create_product_unauthorized(self):
"""Test product creation without authentication."""
response = self.client.post(
self.products_url,
data=json.dumps(self.product_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_product_missing_required_fields(self):
"""Test product creation with missing required fields."""
incomplete_data = self.product_data.copy()
del incomplete_data['name']
response = self.client.post(
self.products_url,
data=json.dumps(incomplete_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
data = response.json()
assert 'name' in data.get('errors', {})
def test_create_product_invalid_category(self):
"""Test product creation with invalid category."""
invalid_data = self.product_data.copy()
invalid_data['category'] = 'INVALID_CATEGORY'
response = self.client.post(
self.products_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_product_negative_price(self):
"""Test product creation with negative price."""
invalid_data = self.product_data.copy()
invalid_data['price'] = -100
response = self.client.post(
self.products_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_product_negative_stock(self):
"""Test product creation with negative stock quantity."""
invalid_data = self.product_data.copy()
invalid_data['stock_quantity'] = -10
response = self.client.post(
self.products_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_product_duplicate_sku(self):
"""Test product creation with duplicate SKU."""
# First request should succeed (if implemented)
first_response = self.client.post(
self.products_url,
data=json.dumps(self.product_data),
content_type='application/json',
**self.tenant_auth
)
if first_response.status_code == status.HTTP_201_CREATED:
# Second request with same SKU should fail
second_response = self.client.post(
self.products_url,
data=json.dumps(self.product_data),
content_type='application/json',
**self.tenant_auth
)
assert second_response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_product_with_image_urls(self):
"""Test product creation with image URLs."""
image_data = self.product_data.copy()
image_data['images'] = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg'
]
response = self.client.post(
self.products_url,
data=json.dumps(image_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'images' in data
assert len(data['images']) == 2
assert data['images'][0] == image_data['images'][0]
def test_create_product_with_variants(self):
"""Test product creation with variants."""
variants_data = self.product_data.copy()
variants_data['variants'] = [
{
'sku': 'LPT-001-BLACK',
'name': 'Laptop Pro 15" - Black',
'attributes': {'color': 'Black'},
'price_adjustment': 0,
'stock_quantity': 25
},
{
'sku': 'LPT-001-WHITE',
'name': 'Laptop Pro 15" - White',
'attributes': {'color': 'White'},
'price_adjustment': 50,
'stock_quantity': 15
}
]
response = self.client.post(
self.products_url,
data=json.dumps(variants_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'variants' in data
assert len(data['variants']) == 2
assert data['variants'][0]['sku'] == 'LPT-001-BLACK'
def test_create_product_malformed_dimensions(self):
"""Test product creation with malformed dimensions JSON."""
invalid_data = self.product_data.copy()
invalid_data['dimensions'] = 'invalid dimensions format'
response = self.client.post(
self.products_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_product_missing_dimension_fields(self):
"""Test product creation with missing dimension fields."""
invalid_data = self.product_data.copy()
invalid_data['dimensions'] = {'length': 35.5} # Missing width, height, unit
response = self.client.post(
self.products_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_product_tenant_isolation(self):
"""Test that product creation respects tenant isolation."""
response = self.client.post(
self.products_url,
data=json.dumps(self.product_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
# Product should be created in the authenticated tenant's context
assert 'tenant_id' in data
# This will be validated once implementation exists
def test_create_product_with_tags(self):
"""Test product creation with tags."""
tags_data = self.product_data.copy()
tags_data['tags'] = ['laptop', 'business', 'electronics', 'premium']
response = self.client.post(
self.products_url,
data=json.dumps(tags_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'tags' in data
assert len(data['tags']) == 4
assert 'laptop' in data['tags']
def test_create_product_with_bulk_pricing(self):
"""Test product creation with bulk pricing tiers."""
bulk_data = self.product_data.copy()
bulk_data['bulk_pricing'] = [
{'min_quantity': 5, 'price': 2399.99},
{'min_quantity': 10, 'price': 2299.99},
{'min_quantity': 25, 'price': 2199.99}
]
response = self.client.post(
self.products_url,
data=json.dumps(bulk_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'bulk_pricing' in data
assert len(data['bulk_pricing']) == 3
assert data['bulk_pricing'][0]['min_quantity'] == 5

View File

@@ -0,0 +1,388 @@
"""
Contract test for POST /retail/sales endpoint.
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 RetailSalesPostContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.sales_url = '/api/v1/retail/sales/'
# Tenant authentication header
self.tenant_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_token'}
# Valid sale data
self.sale_data = {
'customer': {
'name': 'John Doe',
'email': 'john.doe@example.com',
'phone': '+60123456789',
'address': {
'street': '123 Customer Street',
'city': 'Kuala Lumpur',
'state': 'Wilayah Persekutuan',
'postal_code': '50000',
'country': 'Malaysia'
}
},
'items': [
{
'product_id': 'product-001',
'sku': 'LPT-001',
'quantity': 2,
'unit_price': 2499.99,
'discount_percentage': 5.0,
'tax_rate': 6.0
},
{
'product_id': 'product-002',
'sku': 'MOU-001',
'quantity': 1,
'unit_price': 99.99,
'discount_percentage': 0.0,
'tax_rate': 6.0
}
],
'payment': {
'method': 'CASH',
'amount_paid': 5300.00,
'reference_number': 'CASH-001'
},
'discount': {
'type': 'percentage',
'value': 2.0,
'reason': 'Loyalty discount'
},
'notes': 'Customer requested expedited delivery',
'sales_channel': 'IN_STORE',
'staff_id': 'staff-001'
}
def test_create_sale_success(self):
"""Test successful sale creation."""
response = self.client.post(
self.sales_url,
data=json.dumps(self.sale_data),
content_type='application/json',
**self.tenant_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert 'id' in data
assert data['status'] == 'COMPLETED'
assert 'customer' in data
assert 'items' in data
assert 'payment' in data
assert 'totals' in data
# Check customer information
customer = data['customer']
assert customer['name'] == self.sale_data['customer']['name']
assert customer['email'] == self.sale_data['customer']['email']
# Check items
items = data['items']
assert len(items) == 2
assert items[0]['product_id'] == self.sale_data['items'][0]['product_id']
assert items[0]['quantity'] == self.sale_data['items'][0]['quantity']
# Check payment
payment = data['payment']
assert payment['method'] == self.sale_data['payment']['method']
assert payment['amount_paid'] == self.sale_data['payment']['amount_paid']
# Check totals
totals = data['totals']
assert 'subtotal' in totals
assert 'discount_amount' in totals
assert 'tax_amount' in totals
assert 'total_amount' in totals
# Should have timestamps
assert 'created_at' in data
assert 'updated_at' in data
# Should have tenant_id from context
assert 'tenant_id' in data
def test_create_sale_unauthorized(self):
"""Test sale creation without authentication."""
response = self.client.post(
self.sales_url,
data=json.dumps(self.sale_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_sale_missing_required_fields(self):
"""Test sale creation with missing required fields."""
incomplete_data = self.sale_data.copy()
del incomplete_data['customer']
response = self.client.post(
self.sales_url,
data=json.dumps(incomplete_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
data = response.json()
assert 'customer' in data.get('errors', {})
def test_create_sale_empty_items(self):
"""Test sale creation with empty items list."""
invalid_data = self.sale_data.copy()
invalid_data['items'] = []
response = self.client.post(
self.sales_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_sale_invalid_payment_method(self):
"""Test sale creation with invalid payment method."""
invalid_data = self.sale_data.copy()
invalid_data['payment']['method'] = 'INVALID_METHOD'
response = self.client.post(
self.sales_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_sale_insufficient_payment(self):
"""Test sale creation with insufficient payment amount."""
invalid_data = self.sale_data.copy()
invalid_data['payment']['amount_paid'] = 100.00 # Much less than total
response = self.client.post(
self.sales_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_sale_negative_quantity(self):
"""Test sale creation with negative quantity."""
invalid_data = self.sale_data.copy()
invalid_data['items'][0]['quantity'] = -1
response = self.client.post(
self.sales_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_sale_invalid_discount_percentage(self):
"""Test sale creation with invalid discount percentage."""
invalid_data = self.sale_data.copy()
invalid_data['items'][0]['discount_percentage'] = 150.0 # Over 100%
response = self.client.post(
self.sales_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.tenant_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_sale_with_installment(self):
"""Test sale creation with installment payment."""
installment_data = self.sale_data.copy()
installment_data['payment']['method'] = 'INSTALLMENT'
installment_data['payment']['installment_plan'] = {
'down_payment': 1000.00,
'number_of_installments': 12,
'installment_amount': 358.33,
'first_installment_date': '2024-02-01'
}
response = self.client.post(
self.sales_url,
data=json.dumps(installment_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert data['status'] == 'COMPLETED'
assert 'installment_plan' in data['payment']
def test_create_sale_with_multiple_payments(self):
"""Test sale creation with multiple payment methods."""
multi_payment_data = self.sale_data.copy()
multi_payment_data['payment'] = [
{
'method': 'CASH',
'amount_paid': 2000.00,
'reference_number': 'CASH-001'
},
{
'method': 'CARD',
'amount_paid': 3300.00,
'reference_number': 'CARD-001',
'card_last4': '4242'
}
]
response = self.client.post(
self.sales_url,
data=json.dumps(multi_payment_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert len(data['payment']) == 2
assert data['payment'][0]['method'] == 'CASH'
def test_create_sale_with_loyalty_points(self):
"""Test sale creation with loyalty points redemption."""
loyalty_data = self.sale_data.copy()
loyalty_data['loyalty'] = {
'points_used': 1000,
'points_value': 100.00,
'customer_id': 'customer-001'
}
response = self.client.post(
self.sales_url,
data=json.dumps(loyalty_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'loyalty' in data
assert data['loyalty']['points_used'] == 1000
def test_create_sale_with_delivery_info(self):
"""Test sale creation with delivery information."""
delivery_data = self.sale_data.copy()
delivery_data['delivery'] = {
'method': 'DELIVERY',
'address': self.sale_data['customer']['address'],
'scheduled_date': '2024-01-20',
'delivery_fee': 50.00,
'notes': 'Leave at front door'
}
response = self.client.post(
self.sales_url,
data=json.dumps(delivery_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'delivery' in data
assert data['delivery']['method'] == 'DELIVERY'
def test_create_sale_with_exchange_items(self):
"""Test sale creation with item exchange."""
exchange_data = self.sale_data.copy()
exchange_data['exchange'] = {
'items': [
{
'product_id': 'old-product-001',
'sku': 'OLD-001',
'condition': 'GOOD',
'exchange_value': 500.00
}
],
'total_exchange_value': 500.00
}
response = self.client.post(
self.sales_url,
data=json.dumps(exchange_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'exchange' in data
assert len(data['exchange']['items']) == 1
def test_create_sale_tax_calculation(self):
"""Test that tax calculation is correct."""
response = self.client.post(
self.sales_url,
data=json.dumps(self.sale_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
totals = data['totals']
# Verify tax calculation (6% GST on subtotal after discounts)
# This will be validated once implementation exists
assert 'tax_amount' in totals
assert totals['tax_amount'] >= 0
def test_create_sale_tenant_isolation(self):
"""Test that sale creation respects tenant isolation."""
response = self.client.post(
self.sales_url,
data=json.dumps(self.sale_data),
content_type='application/json',
**self.tenant_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
# Sale should be created in the authenticated tenant's context
assert 'tenant_id' in data
# This will be validated once implementation exists
def test_create_sale_inventory_validation(self):
"""Test that sale creation validates inventory availability."""
response = self.client.post(
self.sales_url,
data=json.dumps(self.sale_data),
content_type='application/json',
**self.tenant_auth
)
# This test will ensure that the system checks if sufficient stock is available
# The test should pass if implementation exists and validates inventory
if response.status_code == status.HTTP_201_CREATED:
# Success means inventory was available
pass
elif response.status_code == status.HTTP_400_BAD_REQUEST:
# This could also be valid if inventory validation fails
pass

View File

@@ -0,0 +1,224 @@
"""
Contract test for GET /subscriptions endpoint.
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 SubscriptionsGetContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.subscriptions_url = '/api/v1/subscriptions/'
# Admin authentication header
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer admin_token'}
# Tenant admin authentication header
self.tenant_admin_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_admin_token'}
def test_get_subscriptions_success_admin(self):
"""Test successful retrieval of subscriptions list by admin."""
response = self.client.get(
self.subscriptions_url,
**self.admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'subscriptions' in data
assert isinstance(data['subscriptions'], list)
# Check pagination structure
assert 'pagination' in data
pagination = data['pagination']
assert 'page' in pagination
assert 'limit' in pagination
assert 'total' in pagination
assert 'pages' in pagination
def test_get_subscriptions_success_tenant_admin(self):
"""Test successful retrieval of subscriptions list by tenant admin."""
response = self.client.get(
self.subscriptions_url,
**self.tenant_admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'subscriptions' in data
assert isinstance(data['subscriptions'], list)
# Tenant admin should only see subscriptions from their tenant
# This will be validated once implementation exists
def test_get_subscriptions_unauthorized(self):
"""Test subscriptions list retrieval without authentication."""
response = self.client.get(self.subscriptions_url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_get_subscriptions_with_pagination(self):
"""Test subscriptions list retrieval with pagination parameters."""
params = {
'page': 2,
'limit': 10
}
response = self.client.get(
self.subscriptions_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data['pagination']['page'] == 2
assert data['pagination']['limit'] == 10
def test_get_subscriptions_filter_by_status(self):
"""Test subscriptions list retrieval filtered by status."""
params = {
'status': 'ACTIVE'
}
response = self.client.get(
self.subscriptions_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned subscriptions should have the specified status
for subscription in data['subscriptions']:
assert subscription['status'] == 'ACTIVE'
def test_get_subscriptions_filter_by_plan(self):
"""Test subscriptions list retrieval filtered by plan."""
params = {
'plan': 'GROWTH'
}
response = self.client.get(
self.subscriptions_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned subscriptions should have the specified plan
for subscription in data['subscriptions']:
assert subscription['plan'] == 'GROWTH'
def test_get_subscriptions_filter_by_tenant(self):
"""Test subscriptions list retrieval filtered by tenant."""
params = {
'tenant_id': 'test-tenant-id'
}
response = self.client.get(
self.subscriptions_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned subscriptions should belong to the specified tenant
for subscription in data['subscriptions']:
assert subscription['tenant_id'] == 'test-tenant-id'
def test_get_subscriptions_tenant_isolation(self):
"""Test that tenant admin can only see subscriptions from their tenant."""
# This test verifies tenant isolation for subscription data
response = self.client.get(
self.subscriptions_url,
**self.tenant_admin_auth
)
if response.status_code == status.HTTP_200_OK:
data = response.json()
# For tenant users, all returned subscriptions should belong to their tenant
# This will be validated once implementation exists
pass
def test_get_subscriptions_data_structure(self):
"""Test that subscription data structure matches the contract."""
response = self.client.get(
self.subscriptions_url,
**self.admin_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['subscriptions']) > 0:
subscription = response.json()['subscriptions'][0]
# Required fields according to contract
required_fields = [
'id', 'tenant_id', 'plan', 'status', 'pricing_model',
'billing_cycle', 'current_period_start', 'current_period_end',
'trial_end', 'created_at', 'updated_at'
]
for field in required_fields:
assert field in subscription
# Field types and enums
assert isinstance(subscription['id'], str)
assert isinstance(subscription['tenant_id'], str)
assert subscription['plan'] in ['STARTER', 'GROWTH', 'PRO', 'ENTERPRISE']
assert subscription['status'] in ['TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELLED', 'EXPIRED']
assert subscription['pricing_model'] in ['SUBSCRIPTION', 'PERPETUAL']
assert subscription['billing_cycle'] in ['MONTHLY', 'QUARTERLY', 'YEARLY']
def test_get_subscriptions_with_usage_data(self):
"""Test that subscription data includes usage information."""
response = self.client.get(
self.subscriptions_url,
**self.admin_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['subscriptions']) > 0:
subscription = response.json()['subscriptions'][0]
# Should include usage metrics
assert 'usage' in subscription
usage = subscription['usage']
# Usage should include relevant metrics
expected_usage_fields = ['users_count', 'storage_used', 'api_calls']
for field in expected_usage_fields:
assert field in usage
def test_get_subscriptions_with_billing_info(self):
"""Test that subscription data includes billing information."""
response = self.client.get(
self.subscriptions_url,
**self.admin_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['subscriptions']) > 0:
subscription = response.json()['subscriptions'][0]
# Should include billing information
assert 'billing' in subscription
billing = subscription['billing']
# Billing should include relevant fields
expected_billing_fields = ['next_billing_date', 'amount', 'currency', 'payment_method']
for field in expected_billing_fields:
assert field in billing

View File

@@ -0,0 +1,264 @@
"""
Contract test for POST /subscriptions endpoint.
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 SubscriptionsPostContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.subscriptions_url = '/api/v1/subscriptions/'
# Admin authentication header
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer admin_token'}
# Tenant admin authentication header
self.tenant_admin_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_admin_token'}
# Valid subscription data
self.subscription_data = {
'tenant_id': 'test-tenant-id',
'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,
'notes': 'Subscription for retail business'
}
def test_create_subscription_success_admin(self):
"""Test successful subscription creation by admin."""
response = self.client.post(
self.subscriptions_url,
data=json.dumps(self.subscription_data),
content_type='application/json',
**self.admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert 'id' in data
assert data['tenant_id'] == self.subscription_data['tenant_id']
assert data['plan'] == self.subscription_data['plan']
assert data['pricing_model'] == self.subscription_data['pricing_model']
assert data['billing_cycle'] == self.subscription_data['billing_cycle']
assert data['status'] == 'TRIAL' # Default status with trial_days
# Should have timestamps
assert 'created_at' in data
assert 'updated_at' in data
# Should have billing period information
assert 'current_period_start' in data
assert 'current_period_end' in data
assert 'trial_end' in data
# Should include modules
assert 'modules' in data
assert data['modules'] == self.subscription_data['modules']
def test_create_subscription_success_tenant_admin(self):
"""Test successful subscription creation by tenant admin."""
# Tenant admin creates subscription for their own tenant
tenant_subscription_data = self.subscription_data.copy()
del tenant_subscription_data['tenant_id'] # Should be inferred from context
response = self.client.post(
self.subscriptions_url,
data=json.dumps(tenant_subscription_data),
content_type='application/json',
**self.tenant_admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert 'id' in data
assert data['plan'] == tenant_subscription_data['plan']
assert data['tenant_id'] # Should be auto-populated
def test_create_subscription_unauthorized(self):
"""Test subscription creation without authentication."""
response = self.client.post(
self.subscriptions_url,
data=json.dumps(self.subscription_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_subscription_forbidden(self):
"""Test subscription creation by regular user (no permissions)."""
user_auth = {'HTTP_AUTHORIZATION': 'Bearer user_token'}
response = self.client.post(
self.subscriptions_url,
data=json.dumps(self.subscription_data),
content_type='application/json',
**user_auth
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_create_subscription_missing_required_fields(self):
"""Test subscription creation with missing required fields."""
incomplete_data = self.subscription_data.copy()
del incomplete_data['plan']
response = self.client.post(
self.subscriptions_url,
data=json.dumps(incomplete_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
data = response.json()
assert 'plan' in data.get('errors', {})
def test_create_subscription_invalid_plan(self):
"""Test subscription creation with invalid plan."""
invalid_data = self.subscription_data.copy()
invalid_data['plan'] = 'INVALID_PLAN'
response = self.client.post(
self.subscriptions_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_subscription_invalid_billing_cycle(self):
"""Test subscription creation with invalid billing cycle."""
invalid_data = self.subscription_data.copy()
invalid_data['billing_cycle'] = 'INVALID_CYCLE'
response = self.client.post(
self.subscriptions_url,
data=json.dumps(incomplete_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_subscription_duplicate_tenant(self):
"""Test subscription creation with duplicate tenant."""
# First request should succeed (if implemented)
first_response = self.client.post(
self.subscriptions_url,
data=json.dumps(self.subscription_data),
content_type='application/json',
**self.admin_auth
)
if first_response.status_code == status.HTTP_201_CREATED:
# Second request with same tenant should fail
second_response = self.client.post(
self.subscriptions_url,
data=json.dumps(self.subscription_data),
content_type='application/json',
**self.admin_auth
)
assert second_response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_subscription_without_trial(self):
"""Test subscription creation without trial period."""
no_trial_data = self.subscription_data.copy()
del no_trial_data['trial_days']
response = self.client.post(
self.subscriptions_url,
data=json.dumps(no_trial_data),
content_type='application/json',
**self.admin_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
# Should be active immediately without trial
assert data['status'] == 'ACTIVE'
assert 'trial_end' not in data or data['trial_end'] is None
def test_create_subscription_with_invalid_modules(self):
"""Test subscription creation with invalid modules."""
invalid_data = self.subscription_data.copy()
invalid_data['modules'] = ['invalid_module']
response = self.client.post(
self.subscriptions_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_subscription_tenant_admin_cross_tenant(self):
"""Test that tenant admin cannot create subscription for other tenant."""
# Tenant admin trying to create subscription for different tenant
response = self.client.post(
self.subscriptions_url,
data=json.dumps(self.subscription_data),
content_type='application/json',
**self.tenant_admin_auth
)
# Should fail because tenant_id doesn't match their tenant
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_create_subscription_payment_method_validation(self):
"""Test subscription creation with invalid payment method."""
invalid_data = self.subscription_data.copy()
invalid_data['payment_method'] = {
'type': 'CARD',
'card_last4': '4242',
# Missing required expiry fields
}
response = self.client.post(
self.subscriptions_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_subscription_with_promo_code(self):
"""Test subscription creation with promo code."""
promo_data = self.subscription_data.copy()
promo_data['promo_code'] = 'WELCOME20'
response = self.client.post(
self.subscriptions_url,
data=json.dumps(promo_data),
content_type='application/json',
**self.admin_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
# Should include discount information
assert 'discount' in data
assert data['promo_code'] == 'WELCOME20'

View File

@@ -0,0 +1,145 @@
"""
Contract test for GET /tenants endpoint.
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 TenantsGetContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.tenants_url = '/api/v1/tenants/'
# Admin authentication header
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer admin_token'}
def test_get_tenants_success(self):
"""Test successful retrieval of tenants list."""
response = self.client.get(
self.tenants_url,
**self.admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'tenants' in data
assert isinstance(data['tenants'], list)
# Check pagination structure
assert 'pagination' in data
pagination = data['pagination']
assert 'page' in pagination
assert 'limit' in pagination
assert 'total' in pagination
assert 'pages' in pagination
def test_get_tenants_unauthorized(self):
"""Test tenants list retrieval without authentication."""
response = self.client.get(self.tenants_url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_get_tenants_forbidden(self):
"""Test tenants list retrieval by non-admin user."""
non_admin_auth = {'HTTP_AUTHORIZATION': 'Bearer user_token'}
response = self.client.get(
self.tenants_url,
**non_admin_auth
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_get_tenants_with_pagination(self):
"""Test tenants list retrieval with pagination parameters."""
params = {
'page': 2,
'limit': 10
}
response = self.client.get(
self.tenants_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data['pagination']['page'] == 2
assert data['pagination']['limit'] == 10
def test_get_tenants_with_search(self):
"""Test tenants list retrieval with search parameter."""
params = {
'search': 'test'
}
response = self.client.get(
self.tenants_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned tenants should match search criteria
for tenant in data['tenants']:
assert 'test' in tenant['name'].lower()
def test_get_tenants_filter_by_status(self):
"""Test tenants list retrieval filtered by status."""
params = {
'status': 'ACTIVE'
}
response = self.client.get(
self.tenants_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned tenants should have the specified status
for tenant in data['tenants']:
assert tenant['status'] == 'ACTIVE'
def test_get_tenants_data_structure(self):
"""Test that tenant data structure matches the contract."""
response = self.client.get(
self.tenants_url,
**self.admin_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['tenants']) > 0:
tenant = response.json()['tenants'][0]
# Required fields according to contract
required_fields = [
'id', 'name', 'slug', 'email', 'phone', 'business_type',
'subscription_plan', 'pricing_model', 'status', 'created_at'
]
for field in required_fields:
assert field in tenant
# Field types
assert isinstance(tenant['id'], str)
assert isinstance(tenant['name'], str)
assert isinstance(tenant['slug'], str)
assert isinstance(tenant['email'], str)
assert tenant['business_type'] in ['RETAIL', 'HEALTHCARE', 'EDUCATION', 'LOGISTICS', 'BEAUTY']
assert tenant['subscription_plan'] in ['STARTER', 'GROWTH', 'PRO', 'ENTERPRISE']
assert tenant['pricing_model'] in ['SUBSCRIPTION', 'PERPETUAL']
assert tenant['status'] in ['PENDING', 'ACTIVE', 'SUSPENDED', 'TERMINATED']

View File

@@ -0,0 +1,182 @@
"""
Contract test for POST /tenants endpoint.
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 TenantsPostContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.tenants_url = '/api/v1/tenants/'
# Admin authentication header
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer admin_token'}
# Valid tenant data
self.tenant_data = {
'name': 'Test Business Sdn Bhd',
'email': 'business@test.com',
'phone': '+60123456789',
'address': {
'street': '123 Business Street',
'city': 'Kuala Lumpur',
'state': 'Wilayah Persekutuan',
'postal_code': '50000',
'country': 'Malaysia'
},
'business_type': 'RETAIL',
'subscription_plan': 'STARTER',
'pricing_model': 'SUBSCRIPTION'
}
def test_create_tenant_success(self):
"""Test successful tenant creation."""
response = self.client.post(
self.tenants_url,
data=json.dumps(self.tenant_data),
content_type='application/json',
**self.admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert 'id' in data
assert data['name'] == self.tenant_data['name']
assert data['email'] == self.tenant_data['email']
assert data['business_type'] == self.tenant_data['business_type']
assert data['subscription_plan'] == self.tenant_data['subscription_plan']
assert data['pricing_model'] == self.tenant_data['pricing_model']
assert data['status'] == 'PENDING' # Default status
# Should have generated slug
assert 'slug' in data
assert data['slug'] == 'test-business-sdn-bhd'
# Should have timestamps
assert 'created_at' in data
assert 'updated_at' in data
def test_create_tenant_unauthorized(self):
"""Test tenant creation without authentication."""
response = self.client.post(
self.tenants_url,
data=json.dumps(self.tenant_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_tenant_forbidden(self):
"""Test tenant creation by non-admin user."""
non_admin_auth = {'HTTP_AUTHORIZATION': 'Bearer user_token'}
response = self.client.post(
self.tenants_url,
data=json.dumps(self.tenant_data),
content_type='application/json',
**non_admin_auth
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_create_tenant_missing_required_fields(self):
"""Test tenant creation with missing required fields."""
incomplete_data = self.tenant_data.copy()
del incomplete_data['name']
response = self.client.post(
self.tenants_url,
data=json.dumps(incomplete_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
data = response.json()
assert 'name' in data.get('errors', {})
def test_create_tenant_invalid_email(self):
"""Test tenant creation with invalid email format."""
invalid_data = self.tenant_data.copy()
invalid_data['email'] = 'invalid-email'
response = self.client.post(
self.tenants_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_tenant_invalid_business_type(self):
"""Test tenant creation with invalid business type."""
invalid_data = self.tenant_data.copy()
invalid_data['business_type'] = 'INVALID_TYPE'
response = self.client.post(
self.tenants_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_tenant_duplicate_email(self):
"""Test tenant creation with duplicate email."""
# First request should succeed (if implemented)
first_response = self.client.post(
self.tenants_url,
data=json.dumps(self.tenant_data),
content_type='application/json',
**self.admin_auth
)
if first_response.status_code == status.HTTP_201_CREATED:
# Second request with same email should fail
second_response = self.client.post(
self.tenants_url,
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_create_tenant_invalid_address(self):
"""Test tenant creation with invalid address format."""
invalid_data = self.tenant_data.copy()
invalid_data['address'] = 'invalid address format'
response = self.client.post(
self.tenants_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_tenant_malformed_address(self):
"""Test tenant creation with malformed address JSON."""
invalid_data = self.tenant_data.copy()
invalid_data['address'] = {'street': '123 Street'} # Missing required fields
response = self.client.post(
self.tenants_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST

View File

@@ -0,0 +1,185 @@
"""
Contract test for GET /users endpoint.
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 UsersGetContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.users_url = '/api/v1/users/'
# Admin authentication header
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer admin_token'}
# Regular user authentication header
self.user_auth = {'HTTP_AUTHORIZATION': 'Bearer user_token'}
def test_get_users_success_admin(self):
"""Test successful retrieval of users list by admin."""
response = self.client.get(
self.users_url,
**self.admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'users' in data
assert isinstance(data['users'], list)
# Check pagination structure
assert 'pagination' in data
pagination = data['pagination']
assert 'page' in pagination
assert 'limit' in pagination
assert 'total' in pagination
assert 'pages' in pagination
def test_get_users_success_tenant_admin(self):
"""Test successful retrieval of users list by tenant admin."""
response = self.client.get(
self.users_url,
**self.user_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert 'users' in data
assert isinstance(data['users'], list)
# Tenant admin should only see users from their tenant
# This will be validated once implementation exists
def test_get_users_unauthorized(self):
"""Test users list retrieval without authentication."""
response = self.client.get(self.users_url)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_get_users_with_pagination(self):
"""Test users list retrieval with pagination parameters."""
params = {
'page': 2,
'limit': 10
}
response = self.client.get(
self.users_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data['pagination']['page'] == 2
assert data['pagination']['limit'] == 10
def test_get_users_with_search(self):
"""Test users list retrieval with search parameter."""
params = {
'search': 'john'
}
response = self.client.get(
self.users_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned users should match search criteria
for user in data['users']:
assert 'john' in user['name'].lower() or 'john' in user['email'].lower()
def test_get_users_filter_by_role(self):
"""Test users list retrieval filtered by role."""
params = {
'role': 'TENANT_ADMIN'
}
response = self.client.get(
self.users_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned users should have the specified role
for user in data['users']:
assert user['role'] == 'TENANT_ADMIN'
def test_get_users_filter_by_status(self):
"""Test users list retrieval filtered by status."""
params = {
'status': 'ACTIVE'
}
response = self.client.get(
self.users_url,
data=params,
**self.admin_auth
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# All returned users should have the specified status
for user in data['users']:
assert user['status'] == 'ACTIVE'
def test_get_users_tenant_isolation(self):
"""Test that tenant admin can only see users from their tenant."""
# This test verifies tenant isolation for user data
response = self.client.get(
self.users_url,
**self.user_auth
)
if response.status_code == status.HTTP_200_OK:
data = response.json()
# For tenant users, all returned users should belong to their tenant
# This will be validated once implementation exists
pass
def test_get_users_data_structure(self):
"""Test that user data structure matches the contract."""
response = self.client.get(
self.users_url,
**self.admin_auth
)
if response.status_code == status.HTTP_200_OK and len(response.json()['users']) > 0:
user = response.json()['users'][0]
# Required fields according to contract
required_fields = [
'id', 'email', 'name', 'role', 'status',
'tenant_id', 'created_at', 'last_login'
]
for field in required_fields:
assert field in user
# Field types and enums
assert isinstance(user['id'], str)
assert isinstance(user['email'], str)
assert isinstance(user['name'], str)
assert user['role'] in ['SUPER_ADMIN', 'TENANT_ADMIN', 'MANAGER', 'STAFF', 'VIEWER']
assert user['status'] in ['ACTIVE', 'INACTIVE', 'PENDING', 'SUSPENDED']
assert isinstance(user['tenant_id'], str)

View File

@@ -0,0 +1,251 @@
"""
Contract test for POST /users endpoint.
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 UsersPostContractTest(TestCase):
def setUp(self):
self.client = APIClient()
self.users_url = '/api/v1/users/'
# Admin authentication header
self.admin_auth = {'HTTP_AUTHORIZATION': 'Bearer admin_token'}
# Tenant admin authentication header
self.tenant_admin_auth = {'HTTP_AUTHORIZATION': 'Bearer tenant_admin_token'}
# Valid user data
self.user_data = {
'email': 'john.doe@example.com',
'name': 'John Doe',
'password': 'SecurePassword123!',
'role': 'STAFF',
'department': 'Operations',
'phone': '+60123456789',
'profile': {
'position': 'Manager',
'skills': ['leadership', 'operations'],
'experience_years': 5
}
}
def test_create_user_success_admin(self):
"""Test successful user creation by super admin."""
response = self.client.post(
self.users_url,
data=json.dumps(self.user_data),
content_type='application/json',
**self.admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert 'id' in data
assert data['email'] == self.user_data['email']
assert data['name'] == self.user_data['name']
assert data['role'] == self.user_data['role']
assert data['status'] == 'PENDING' # Default status for new users
assert data['department'] == self.user_data['department']
assert data['phone'] == self.user_data['phone']
# Should have generated tenant_id from context
assert 'tenant_id' in data
# Should have timestamps
assert 'created_at' in data
assert 'updated_at' in data
# Password should not be returned
assert 'password' not in data
def test_create_user_success_tenant_admin(self):
"""Test successful user creation by tenant admin."""
response = self.client.post(
self.users_url,
data=json.dumps(self.user_data),
content_type='application/json',
**self.tenant_admin_auth
)
# This should fail before implementation
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert 'id' in data
assert data['email'] == self.user_data['email']
assert data['name'] == self.user_data['name']
# Tenant admin cannot create SUPER_ADMIN users
# This will be validated once implementation exists
def test_create_user_unauthorized(self):
"""Test user creation without authentication."""
response = self.client.post(
self.users_url,
data=json.dumps(self.user_data),
content_type='application/json'
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_create_user_forbidden(self):
"""Test user creation by regular user (no permissions)."""
user_auth = {'HTTP_AUTHORIZATION': 'Bearer user_token'}
response = self.client.post(
self.users_url,
data=json.dumps(self.user_data),
content_type='application/json',
**user_auth
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_create_user_missing_required_fields(self):
"""Test user creation with missing required fields."""
incomplete_data = self.user_data.copy()
del incomplete_data['email']
response = self.client.post(
self.users_url,
data=json.dumps(incomplete_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
data = response.json()
assert 'email' in data.get('errors', {})
def test_create_user_invalid_email(self):
"""Test user creation with invalid email format."""
invalid_data = self.user_data.copy()
invalid_data['email'] = 'invalid-email'
response = self.client.post(
self.users_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_user_weak_password(self):
"""Test user creation with weak password."""
invalid_data = self.user_data.copy()
invalid_data['password'] = '123'
response = self.client.post(
self.users_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_user_invalid_role(self):
"""Test user creation with invalid role."""
invalid_data = self.user_data.copy()
invalid_data['role'] = 'INVALID_ROLE'
response = self.client.post(
self.users_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_user_duplicate_email(self):
"""Test user creation with duplicate email."""
# First request should succeed (if implemented)
first_response = self.client.post(
self.users_url,
data=json.dumps(self.user_data),
content_type='application/json',
**self.admin_auth
)
if first_response.status_code == status.HTTP_201_CREATED:
# Second request with same email should fail
second_response = self.client.post(
self.users_url,
data=json.dumps(self.user_data),
content_type='application/json',
**self.admin_auth
)
assert second_response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_user_tenant_admin_cannot_create_super_admin(self):
"""Test that tenant admin cannot create super admin users."""
super_admin_data = self.user_data.copy()
super_admin_data['role'] = 'SUPER_ADMIN'
response = self.client.post(
self.users_url,
data=json.dumps(super_admin_data),
content_type='application/json',
**self.tenant_admin_auth
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_create_user_with_profile_data(self):
"""Test user creation with profile information."""
response = self.client.post(
self.users_url,
data=json.dumps(self.user_data),
content_type='application/json',
**self.admin_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
assert 'profile' in data
profile = data['profile']
assert profile['position'] == self.user_data['profile']['position']
assert profile['skills'] == self.user_data['profile']['skills']
assert profile['experience_years'] == self.user_data['profile']['experience_years']
def test_create_user_malformed_profile(self):
"""Test user creation with malformed profile JSON."""
invalid_data = self.user_data.copy()
invalid_data['profile'] = 'invalid profile format'
response = self.client.post(
self.users_url,
data=json.dumps(invalid_data),
content_type='application/json',
**self.admin_auth
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_user_tenant_isolation(self):
"""Test that user creation respects tenant isolation."""
response = self.client.post(
self.users_url,
data=json.dumps(self.user_data),
content_type='application/json',
**self.tenant_admin_auth
)
if response.status_code == status.HTTP_201_CREATED:
data = response.json()
# User should be created in the tenant admin's tenant
# This will be validated once implementation exists
assert 'tenant_id' in data

View 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

View 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

View 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

View 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)

View 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'

View 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

View File

View File

@@ -0,0 +1,846 @@
"""
Load Testing for Multi-Tenant Scenarios
Comprehensive load testing for:
- Concurrent tenant operations
- Database connection pooling under load
- Schema isolation performance
- Resource usage optimization
- Scalability testing
Author: Claude
"""
import pytest
import time
import threading
import statistics
import queue
import random
from datetime import datetime, timedelta
from decimal import Decimal
from django.test import TestCase
from django.db import connection, connections, transaction
from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.conf import settings
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.core.models.subscription import Subscription
from backend.src.modules.retail.models.product import Product
from backend.src.modules.healthcare.models.patient import Patient
from backend.src.modules.education.models.student import Student
from backend.src.modules.logistics.models.shipment import Shipment
from backend.src.modules.beauty.models.client import Client
User = get_user_model()
class MultiTenantLoadTest(TestCase):
"""Load testing for multi-tenant scenarios"""
def setUp(self):
"""Set up test environment for load testing"""
# Create base tenants for load testing
self.tenants = []
for i in range(20):
tenant = Tenant.objects.create(
name=f'Load Test Tenant {i:03d}',
schema_name=f'load_test_{i:03d}',
domain=f'loadtest{i:03d}.com',
business_type=random.choice(['retail', 'healthcare', 'education', 'logistics', 'beauty']),
registration_number=f'202401{i:06d}',
tax_id=f'MY123456{i:04d}',
contact_email=f'contact{i:03d}@loadtest.com',
contact_phone=f'+6012345{i:04d}',
address=f'{i+1} Load Test Street',
city='Kuala Lumpur',
state='KUL',
postal_code='50000'
)
self.tenants.append(tenant)
# Create users for each tenant
self.users = []
for tenant in self.tenants:
for i in range(5): # 5 users per tenant
user = User.objects.create_user(
username=f'user_{tenant.schema_name}_{i}',
email=f'user{i}@{tenant.domain}',
password='test123',
tenant=tenant,
role=random.choice(['admin', 'staff', 'user']),
first_name=f'User{i}',
last_name=f'From {tenant.name}'
)
self.users.append(user)
# Create subscriptions for tenants
self.subscriptions = []
for tenant in self.tenants:
subscription = Subscription.objects.create(
tenant=tenant,
plan=random.choice(['basic', 'premium', 'enterprise']),
status='active',
start_date=datetime.now().date(),
end_date=datetime.now().date() + timedelta(days=30),
amount=Decimal(random.choice([99.00, 299.00, 999.00])),
currency='MYR',
billing_cycle='monthly',
auto_renew=True
)
self.subscriptions.append(subscription)
# Create test data for different modules
self.create_test_data()
def create_test_data(self):
"""Create test data for different modules"""
# Products for retail tenants
self.products = []
retail_tenants = [t for t in self.tenants if t.business_type == 'retail']
for tenant in retail_tenants:
for i in range(50):
product = Product.objects.create(
tenant=tenant,
sku=f'{tenant.schema_name}_PRD_{i:04d}',
name=f'Product {i} for {tenant.name}',
description=f'Description for product {i}',
category=random.choice(['electronics', 'clothing', 'food', 'books']),
brand='Test Brand',
barcode=f'123456789{i:04d}',
unit='piece',
current_stock=random.randint(10, 1000),
minimum_stock=10,
maximum_stock=1000,
purchase_price=Decimal(random.uniform(10, 100)),
selling_price=Decimal(random.uniform(20, 200)),
tax_rate=6.0,
is_active=True
)
self.products.append(product)
# Patients for healthcare tenants
self.patients = []
healthcare_tenants = [t for t in self.tenants if t.business_type == 'healthcare']
for tenant in healthcare_tenants:
for i in range(30):
patient = Patient.objects.create(
tenant=tenant,
patient_id=f'{tenant.schema_name}_PAT_{i:04d}',
first_name=f'Patient{i}',
last_name=f'Test{i}',
ic_number=f'{random.randint(500101, 991231):02d}-{random.randint(10, 99):02d}-{random.randint(1000, 9999):04d}',
gender=random.choice(['male', 'female']),
date_of_birth=datetime.now() - timedelta(days=random.randint(365*18, 365*70)),
blood_type=random.choice(['A+', 'A-', 'B+', 'B-', 'O+', 'O-', 'AB+', 'AB-']),
email=f'patient{i}@{tenant.domain}',
phone=f'+6012345{i:04d}',
address=f'{i+1} Patient Street',
city='Kuala Lumpur',
state='KUL',
postal_code='50000',
is_active=True
)
self.patients.append(patient)
# Students for education tenants
self.students = []
education_tenants = [t for t in self.tenants if t.business_type == 'education']
for tenant in education_tenants:
for i in range(100):
student = Student.objects.create(
tenant=tenant,
student_id=f'{tenant.schema_name}_STU_{i:04d}',
first_name=f'Student{i}',
last_name=f'Test{i}',
ic_number=f'{random.randint(500101, 991231):02d}-{random.randint(10, 99):02d}-{random.randint(1000, 9999):04d}',
gender=random.choice(['male', 'female']),
date_of_birth=datetime.now() - timedelta(days=random.randint(365*6, 365*18)),
email=f'student{i}@{tenant.domain}',
phone=f'+6012345{i:04d}',
current_grade=random.choice(['Form 1', 'Form 2', 'Form 3', 'Form 4', 'Form 5']),
stream=random.choice(['science', 'arts', 'commerce']),
admission_date=datetime.now() - timedelta(days=random.randint(30, 365)),
status='active',
is_active=True
)
self.students.append(student)
# Shipments for logistics tenants
self.shipments = []
logistics_tenants = [t for t in self.tenants if t.business_type == 'logistics']
for tenant in logistics_tenants:
for i in range(25):
shipment = Shipment.objects.create(
tenant=tenant,
tracking_number=f'{tenant.schema_name}_TRK_{i:04d}',
order_number=f'ORD_{i:06d}',
sender_name=f'Sender {i}',
receiver_name=f'Receiver {i}',
sender_phone=f'+6012345{i:04d}',
receiver_phone=f'+6012345{i:04d}',
origin_state=random.choice(['KUL', 'PNG', 'JHR', 'KDH']),
destination_state=random.choice(['KUL', 'PNG', 'JHR', 'KDH']),
service_type=random.choice(['express', 'standard', 'economy']),
package_type=random.choice(['document', 'parcel', 'freight']),
weight=Decimal(random.uniform(0.5, 50)),
length=Decimal(random.uniform(10, 100)),
width=Decimal(random.uniform(10, 100)),
height=Decimal(random.uniform(10, 100)),
shipping_cost=Decimal(random.uniform(5, 200)),
status=random.choice(['processing', 'in_transit', 'delivered']),
priority=random.choice(['normal', 'urgent'])
)
self.shipments.append(shipment)
# Clients for beauty tenants
self.clients = []
beauty_tenants = [t for t in self.tenants if t.business_type == 'beauty']
for tenant in beauty_tenants:
for i in range(40):
client = Client.objects.create(
tenant=tenant,
client_number=f'{tenant.schema_name}_CLI_{i:04d}',
first_name=f'Client{i}',
last_name=f'Test{i}',
ic_number=f'{random.randint(500101, 991231):02d}-{random.randint(10, 99):02d}-{random.randint(1000, 9999):04d}',
gender=random.choice(['male', 'female']),
date_of_birth=datetime.now() - timedelta(days=random.randint(365*18, 365*70)),
email=f'client{i}@{tenant.domain}',
phone=f'+6012345{i:04d}',
membership_tier=random.choice(['basic', 'silver', 'gold', 'platinum']),
loyalty_points=random.randint(0, 1000),
total_spent=Decimal(random.uniform(0, 10000)),
visit_count=random.randint(0, 50),
is_active=True
)
self.clients.append(client)
def test_concurrent_tenant_operations(self):
"""Test concurrent operations across multiple tenants"""
results = queue.Queue()
errors = queue.Queue()
def tenant_worker(tenant_id, worker_id):
"""Worker function for tenant operations"""
start_time = time.time()
operations_completed = 0
try:
tenant = self.tenants[tenant_id]
# Perform various operations
for i in range(20): # 20 operations per worker
operation_type = random.choice(['read', 'write', 'update'])
if operation_type == 'read':
# Read operations
users = User.objects.filter(tenant=tenant)
subscription = Subscription.objects.filter(tenant=tenant).first()
operations_completed += 2
elif operation_type == 'write':
# Write operations (create new records)
if tenant.business_type == 'retail':
Product.objects.create(
tenant=tenant,
sku=f'LOAD_{worker_id}_{i:04d}',
name=f'Load Test Product {worker_id}-{i}',
category='electronics',
unit='piece',
current_stock=100,
minimum_stock=10,
purchase_price=Decimal('50.00'),
selling_price=Decimal('100.00'),
tax_rate=6.0,
is_active=True
)
elif tenant.business_type == 'healthcare':
Patient.objects.create(
tenant=tenant,
patient_id=f'LOAD_{worker_id}_{i:04d}',
first_name=f'Load Patient {worker_id}-{i}',
ic_number=f'{random.randint(500101, 991231):02d}-{random.randint(10, 99):02d}-{random.randint(1000, 9999):04d}',
gender='male',
date_of_birth=datetime.now() - timedelta(days=365*30),
email=f'load{worker_id}-{i}@{tenant.domain}',
phone=f'+6012345{worker_id:02d}{i:02d}',
is_active=True
)
operations_completed += 1
elif operation_type == 'update':
# Update operations
tenant.name = f'Updated Tenant {tenant_id} at {time.time()}'
tenant.save()
# Update user data
users = User.objects.filter(tenant=tenant)
for user in users[:5]: # Update first 5 users
user.last_login = datetime.now()
user.save()
operations_completed += len(users[:5]) + 1
# Small delay to simulate real usage
time.sleep(0.01)
end_time = time.time()
results.put({
'worker_id': worker_id,
'tenant_id': tenant_id,
'operations_completed': operations_completed,
'time_taken': end_time - start_time,
'success': True
})
except Exception as e:
errors.put({
'worker_id': worker_id,
'tenant_id': tenant_id,
'error': str(e),
'time_taken': time.time() - start_time,
'success': False
})
# Start concurrent workers
start_time = time.time()
threads = []
# Create workers for different tenants (concurrency level)
concurrency_level = 15
for i in range(concurrency_level):
tenant_id = i % len(self.tenants)
thread = threading.Thread(
target=tenant_worker,
args=(tenant_id, i)
)
threads.append(thread)
# Start all threads
for thread in threads:
thread.start()
# Wait for all threads to complete
for thread in threads:
thread.join()
total_time = time.time() - start_time
# Collect results
successful_operations = []
while not results.empty():
successful_operations.append(results.get())
failed_operations = []
while not errors.empty():
failed_operations.append(errors.get())
# Analyze results
total_operations = sum(op['operations_completed'] for op in successful_operations)
operations_per_second = total_operations / total_time
success_rate = len(successful_operations) / (len(successful_operations) + len(failed_operations)) * 100
# Performance assertions
self.assertGreaterEqual(success_rate, 95.0,
"Success rate should be at least 95% for concurrent operations")
self.assertGreater(operations_per_second, 10,
"Should handle at least 10 operations per second")
# Log performance metrics
print(f"\nConcurrent Tenant Operations Results:")
print(f"Total time: {total_time:.2f}s")
print(f"Total operations: {total_operations}")
print(f"Operations per second: {operations_per_second:.1f}")
print(f"Success rate: {success_rate:.1f}%")
print(f"Successful workers: {len(successful_operations)}")
print(f"Failed workers: {len(failed_operations)}")
if failed_operations:
print(f"\nFailed operations:")
for failure in failed_operations:
print(f" Worker {failure['worker_id']}: {failure['error']}")
def test_database_connection_pooling_under_load(self):
"""Test database connection pooling under heavy load"""
connection_metrics = []
def connection_test_worker(worker_id, operations):
"""Worker to test database connections"""
worker_metrics = {
'worker_id': worker_id,
'connections': [],
'success_count': 0,
'error_count': 0
}
for i in range(operations):
start_time = time.time()
try:
with connection.cursor() as cursor:
# Execute query with tenant isolation
tenant = self.tenants[worker_id % len(self.tenants)]
cursor.execute(f'SET search_path TO "{tenant.schema_name}", public;')
cursor.execute("SELECT COUNT(*) FROM auth_user;")
count = cursor.fetchone()[0]
connection_time = time.time() - start_time
worker_metrics['connections'].append(connection_time)
worker_metrics['success_count'] += 1
# Small delay to simulate real usage
time.sleep(0.001)
except Exception as e:
worker_metrics['error_count'] += 1
connection_time = time.time() - start_time
worker_metrics['connections'].append(connection_time)
return worker_metrics
# Test with different load levels
load_levels = [10, 25, 50, 100]
for load_level in load_levels:
print(f"\nTesting connection pooling with {load_level} concurrent connections:")
threads = []
results = queue.Queue()
# Create worker threads
for i in range(load_level):
thread = threading.Thread(
target=lambda q, wid: q.put(connection_test_worker(wid, 20)),
args=(results, i)
)
threads.append(thread)
# Start all threads
start_time = time.time()
for thread in threads:
thread.start()
# Wait for completion
for thread in threads:
thread.join()
total_time = time.time() - start_time
# Collect and analyze results
all_metrics = []
while not results.empty():
all_metrics.append(results.get())
total_connections = sum(m['success_count'] + m['error_count'] for m in all_metrics)
successful_connections = sum(m['success_count'] for m in all_metrics)
connection_times = [time for m in all_metrics for time in m['connections']]
if connection_times:
avg_connection_time = statistics.mean(connection_times)
max_connection_time = max(connection_times)
min_connection_time = min(connection_times)
connections_per_second = total_connections / total_time
success_rate = successful_connections / total_connections * 100
# Performance assertions
self.assertLess(avg_connection_time, 0.05,
f"Average connection time should be under 50ms at {load_level} connections")
self.assertLess(max_connection_time, 0.2,
f"Maximum connection time should be under 200ms at {load_level} connections")
self.assertGreaterEqual(success_rate, 98.0,
f"Success rate should be at least 98% at {load_level} connections")
print(f" Average connection time: {avg_connection_time:.3f}s")
print(f" Max connection time: {max_connection_time:.3f}s")
print(f" Connections per second: {connections_per_second:.1f}")
print(f" Success rate: {success_rate:.1f}%")
def test_schema_isolation_performance(self):
"""Test performance of schema isolation under load"""
isolation_metrics = []
def schema_isolation_worker(tenant_id, worker_id):
"""Worker to test schema isolation"""
start_time = time.time()
operations_completed = 0
try:
tenant = self.tenants[tenant_id]
# Test schema-specific operations
with connection.cursor() as cursor:
# Switch to tenant schema
cursor.execute(f'SET search_path TO "{tenant.schema_name}", public;')
# Perform operations in tenant schema
for i in range(10):
# Count users in tenant schema
cursor.execute("SELECT COUNT(*) FROM auth_user;")
user_count = cursor.fetchone()[0]
# Get tenant-specific data
if tenant.business_type == 'retail':
cursor.execute("SELECT COUNT(*) FROM core_product;")
product_count = cursor.fetchone()[0]
elif tenant.business_type == 'healthcare':
cursor.execute("SELECT COUNT(*) FROM healthcare_patient;")
patient_count = cursor.fetchone()[0]
operations_completed += 1
# Small delay
time.sleep(0.001)
end_time = time.time()
isolation_metrics.append({
'worker_id': worker_id,
'tenant_id': tenant_id,
'operations_completed': operations_completed,
'time_taken': end_time - start_time,
'success': True
})
except Exception as e:
isolation_metrics.append({
'worker_id': worker_id,
'tenant_id': tenant_id,
'error': str(e),
'time_taken': time.time() - start_time,
'success': False
})
# Test schema isolation with concurrent access
threads = []
for i in range(30): # 30 concurrent workers
tenant_id = i % len(self.tenants)
thread = threading.Thread(
target=schema_isolation_worker,
args=(tenant_id, i)
)
threads.append(thread)
start_time = time.time()
for thread in threads:
thread.start()
for thread in threads:
thread.join()
total_time = time.time() - start_time
# Analyze isolation performance
successful_ops = [m for m in isolation_metrics if m['success']]
failed_ops = [m for m in isolation_metrics if not m['success']]
total_operations = sum(op['operations_completed'] for op in successful_ops)
success_rate = len(successful_ops) / len(isolation_metrics) * 100
operations_per_second = total_operations / total_time
if successful_ops:
avg_time_per_op = statistics.mean([op['time_taken'] / op['operations_completed'] for op in successful_ops])
# Performance assertions
self.assertLess(avg_time_per_op, 0.01,
"Average time per schema operation should be under 10ms")
self.assertGreaterEqual(success_rate, 95.0,
"Schema isolation success rate should be at least 95%")
self.assertGreater(operations_per_second, 50,
"Should handle at least 50 schema operations per second")
print(f"\nSchema Isolation Performance:")
print(f"Total time: {total_time:.2f}s")
print(f"Total operations: {total_operations}")
print(f"Operations per second: {operations_per_second:.1f}")
print(f"Success rate: {success_rate:.1f}%")
if successful_ops:
print(f"Average time per operation: {avg_time_per_op:.4f}s")
def test_resource_usage_optimization(self):
"""Test resource usage optimization under multi-tenant load"""
import psutil
import os
process = psutil.Process(os.getpid())
# Monitor resource usage during load test
def resource_monitor_worker(duration, results_queue):
"""Worker to monitor resource usage"""
start_time = time.time()
memory_samples = []
cpu_samples = []
while time.time() - start_time < duration:
memory_info = process.memory_info()
cpu_percent = process.cpu_percent()
memory_samples.append(memory_info.rss / 1024 / 1024) # MB
cpu_samples.append(cpu_percent)
time.sleep(0.1) # Sample every 100ms
results_queue.put({
'memory_samples': memory_samples,
'cpu_samples': cpu_samples,
'duration': duration
})
def load_worker(worker_id, operations):
"""Load generation worker"""
for i in range(operations):
try:
# Random tenant operations
tenant = random.choice(self.tenants)
# Perform random database operations
with connection.cursor() as cursor:
cursor.execute(f'SET search_path TO "{tenant.schema_name}", public;')
cursor.execute("SELECT COUNT(*) FROM auth_user;")
# Small delay
time.sleep(0.005)
except Exception as e:
print(f"Worker {worker_id} error: {e}")
# Start resource monitoring
monitor_results = queue.Queue()
monitor_thread = threading.Thread(
target=resource_monitor_worker,
args=(10, monitor_results) # Monitor for 10 seconds
)
monitor_thread.start()
# Start load generation
start_time = time.time()
threads = []
# Create load workers
for i in range(50): # 50 concurrent workers
thread = threading.Thread(
target=load_worker,
args=(i, 100) # Each worker performs 100 operations
)
threads.append(thread)
for thread in threads:
thread.start()
for thread in threads:
thread.join()
total_time = time.time() - start_time
# Wait for monitoring to complete
monitor_thread.join()
resource_data = monitor_results.get()
# Analyze resource usage
memory_samples = resource_data['memory_samples']
cpu_samples = resource_data['cpu_samples']
avg_memory = statistics.mean(memory_samples)
max_memory = max(memory_samples)
avg_cpu = statistics.mean(cpu_samples)
max_cpu = max(cpu_samples)
total_operations = 50 * 100 # 50 workers * 100 operations each
operations_per_second = total_operations / total_time
# Performance assertions
self.assertLess(avg_memory, 1000, # 1GB
"Average memory usage should be under 1GB")
self.assertLess(max_memory, 1500, # 1.5GB
"Peak memory usage should be under 1.5GB")
self.assertLess(avg_cpu, 80, # 80%
"Average CPU usage should be under 80%")
self.assertGreater(operations_per_second, 25,
"Should handle at least 25 operations per second under load")
print(f"\nResource Usage Optimization Results:")
print(f"Total operations: {total_operations}")
print(f"Operations per second: {operations_per_second:.1f}")
print(f"Average memory usage: {avg_memory:.1f} MB")
print(f"Peak memory usage: {max_memory:.1f} MB")
print(f"Average CPU usage: {avg_cpu:.1f}%")
print(f"Peak CPU usage: {max_cpu:.1f}%")
def test_scalability_benchmark(self):
"""Test scalability with increasing load"""
scalability_results = []
# Test with different tenant counts
tenant_counts = [5, 10, 15, 20]
for tenant_count in tenant_counts:
print(f"\nTesting scalability with {tenant_count} tenants:")
# Use subset of tenants
test_tenants = self.tenants[:tenant_count]
def scalability_worker(operations):
"""Worker for scalability testing"""
for i in range(operations):
try:
tenant = random.choice(test_tenants)
# Perform tenant-specific operations
with connection.cursor() as cursor:
cursor.execute(f'SET search_path TO "{tenant.schema_name}", public;')
cursor.execute("SELECT COUNT(*) FROM auth_user;")
# Simulate processing time
time.sleep(0.01)
except Exception as e:
continue
# Run test with increasing concurrency
concurrency_levels = [5, 10, 20]
for concurrency in concurrency_levels:
start_time = time.time()
threads = []
# Create worker threads
for i in range(concurrency):
thread = threading.Thread(
target=scalability_worker,
args=(20,) # 20 operations per worker
)
threads.append(thread)
# Start and wait for completion
for thread in threads:
thread.start()
for thread in threads:
thread.join()
total_time = time.time() - start_time
total_operations = concurrency * 20
operations_per_second = total_operations / total_time
scalability_results.append({
'tenant_count': tenant_count,
'concurrency': concurrency,
'total_time': total_time,
'operations_per_second': operations_per_second
})
print(f" Concurrency {concurrency}: {operations_per_second:.1f} ops/sec")
# Analyze scalability
print(f"\nScalability Analysis:")
for result in scalability_results:
throughput = result['operations_per_second']
tenant_count = result['tenant_count']
concurrency = result['concurrency']
# Calculate throughput per tenant
throughput_per_tenant = throughput / tenant_count
print(f" {tenant_count} tenants, {concurrency} concurrent: "
f"{throughput:.1f} ops/sec ({throughput_per_tenant:.1f} per tenant)")
# Performance assertions for scalability
# Throughput should not decrease significantly with more tenants
baseline_throughput = scalability_results[0]['operations_per_second']
max_throughput = max(r['operations_per_second'] for r in scalability_results)
self.assertGreater(max_throughput, baseline_throughput * 0.5,
"Throughput should not degrade by more than 50% under load")
def test_multi_tenant_transaction_performance(self):
"""Test transaction performance across multiple tenants"""
transaction_metrics = []
def transaction_worker(tenant_id, worker_id):
"""Worker for transaction testing"""
start_time = time.time()
try:
tenant = self.tenants[tenant_id]
# Perform transactions in tenant schema
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute(f'SET search_path TO "{tenant.schema_name}", public;')
# Create multiple records in a transaction
for i in range(5):
cursor.execute(
"INSERT INTO auth_user (username, email, password, tenant_id, is_active) "
"VALUES (%s, %s, %s, %s, %s) RETURNING id;",
[f'tx_user_{worker_id}_{i}', f'user{i}@{tenant.domain}', 'hash', tenant.id, True]
)
# Update tenant stats
cursor.execute(
"UPDATE core_tenant SET name = %s WHERE id = %s;",
[f'Updated at {time.time()}', tenant.id]
)
end_time = time.time()
transaction_metrics.append({
'worker_id': worker_id,
'tenant_id': tenant_id,
'time_taken': end_time - start_time,
'success': True
})
except Exception as e:
transaction_metrics.append({
'worker_id': worker_id,
'tenant_id': tenant_id,
'error': str(e),
'time_taken': time.time() - start_time,
'success': False
})
# Test concurrent transactions
threads = []
for i in range(40): # 40 concurrent transaction workers
tenant_id = i % len(self.tenants)
thread = threading.Thread(
target=transaction_worker,
args=(tenant_id, i)
)
threads.append(thread)
start_time = time.time()
for thread in threads:
thread.start()
for thread in threads:
thread.join()
total_time = time.time() - start_time
# Analyze transaction performance
successful_tx = [m for m in transaction_metrics if m['success']]
failed_tx = [m for m in transaction_metrics if not m['success']]
success_rate = len(successful_tx) / len(transaction_metrics) * 100
if successful_tx:
avg_tx_time = statistics.mean([tx['time_taken'] for tx in successful_tx])
transactions_per_second = len(successful_tx) / total_time
# Performance assertions
self.assertLess(avg_tx_time, 0.1,
"Average transaction time should be under 100ms")
self.assertGreaterEqual(success_rate, 95.0,
"Transaction success rate should be at least 95%")
self.assertGreater(transactions_per_second, 20,
"Should handle at least 20 transactions per second")
print(f"\nMulti-Tenant Transaction Performance:")
print(f"Total time: {total_time:.2f}s")
print(f"Total transactions: {len(successful_tx)}")
print(f"Transactions per second: {len(successful_tx) / total_time:.1f}")
print(f"Success rate: {success_rate:.1f}%")
if successful_tx:
print(f"Average transaction time: {avg_tx_time:.3f}s")

View File

View File

@@ -0,0 +1,441 @@
"""
Performance Tests for API Endpoints
Tests for API performance optimization:
- Response time optimization
- Concurrency handling
- Rate limiting efficiency
- Caching strategies
- Payload size optimization
Author: Claude
"""
import pytest
import time
import statistics
import threading
import requests
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.conf import settings
from decimal import Decimal
from datetime import date
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.modules.retail.models.product import Product
User = get_user_model()
class APIPerformanceTest(TestCase):
"""Test cases for API performance optimization"""
def setUp(self):
self.client = Client()
# Create test tenant and user
self.tenant = Tenant.objects.create(
name='API Performance Test',
schema_name='api_perf_test',
domain='apiperf.com',
business_type='retail'
)
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='test123',
tenant=self.tenant,
role='admin'
)
# Create test data
self.products = []
for i in range(100):
product = Product.objects.create(
tenant=self.tenant,
sku=f'API-TEST-{i:06d}',
name=f'API Test Product {i}',
description=f'Description for API test product {i}',
category='electronics',
brand='Test Brand',
barcode=f'123456789{i:04d}',
unit='piece',
current_stock=100 + i,
minimum_stock=10,
maximum_stock=500,
purchase_price=Decimal('50.00') + (i * 0.1),
selling_price=Decimal('100.00') + (i * 0.2),
tax_rate=10.0,
is_active=True
)
self.products.append(product)
def test_api_response_time_optimization(self):
"""Test API response time optimization"""
# Test various API endpoints
endpoints = [
('api:tenant-list', 'GET', {}),
('api:user-list', 'GET', {}),
('api:product-list', 'GET', {}),
('api:tenant-detail', 'GET', {'pk': self.tenant.id}),
('api:user-detail', 'GET', {'pk': self.user.id}),
('api:product-detail', 'GET', {'pk': self.products[0].id}),
]
response_times = {}
for endpoint_name, method, params in endpoints:
times = []
# Warm up cache
for _ in range(3):
if method == 'GET':
self.client.get(reverse(endpoint_name, kwargs=params))
elif method == 'POST':
self.client.post(reverse(endpoint_name, kwargs=params))
# Measure response times
for _ in range(10):
start_time = time.time()
if method == 'GET':
response = self.client.get(reverse(endpoint_name, kwargs=params))
elif method == 'POST':
response = self.client.post(reverse(endpoint_name, kwargs=params))
response_time = time.time() - start_time
times.append(response_time)
# Verify response is successful
self.assertEqual(response.status_code, 200)
avg_time = statistics.mean(times)
max_time = max(times)
min_time = min(times)
response_times[endpoint_name] = {
'avg': avg_time,
'max': max_time,
'min': min_time,
'times': times
}
# Performance assertions
self.assertLess(avg_time, 0.5, f"Average response time for {endpoint_name} should be under 500ms")
self.assertLess(max_time, 1.0, f"Maximum response time for {endpoint_name} should be under 1s")
# Log performance metrics
print(f"\nAPI Response Time Performance:")
for endpoint, metrics in response_times.items():
print(f"{endpoint}: avg={metrics['avg']:.3f}s, max={metrics['max']:.3f}s, min={metrics['min']:.3f}s")
def test_concurrent_request_handling(self):
"""Test concurrent request handling"""
def make_request(request_id, results):
start_time = time.time()
try:
response = self.client.get(reverse('api:product-list'))
response_time = time.time() - start_time
results.append({
'request_id': request_id,
'success': response.status_code == 200,
'response_time': response_time,
'status_code': response.status_code
})
except Exception as e:
results.append({
'request_id': request_id,
'success': False,
'error': str(e),
'response_time': time.time() - start_time
})
# Test with different concurrency levels
concurrency_levels = [10, 25, 50]
for concurrency in concurrency_levels:
results = []
threads = []
# Create concurrent requests
for i in range(concurrency):
thread = threading.Thread(
target=make_request,
args=(i, results)
)
threads.append(thread)
# Start all threads
start_time = time.time()
for thread in threads:
thread.start()
# Wait for all threads to complete
for thread in threads:
thread.join()
total_time = time.time() - start_time
# Analyze results
successful_requests = [r for r in results if r['success']]
failed_requests = [r for r in results if not r['success']]
success_rate = len(successful_requests) / len(results) * 100
avg_response_time = statistics.mean([r['response_time'] for r in results])
# Performance assertions
self.assertGreaterEqual(success_rate, 95.0,
f"Success rate should be at least 95% for {concurrency} concurrent requests")
self.assertLess(total_time, 5.0,
f"Total time for {concurrency} concurrent requests should be under 5s")
print(f"\nConcurrency Test ({concurrency} requests):")
print(f"Success rate: {success_rate:.1f}%")
print(f"Total time: {total_time:.3f}s")
print(f"Average response time: {avg_response_time:.3f}s")
print(f"Failed requests: {len(failed_requests)}")
def test_rate_limiting_efficiency(self):
"""Test rate limiting efficiency"""
# This test assumes rate limiting is implemented
# Make rapid requests to test rate limiting
request_results = []
for i in range(100):
start_time = time.time()
response = self.client.get(reverse('api:product-list'))
response_time = time.time() - start_time
request_results.append({
'request_number': i,
'status_code': response.status_code,
'response_time': response_time,
'timestamp': time.time()
})
# Analyze rate limiting effectiveness
successful_requests = [r for r in request_results if r['status_code'] == 200]
rate_limited_requests = [r for r in request_results if r['status_code'] == 429]
print(f"\nRate Limiting Test:")
print(f"Total requests: {len(request_results)}")
print(f"Successful requests: {len(successful_requests)}")
print(f"Rate limited requests: {len(rate_limited_requests)}")
# If rate limiting is implemented, some requests should be limited
if len(rate_limited_requests) > 0:
print(f"Rate limiting is working - {len(rate_limited_requests)} requests were limited")
# Response times should remain consistent even under load
response_times = [r['response_time'] for r in successful_requests]
if response_times:
avg_response_time = statistics.mean(response_times)
max_response_time = max(response_times)
self.assertLess(avg_response_time, 0.5,
"Average response time should remain under 500ms even with rate limiting")
print(f"Average response time for successful requests: {avg_response_time:.3f}s")
def test_caching_strategies(self):
"""Test caching strategies performance"""
# Clear cache before testing
cache.clear()
# Test cache hit/miss performance
endpoint = reverse('api:product-list')
# First request (cache miss)
start_time = time.time()
response1 = self.client.get(endpoint)
cache_miss_time = time.time() - start_time
# Second request (cache hit)
start_time = time.time()
response2 = self.client.get(endpoint)
cache_hit_time = time.time() - start_time
# Multiple cache hits
cache_hit_times = []
for _ in range(10):
start_time = time.time()
response = self.client.get(endpoint)
cache_hit_times.append(time.time() - start_time)
avg_cache_hit_time = statistics.mean(cache_hit_times)
# Performance assertions
self.assertLess(cache_miss_time, 1.0, "Cache miss should complete within 1s")
self.assertLess(cache_hit_time, 0.1, "Cache hit should complete within 100ms")
self.assertLess(avg_cache_hit_time, 0.05, "Average cache hit should be under 50ms")
# Cache hit should be faster than cache miss
self.assertLess(avg_cache_hit_time, cache_miss_time * 0.5,
"Cache hit should be significantly faster than cache miss")
print(f"\nCaching Strategy Performance:")
print(f"Cache miss time: {cache_miss_time:.3f}s")
print(f"First cache hit time: {cache_hit_time:.3f}s")
print(f"Average cache hit time: {avg_cache_hit_time:.3f}s")
print(f"Cache improvement: {(cache_miss_time / avg_cache_hit_time):.1f}x")
def test_payload_size_optimization(self):
"""Test payload size optimization"""
# Test different payload sizes
test_sizes = [10, 50, 100, 500]
for size in test_sizes:
# Create test data
test_products = []
for i in range(size):
test_products.append({
'sku': f'PAYLOAD-{i:06d}',
'name': f'Payload Test Product {i}',
'description': 'A' * 100, # Long description
'category': 'electronics',
'brand': 'Test Brand',
'current_stock': 100,
'purchase_price': '50.00',
'selling_price': '100.00'
})
# Test different response formats
# Full payload
start_time = time.time()
response = self.client.get(reverse('api:product-list'))
full_payload_time = time.time() - start_time
full_payload_size = len(response.content)
# Paginated payload (assuming pagination is implemented)
start_time = time.time()
response = self.client.get(reverse('api:product-list') + '?page=1&page_size=20')
paginated_time = time.time() - start_time
paginated_size = len(response.content)
# Fields-limited payload
start_time = time.time()
response = self.client.get(reverse('api:product-list') + '?fields=id,name,sku')
fields_limited_time = time.time() - start_time
fields_limited_size = len(response.content)
# Performance assertions
self.assertLess(full_payload_time, 2.0,
f"Full payload request for {size} items should complete within 2s")
self.assertLess(paginated_time, 0.5,
f"Paginated request should be faster")
self.assertLess(fields_limited_time, 0.3,
f"Fields-limited request should be fastest")
# Size assertions
self.assertLess(paginated_size, full_payload_size * 0.3,
f"Paginated payload should be much smaller for {size} items")
self.assertLess(fields_limited_size, full_payload_size * 0.2,
f"Fields-limited payload should be smallest")
print(f"\nPayload Optimization Test ({size} items):")
print(f"Full payload: {full_payload_time:.3f}s, {full_payload_size} bytes")
print(f"Paginated: {paginated_time:.3f}s, {paginated_size} bytes")
print(f"Fields limited: {fields_limited_time:.3f}s, {fields_limited_size} bytes")
def test_database_query_optimization(self):
"""Test database query optimization in API calls"""
# Test N+1 query problems
# First, test without optimization
start_time = time.time()
response = self.client.get(reverse('api:product-list'))
unoptimized_time = time.time() - start_time
# Test with select_related (assuming optimization is implemented)
start_time = time.time()
response = self.client.get(reverse('api:product-list') + '?select_related=tenant')
optimized_time = time.time() - start_time
# Test with pagination
start_time = time.time()
response = self.client.get(reverse('api:product-list') + '?page=1&page_size=10')
paginated_time = time.time() - start_time
# Performance assertions
self.assertLess(unoptimized_time, 1.0, "Unoptimized query should complete within 1s")
self.assertLess(optimized_time, unoptimized_time * 0.8,
"Optimized query should be faster")
self.assertLess(paginated_time, unoptimized_time * 0.3,
"Paginated query should be much faster")
print(f"\nDatabase Query Optimization:")
print(f"Unoptimized query: {unoptimized_time:.3f}s")
print(f"Optimized query: {optimized_time:.3f}s")
print(f"Paginated query: {paginated_time:.3f}s")
def test_memory_usage_optimization(self):
"""Test memory usage optimization"""
import psutil
import os
process = psutil.Process(os.getpid())
# Test memory usage with large datasets
initial_memory = process.memory_info().rss / 1024 / 1024 # MB
# Make multiple requests with large payloads
for i in range(10):
response = self.client.get(reverse('api:product-list'))
# Process response to simulate real usage
data = response.json()
peak_memory = process.memory_info().rss / 1024 / 1024 # MB
memory_increase = peak_memory - initial_memory
# Performance assertions
self.assertLess(memory_increase, 50,
"Memory increase should be under 50MB for large dataset processing")
print(f"\nMemory Usage Optimization:")
print(f"Initial memory: {initial_memory:.1f} MB")
print(f"Peak memory: {peak_memory:.1f} MB")
print(f"Memory increase: {memory_increase:.1f} MB")
def test_authentication_performance(self):
"""Test authentication performance"""
# Test login performance
login_data = {
'username': 'testuser',
'password': 'test123'
}
login_times = []
for _ in range(10):
start_time = time.time()
response = self.client.post(reverse('api:login'), login_data)
login_time = time.time() - start_time
login_times.append(login_time)
self.assertEqual(response.status_code, 200)
avg_login_time = statistics.mean(login_times)
# Test authenticated request performance
self.client.login(username='testuser', password='test123')
auth_request_times = []
for _ in range(10):
start_time = time.time()
response = self.client.get(reverse('api:product-list'))
auth_request_time = time.time() - start_time
auth_request_times.append(auth_request_time)
self.assertEqual(response.status_code, 200)
avg_auth_request_time = statistics.mean(auth_request_times)
# Performance assertions
self.assertLess(avg_login_time, 0.5, "Average login time should be under 500ms")
self.assertLess(avg_auth_request_time, 0.2, "Average authenticated request time should be under 200ms")
print(f"\nAuthentication Performance:")
print(f"Average login time: {avg_login_time:.3f}s")
print(f"Average authenticated request time: {avg_auth_request_time:.3f}s")

View File

@@ -0,0 +1,418 @@
"""
Performance Tests for Database Operations
Tests for database performance optimization:
- Query optimization
- Connection pooling efficiency
- Multi-tenant query performance
- Index usage validation
- Bulk operations performance
Author: Claude
"""
import pytest
import time
import statistics
from django.test import TestCase
from django.db import connection, connections, transaction
from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.conf import settings
from django.db.utils import OperationalError
from decimal import Decimal
from datetime import date, timedelta
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.modules.retail.models.product import Product
from backend.src.modules.healthcare.models.patient import Patient
from backend.src.modules.education.models.student import Student
User = get_user_model()
class DatabasePerformanceTest(TestCase):
"""Test cases for database performance optimization"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Performance Test Sdn Bhd',
schema_name='performance_test',
domain='performancetest.com',
business_type='retail'
)
def test_query_performance_with_indexes(self):
"""Test query performance with proper indexing"""
# Create test data
products = []
for i in range(1000):
products.append(Product(
tenant=self.tenant,
sku=f'PRD-{i:06d}',
name=f'Product {i}',
description=f'Description for product {i}',
category='electronics',
brand='Test Brand',
barcode=f'123456789{i:04d}',
unit='piece',
current_stock=100 + i,
minimum_stock=10,
maximum_stock=500,
purchase_price=Decimal('50.00') + (i * 0.1),
selling_price=Decimal('100.00') + (i * 0.2),
tax_rate=10.0,
is_active=True
))
# Bulk create for performance
start_time = time.time()
Product.objects.bulk_create(products)
bulk_create_time = time.time() - start_time
# Test indexed query performance
start_time = time.time()
products_by_sku = Product.objects.filter(sku__startswith='PRD-000')
indexed_query_time = time.time() - start_time
# Test non-indexed query performance (description)
start_time = time.time()
products_by_desc = Product.objects.filter(description__contains='Description for product')
non_indexed_query_time = time.time() - start_time
# Test tenant-isolated query performance
start_time = time.time()
tenant_products = Product.objects.filter(tenant=self.tenant)
tenant_query_time = time.time() - start_time
# Performance assertions
self.assertLess(bulk_create_time, 5.0, "Bulk create should complete within 5 seconds")
self.assertLess(indexed_query_time, 0.1, "Indexed query should complete within 100ms")
self.assertLess(tenant_query_time, 0.1, "Tenant query should complete within 100ms")
# Indexed query should be faster than non-indexed
self.assertLess(indexed_query_time, non_indexed_query_time * 2,
"Indexed query should be significantly faster")
# Log performance metrics
print(f"\nBulk create 1000 products: {bulk_create_time:.3f}s")
print(f"Indexed query (SKU): {indexed_query_time:.3f}s")
print(f"Non-indexed query (description): {non_indexed_query_time:.3f}s")
print(f"Tenant isolated query: {tenant_query_time:.3f}s")
def test_connection_pooling_efficiency(self):
"""Test database connection pooling efficiency"""
connection_times = []
# Test multiple rapid connections
for i in range(50):
start_time = time.time()
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
cursor.fetchone()
connection_times.append(time.time() - start_time)
# Analyze connection performance
avg_connection_time = statistics.mean(connection_times)
max_connection_time = max(connection_times)
min_connection_time = min(connection_times)
# Performance assertions
self.assertLess(avg_connection_time, 0.05,
"Average connection time should be under 50ms")
self.assertLess(max_connection_time, 0.1,
"Maximum connection time should be under 100ms")
print(f"\nConnection pooling performance:")
print(f"Average connection time: {avg_connection_time:.3f}s")
print(f"Max connection time: {max_connection_time:.3f}s")
print(f"Min connection time: {min_connection_time:.3f}s")
def test_multi_tenant_query_performance(self):
"""Test multi-tenant query performance"""
# Create multiple tenants
tenants = []
for i in range(10):
tenant = Tenant.objects.create(
name=f'Tenant {i}',
schema_name=f'tenant_{i}',
domain=f'tenant{i}.com',
business_type='retail'
)
tenants.append(tenant)
# Create products for each tenant
all_products = []
for tenant in tenants:
for i in range(100):
all_products.append(Product(
tenant=tenant,
sku=f'{tenant.schema_name}-PRD-{i:03d}',
name=f'Product {i} for {tenant.name}',
category='electronics',
unit='piece',
current_stock=100,
minimum_stock=10,
purchase_price=Decimal('50.00'),
selling_price=Decimal('100.00'),
tax_rate=10.0,
is_active=True
))
Product.objects.bulk_create(all_products)
# Test cross-tenant query performance
start_time = time.time()
all_tenant_products = Product.objects.filter(
tenant__in=tenants[:5]
).select_related('tenant')
cross_tenant_time = time.time() - start_time
# Test single tenant query performance
start_time = time.time()
single_tenant_products = Product.objects.filter(
tenant=tenants[0]
)
single_tenant_time = time.time() - start_time
# Test tenant-specific schema performance
start_time = time.time()
with connection.cursor() as cursor:
cursor.execute(f'SET search_path TO "{tenants[0].schema_name}", public;')
cursor.execute("SELECT COUNT(*) FROM core_product")
cursor.fetchone()
schema_query_time = time.time() - start_time
# Performance assertions
self.assertLess(cross_tenant_time, 0.5, "Cross-tenant query should be fast")
self.assertLess(single_tenant_time, 0.1, "Single tenant query should be fast")
self.assertLess(schema_query_time, 0.05, "Schema-specific query should be fast")
print(f"\nMulti-tenant query performance:")
print(f"Cross-tenant query: {cross_tenant_time:.3f}s")
print(f"Single tenant query: {single_tenant_time:.3f}s")
print(f"Schema-specific query: {schema_query_time:.3f}s")
def test_bulk_operations_performance(self):
"""Test bulk operations performance"""
# Test bulk create performance
products_to_create = []
for i in range(500):
products_to_create.append(Product(
tenant=self.tenant,
sku=f'BULK-{i:06d}',
name=f'Bulk Product {i}',
category='electronics',
unit='piece',
current_stock=100,
minimum_stock=10,
purchase_price=Decimal('50.00'),
selling_price=Decimal('100.00'),
tax_rate=10.0,
is_active=True
))
start_time = time.time()
Product.objects.bulk_create(products_to_create)
bulk_create_time = time.time() - start_time
# Test bulk update performance
products = Product.objects.filter(sku__startswith='BULK-')
for product in products:
product.current_stock += 10
start_time = time.time()
Product.objects.bulk_update(products, ['current_stock'])
bulk_update_time = time.time() - start_time
# Test bulk delete performance
start_time = time.time()
Product.objects.filter(sku__startswith='BULK-').delete()
bulk_delete_time = time.time() - start_time
# Performance assertions
self.assertLess(bulk_create_time, 2.0, "Bulk create 500 items should be fast")
self.assertLess(bulk_update_time, 1.0, "Bulk update 500 items should be fast")
self.assertLess(bulk_delete_time, 0.5, "Bulk delete 500 items should be fast")
print(f"\nBulk operations performance:")
print(f"Bulk create 500 items: {bulk_create_time:.3f}s")
print(f"Bulk update 500 items: {bulk_update_time:.3f}s")
print(f"Bulk delete 500 items: {bulk_delete_time:.3f}s")
def test_transaction_performance(self):
"""Test transaction performance"""
def test_transaction_operations():
with transaction.atomic():
# Create multiple records in a single transaction
for i in range(100):
Product.objects.create(
tenant=self.tenant,
sku=f'TXN-{i:06d}',
name=f'Transaction Product {i}',
category='electronics',
unit='piece',
current_stock=100,
minimum_stock=10,
purchase_price=Decimal('50.00'),
selling_price=Decimal('100.00'),
tax_rate=10.0,
is_active=True
)
# Test transaction performance
transaction_times = []
for i in range(10):
start_time = time.time()
test_transaction_operations()
transaction_times.append(time.time() - start_time)
# Clean up
Product.objects.filter(sku__startswith='TXN-').delete()
avg_transaction_time = statistics.mean(transaction_times)
max_transaction_time = max(transaction_times)
# Performance assertions
self.assertLess(avg_transaction_time, 1.0,
"Average transaction time should be under 1 second")
self.assertLess(max_transaction_time, 2.0,
"Maximum transaction time should be under 2 seconds")
print(f"\nTransaction performance:")
print(f"Average transaction time: {avg_transaction_time:.3f}s")
print(f"Max transaction time: {max_transaction_time:.3f}s")
def test_select_related_performance(self):
"""Test select_related and prefetch_related performance"""
# Create test data with relationships
products = []
for i in range(100):
products.append(Product(
tenant=self.tenant,
sku=f'REL-{i:06d}',
name=f'Related Product {i}',
category='electronics',
unit='piece',
current_stock=100,
minimum_stock=10,
purchase_price=Decimal('50.00'),
selling_price=Decimal('100.00'),
tax_rate=10.0,
is_active=True
))
Product.objects.bulk_create(products)
# Test query without select_related
start_time = time.time()
products_no_select = Product.objects.filter(tenant=self.tenant)
for product in products_no_select:
_ = product.tenant.name # This will cause additional queries
no_select_time = time.time() - start_time
# Test query with select_related
start_time = time.time()
products_with_select = Product.objects.filter(
tenant=self.tenant
).select_related('tenant')
for product in products_with_select:
_ = product.tenant.name # This should not cause additional queries
with_select_time = time.time() - start_time
# Performance assertions
self.assertLess(with_select_time, no_select_time * 0.5,
"Query with select_related should be much faster")
print(f"\nSelect_related performance:")
print(f"Without select_related: {no_select_time:.3f}s")
print(f"With select_related: {with_select_time:.3f}s")
print(f"Performance improvement: {(no_select_time / with_select_time):.1f}x")
def test_query_caching_performance(self):
"""Test query caching performance"""
# Create test data
products = []
for i in range(100):
products.append(Product(
tenant=self.tenant,
sku=f'CACHE-{i:06d}',
name=f'Cached Product {i}',
category='electronics',
unit='piece',
current_stock=100,
minimum_stock=10,
purchase_price=Decimal('50.00'),
selling_price=Decimal('100.00'),
tax_rate=10.0,
is_active=True
))
Product.objects.bulk_create(products)
# Test repeated query performance
query_times = []
for i in range(20):
start_time = time.time()
products = Product.objects.filter(tenant=self.tenant)
list(products) # Force evaluation
query_times.append(time.time() - start_time)
# Analyze caching performance
first_query_time = query_times[0]
avg_subsequent_time = statistics.mean(query_times[1:])
# Subsequent queries should be faster due to caching
self.assertLess(avg_subsequent_time, first_query_time * 0.8,
"Subsequent queries should benefit from caching")
print(f"\nQuery caching performance:")
print(f"First query time: {first_query_time:.3f}s")
print(f"Average subsequent query time: {avg_subsequent_time:.3f}s")
print(f"Caching improvement: {(first_query_time / avg_subsequent_time):.1f}x")
def test_database_connection_health(self):
"""Test database connection health and reliability"""
health_results = []
# Test connection health over multiple attempts
for i in range(10):
start_time = time.time()
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
result = cursor.fetchone()
health_results.append({
'success': True,
'time': time.time() - start_time,
'result': result
})
except OperationalError as e:
health_results.append({
'success': False,
'time': time.time() - start_time,
'error': str(e)
})
# Analyze connection health
successful_connections = [r for r in health_results if r['success']]
failed_connections = [r for r in health_results if not r['success']]
# All connections should succeed
self.assertEqual(len(failed_connections), 0,
"All database connections should succeed")
# Connection times should be consistent
connection_times = [r['time'] for r in successful_connections]
avg_time = statistics.mean(connection_times)
max_time = max(connection_times)
self.assertLess(avg_time, 0.05, "Average connection time should be under 50ms")
self.assertLess(max_time, 0.1, "Maximum connection time should be under 100ms")
print(f"\nDatabase connection health:")
print(f"Successful connections: {len(successful_connections)}/10")
print(f"Failed connections: {len(failed_connections)}/10")
print(f"Average connection time: {avg_time:.3f}s")
print(f"Maximum connection time: {max_time:.3f}s")

View File

@@ -0,0 +1,481 @@
"""
Performance Tests for Frontend Components
Tests for frontend performance optimization:
- Component rendering performance
- State management efficiency
- API call optimization
- Memory usage optimization
- Loading performance
Author: Claude
"""
import pytest
import time
import statistics
import js2py
from django.test import TestCase
# Mock React performance testing utilities
class MockPerformance:
def __init__(self):
self.metrics = {}
def mark(self, name):
self.metrics[name] = time.time()
def measure(self, name, callback):
start_time = time.time()
result = callback()
end_time = time.time()
duration = end_time - start_time
self.metrics[name] = duration
return result, duration
def get_metric(self, name):
return self.metrics.get(name, 0)
def clear_metrics(self):
self.metrics.clear()
class FrontendPerformanceTest(TestCase):
"""Test cases for frontend performance optimization"""
def setUp(self):
self.performance = MockPerformance()
def test_component_rendering_performance(self):
"""Test component rendering performance"""
# Mock component rendering test
def render_component(component_name, props):
"""Mock component rendering function"""
start_time = time.time()
# Simulate different component complexities
if component_name == 'simple':
# Simple component - minimal logic
time.sleep(0.001) # 1ms
elif component_name == 'complex':
# Complex component - data processing, multiple children
time.sleep(0.01) # 10ms
elif component_name == 'data_heavy':
# Data-heavy component - large datasets
time.sleep(0.05) # 50ms
elif component_name == 'optimized':
# Optimized component - memoized, virtualized
time.sleep(0.002) # 2ms
end_time = time.time()
return end_time - start_time
# Test different component types
components = ['simple', 'complex', 'data_heavy', 'optimized']
render_times = {}
for component in components:
times = []
for _ in range(20): # Multiple renders for consistency
render_time = render_component(component, {})
times.append(render_time)
avg_time = statistics.mean(times)
max_time = max(times)
min_time = min(times)
render_times[component] = {
'avg': avg_time,
'max': max_time,
'min': min_time,
'times': times
}
# Performance assertions
self.assertLess(render_times['simple']['avg'], 0.005,
"Simple component should render in under 5ms")
self.assertLess(render_times['complex']['avg'], 0.02,
"Complex component should render in under 20ms")
self.assertLess(render_times['data_heavy']['avg'], 0.1,
"Data-heavy component should render in under 100ms")
self.assertLess(render_times['optimized']['avg'], 0.01,
"Optimized component should render in under 10ms")
# Optimized should be faster than data-heavy
self.assertLess(render_times['optimized']['avg'],
render_times['data_heavy']['avg'] * 0.1,
"Optimized component should be much faster than data-heavy")
print(f"\nComponent Rendering Performance:")
for component, metrics in render_times.items():
print(f"{component}: avg={metrics['avg']:.3f}s, max={metrics['max']:.3f}s, min={metrics['min']:.3f}s")
def test_state_management_performance(self):
"""Test state management performance"""
def test_state_operations(operation_type, iterations=1000):
"""Test different state management operations"""
start_time = time.time()
# Mock state operations
mock_state = {'count': 0, 'data': []}
for i in range(iterations):
if operation_type == 'read':
# Read operation
_ = mock_state['count']
elif operation_type == 'write':
# Write operation
mock_state['count'] = i
elif operation_type == 'complex_update':
# Complex update operation
mock_state['data'].append({'id': i, 'value': i * 2})
elif operation_type == 'bulk_update':
# Bulk update operation
mock_state.update({
'count': i,
'last_updated': time.time(),
'data': [j for j in range(i)]
})
end_time = time.time()
return end_time - start_time
# Test different state operations
operations = ['read', 'write', 'complex_update', 'bulk_update']
operation_times = {}
for operation in operations:
time_taken = test_state_operations(operation)
operation_times[operation] = time_taken
# Performance assertions
self.assertLess(operation_times['read'], 0.01,
"State read operations should be very fast")
self.assertLess(operation_times['write'], 0.05,
"State write operations should be fast")
self.assertLess(operation_times['complex_update'], 0.2,
"Complex state updates should be reasonable")
self.assertLess(operation_times['bulk_update'], 0.1,
"Bulk state updates should be efficient")
print(f"\nState Management Performance:")
for operation, time_taken in operation_times.items():
print(f"{operation}: {time_taken:.3f}s for 1000 operations")
def test_api_call_optimization(self):
"""Test API call optimization in frontend"""
def simulate_api_call(endpoint, cache_key=None, use_cache=False):
"""Simulate API call with caching"""
start_time = time.time()
if use_cache and cache_key:
# Check cache first
if hasattr(simulate_api_call, 'cache') and cache_key in simulate_api_call.cache:
end_time = time.time()
return {'cached': True, 'time': end_time - start_time}
# Simulate API call delay
if 'product' in endpoint:
time.sleep(0.05) # Product endpoint
elif 'user' in endpoint:
time.sleep(0.03) # User endpoint
else:
time.sleep(0.1) # Other endpoints
# Cache result if cache key provided
if use_cache and cache_key:
if not hasattr(simulate_api_call, 'cache'):
simulate_api_call.cache = {}
simulate_api_call.cache[cache_key] = {'data': 'mock_data'}
end_time = time.time()
return {'cached': False, 'time': end_time - start_time}
# Test API calls without caching
no_cache_times = []
endpoints = ['/api/products/', '/api/users/', '/api/tenants/']
for endpoint in endpoints:
result = simulate_api_call(endpoint)
no_cache_times.append(result['time'])
# Test API calls with caching
simulate_api_call.cache = {} # Reset cache
with_cache_times = []
for endpoint in endpoints:
cache_key = f"cache_{endpoint.replace('/', '_')}"
# First call - cache miss
result1 = simulate_api_call(endpoint, cache_key, use_cache=True)
# Second call - cache hit
result2 = simulate_api_call(endpoint, cache_key, use_cache=True)
with_cache_times.append(result1['time']) # Cache miss time
with_cache_times.append(result2['time']) # Cache hit time
avg_no_cache = statistics.mean(no_cache_times)
avg_with_cache = statistics.mean(with_cache_times)
# Performance assertions
self.assertLess(avg_no_cache, 0.15, "Average API call without cache should be under 150ms")
self.assertLess(avg_with_cache, 0.1, "Average API call with cache should be under 100ms")
print(f"\nAPI Call Optimization:")
print(f"Average without cache: {avg_no_cache:.3f}s")
print(f"Average with cache: {avg_with_cache:.3f}s")
print(f"Cache improvement: {(avg_no_cache / avg_with_cache):.1f}x")
def test_memory_usage_optimization(self):
"""Test memory usage optimization"""
def simulate_memory_usage(component_type, data_size=1000):
"""Simulate memory usage patterns"""
import sys
# Simulate component memory usage
if component_type == 'leaky':
# Memory leak simulation
data = []
for i in range(data_size):
data.append({'id': i, 'data': 'x' * 100}) # Retain references
return sys.getsizeof(data)
elif component_type == 'optimized':
# Memory optimized - clean up references
data = [{'id': i, 'data': 'x' * 100} for i in range(data_size)]
size = sys.getsizeof(data)
# Clear references
data.clear()
return size
elif component_type == 'virtualized':
# Virtualized list - only render visible items
visible_items = 50 # Only 50 items visible at once
data = [{'id': i, 'data': 'x' * 100} for i in range(visible_items)]
return sys.getsizeof(data)
# Test different memory usage patterns
memory_usage = {}
for component_type in ['leaky', 'optimized', 'virtualized']:
sizes = []
for _ in range(10): # Multiple measurements
size = simulate_memory_usage(component_type)
sizes.append(size)
avg_size = statistics.mean(sizes)
memory_usage[component_type] = avg_size
# Performance assertions
self.assertLess(memory_usage['optimized'], memory_usage['leaky'] * 0.5,
"Optimized component should use less memory")
self.assertLess(memory_usage['virtualized'], memory_usage['leaky'] * 0.1,
"Virtualized component should use much less memory")
print(f"\nMemory Usage Optimization:")
for component_type, size in memory_usage.items():
print(f"{component_type}: {size:.0f} bytes average")
def test_loading_performance(self):
"""Test loading and bundle performance"""
def simulate_bundle_loading(bundle_type):
"""Simulate different bundle loading scenarios"""
start_time = time.time()
if bundle_type == 'monolithic':
# Single large bundle
time.sleep(0.1) # 100ms for large bundle
bundle_size = 2000000 # 2MB
elif bundle_type == 'code_split':
# Code split bundles
time.sleep(0.05) # 50ms for initial bundle
time.sleep(0.02) # 20ms for lazy loaded bundle
bundle_size = 500000 # 500KB initial + 300KB lazy
elif bundle_type == 'optimized':
# Optimized with tree shaking
time.sleep(0.03) # 30ms for optimized bundle
bundle_size = 300000 # 300KB
end_time = time.time()
return {
'load_time': end_time - start_time,
'bundle_size': bundle_size
}
# Test different bundle strategies
bundle_results = {}
for bundle_type in ['monolithic', 'code_split', 'optimized']:
results = []
for _ in range(5): # Multiple measurements
result = simulate_bundle_loading(bundle_type)
results.append(result)
avg_load_time = statistics.mean([r['load_time'] for r in results])
avg_bundle_size = statistics.mean([r['bundle_size'] for r in results])
bundle_results[bundle_type] = {
'avg_load_time': avg_load_time,
'avg_bundle_size': avg_bundle_size
}
# Performance assertions
self.assertLess(bundle_results['monolithic']['avg_load_time'], 0.15,
"Monolithic bundle should load in under 150ms")
self.assertLess(bundle_results['code_split']['avg_load_time'], 0.1,
"Code split bundle should load faster")
self.assertLess(bundle_results['optimized']['avg_load_time'], 0.05,
"Optimized bundle should load fastest")
self.assertLess(bundle_results['optimized']['avg_bundle_size'], 500000,
"Optimized bundle should be under 500KB")
print(f"\nLoading Performance:")
for bundle_type, results in bundle_results.items():
print(f"{bundle_type}: {results['avg_load_time']:.3f}s, {results['avg_bundle_size']:.0f} bytes")
def test_react_optimization_techniques(self):
"""Test React optimization techniques"""
def test_render_technique(technique, items=100):
"""Test different React rendering optimization techniques"""
start_time = time.time()
if technique == 'basic':
# Basic rendering - re-renders all items
for i in range(items):
# Simulate DOM update for each item
time.sleep(0.001) # 1ms per item
elif technique == 'memoized':
# Memoized components - only re-renders changed items
changed_items = items // 10 # Only 10% changed
for i in range(changed_items):
time.sleep(0.001) # 1ms per changed item
elif technique == 'virtualized':
# Virtualized list - only renders visible items
visible_items = 20 # Only 20 items visible
for i in range(visible_items):
time.sleep(0.001) # 1ms per visible item
elif technique == 'debounced':
# Debounced updates - batch updates
time.sleep(0.01) # Single batch update
end_time = time.time()
return end_time - start_time
# Test different optimization techniques
techniques = ['basic', 'memoized', 'virtualized', 'debounced']
technique_results = {}
for technique in techniques:
times = []
for _ in range(10): # Multiple measurements
render_time = test_render_technique(technique)
times.append(render_time)
avg_time = statistics.mean(times)
technique_results[technique] = avg_time
# Performance assertions
self.assertLess(technique_results['memoized'], technique_results['basic'] * 0.3,
"Memoized rendering should be much faster than basic")
self.assertLess(technique_results['virtualized'], technique_results['basic'] * 0.2,
"Virtualized rendering should be much faster than basic")
self.assertLess(technique_results['debounced'], technique_results['basic'] * 0.1,
"Debounced updates should be much faster than basic")
print(f"\nReact Optimization Techniques:")
for technique, avg_time in technique_results.items():
print(f"{technique}: {avg_time:.3f}s average")
def test_image_and_asset_optimization(self):
"""Test image and asset optimization"""
def simulate_image_loading(image_type, file_size):
"""Simulate image loading with optimization"""
start_time = time.time()
if image_type == 'unoptimized':
# Large, unoptimized image
load_time = file_size / 1000000 * 0.5 # 0.5s per MB
elif image_type == 'compressed':
# Compressed image
compressed_size = file_size * 0.3 # 70% compression
load_time = compressed_size / 1000000 * 0.3 # Faster loading
elif image_type == 'lazy_loaded':
# Lazy loaded image
load_time = 0.01 # Very fast, loads on demand
elif image_type == 'webp':
# Modern format (WebP)
webp_size = file_size * 0.5 # 50% smaller
load_time = webp_size / 1000000 * 0.2 # Much faster
time.sleep(load_time)
end_time = time.time()
return {
'load_time': end_time - start_time,
'effective_size': file_size if image_type == 'unoptimized' else file_size * 0.5
}
# Test different image optimization strategies
image_size = 2000000 # 2MB image
optimization_results = {}
for image_type in ['unoptimized', 'compressed', 'lazy_loaded', 'webp']:
results = []
for _ in range(5):
result = simulate_image_loading(image_type, image_size)
results.append(result)
avg_load_time = statistics.mean([r['load_time'] for r in results])
avg_effective_size = statistics.mean([r['effective_size'] for r in results])
optimization_results[image_type] = {
'avg_load_time': avg_load_time,
'avg_effective_size': avg_effective_size
}
# Performance assertions
self.assertLess(optimization_results['compressed']['avg_load_time'],
optimization_results['unoptimized']['avg_load_time'] * 0.5,
"Compressed images should load faster")
self.assertLess(optimization_results['webp']['avg_load_time'],
optimization_results['unoptimized']['avg_load_time'] * 0.4,
"WebP images should load much faster")
print(f"\nImage Optimization Performance (2MB original):")
for image_type, results in optimization_results.items():
print(f"{image_type}: {results['avg_load_time']:.3f}s, {results['avg_effective_size']:.0f} bytes")
def test_overall_performance_score(self):
"""Calculate overall performance score"""
# This is a comprehensive performance score calculation
performance_metrics = {
'component_rendering': 0.8, # 80% good
'state_management': 0.9, # 90% good
'api_optimization': 0.85, # 85% good
'memory_usage': 0.75, # 75% good
'loading_performance': 0.8, # 80% good
'react_optimization': 0.85, # 85% good
'image_optimization': 0.7 # 70% good
}
overall_score = statistics.mean(performance_metrics.values())
# Performance assertions
self.assertGreater(overall_score, 0.7,
"Overall performance score should be above 70%")
print(f"\nOverall Performance Score:")
for metric, score in performance_metrics.items():
print(f"{metric}: {score:.1%}")
print(f"Overall Score: {overall_score:.1%}")
# Provide optimization recommendations
if overall_score < 0.8:
recommendations = [
"Implement code splitting for better loading performance",
"Add image compression and lazy loading",
"Optimize component rendering with memoization",
"Implement proper caching strategies",
"Use virtualized lists for large datasets"
]
print("\nOptimization Recommendations:")
for i, rec in enumerate(recommendations, 1):
print(f"{i}. {rec}")

View File

View File

@@ -0,0 +1,459 @@
"""
Unit tests for Beauty Models
Tests for beauty module models:
- Client
- Service
Author: Claude
"""
import pytest
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from decimal import Decimal
from datetime import date, time, timedelta
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.modules.beauty.models.client import Client
from backend.src.modules.beauty.models.service import Service
User = get_user_model()
class ClientModelTest(TestCase):
"""Test cases for Client model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Beauty Salon',
schema_name='test_beauty',
domain='testbeauty.com',
business_type='beauty'
)
self.user = User.objects.create_user(
username='receptionist',
email='receptionist@test.com',
password='test123',
tenant=self.tenant,
role='staff'
)
self.client_data = {
'tenant': self.tenant,
'client_number': 'C2024010001',
'first_name': 'Siti',
'last_name': 'Binti Ahmad',
'ic_number': '000101-01-0001',
'passport_number': '',
'nationality': 'Malaysian',
'gender': 'female',
'date_of_birth': date(1995, 1, 1),
'email': 'siti.client@test.com',
'phone': '+60123456789',
'whatsapp_number': '+60123456789',
'emergency_contact_name': 'Ahmad Bin Ibrahim',
'emergency_contact_phone': '+60123456788',
'emergency_contact_relationship': 'Husband',
'address': '123 Beauty Street',
'city': 'Kuala Lumpur',
'state': 'KUL',
'postal_code': '50000',
'occupation': 'Office Worker',
'company': 'Test Company',
'skin_type': 'normal',
'hair_type': 'straight',
'allergies': 'None',
'skin_conditions': 'None',
'medications': 'None',
'pregnancy_status': False,
'pregnancy_due_date': None,
'breastfeeding': False,
'preferred_services': ['facial', 'manicure'],
'membership_tier': 'basic',
'loyalty_points': 0,
'total_spent': Decimal('0.00'),
'visit_count': 0,
'last_visit_date': None,
'preferred_stylist': '',
'preferred_appointment_time': 'morning',
'marketing_consent': True,
'sms_consent': True,
'email_consent': True,
'photo_consent': False,
'medical_consent': True,
'privacy_consent': True,
'notes': 'New client',
'referral_source': 'walk_in',
'referred_by': '',
'is_active': True,
'created_by': self.user
}
def test_create_client(self):
"""Test creating a new client"""
client = Client.objects.create(**self.client_data)
self.assertEqual(client.tenant, self.tenant)
self.assertEqual(client.client_number, self.client_data['client_number'])
self.assertEqual(client.first_name, self.client_data['first_name'])
self.assertEqual(client.last_name, self.client_data['last_name'])
self.assertEqual(client.ic_number, self.client_data['ic_number'])
self.assertEqual(client.gender, self.client_data['gender'])
self.assertEqual(client.skin_type, self.client_data['skin_type'])
self.assertEqual(client.membership_tier, self.client_data['membership_tier'])
self.assertEqual(client.loyalty_points, self.client_data['loyalty_points'])
self.assertTrue(client.is_active)
def test_client_string_representation(self):
"""Test client string representation"""
client = Client.objects.create(**self.client_data)
self.assertEqual(str(client), f"{client.first_name} {client.last_name} ({client.client_number})")
def test_client_full_name(self):
"""Test client full name property"""
client = Client.objects.create(**self.client_data)
self.assertEqual(client.full_name, f"{client.first_name} {client.last_name}")
def test_client_age(self):
"""Test client age calculation"""
client = Client.objects.create(**self.client_data)
# Age should be calculated based on date of birth
today = date.today()
expected_age = today.year - client.date_of_birth.year
if today.month < client.date_of_birth.month or (today.month == client.date_of_birth.month and today.day < client.date_of_birth.day):
expected_age -= 1
self.assertEqual(client.age, expected_age)
def test_client_malaysian_ic_validation(self):
"""Test Malaysian IC number validation"""
# Valid IC number
client = Client.objects.create(**self.client_data)
self.assertEqual(client.ic_number, self.client_data['ic_number'])
# Invalid IC number format
invalid_data = self.client_data.copy()
invalid_data['ic_number'] = '123'
with self.assertRaises(Exception):
Client.objects.create(**invalid_data)
def test_client_gender_choices(self):
"""Test client gender validation"""
invalid_data = self.client_data.copy()
invalid_data['gender'] = 'invalid_gender'
with self.assertRaises(Exception):
Client.objects.create(**invalid_data)
def test_client_membership_tier_choices(self):
"""Test client membership tier validation"""
invalid_data = self.client_data.copy()
invalid_data['membership_tier'] = 'invalid_tier'
with self.assertRaises(Exception):
Client.objects.create(**invalid_data)
def test_client_skin_type_choices(self):
"""Test client skin type validation"""
invalid_data = self.client_data.copy()
invalid_data['skin_type'] = 'invalid_skin'
with self.assertRaises(Exception):
Client.objects.create(**invalid_data)
def test_client_hair_type_choices(self):
"""Test client hair type validation"""
invalid_data = self.client_data.copy()
invalid_data['hair_type'] = 'invalid_hair'
with self.assertRaises(Exception):
Client.objects.create(**invalid_data)
def test_client_phone_validation(self):
"""Test Malaysian phone number validation"""
# Valid Malaysian phone numbers
client = Client.objects.create(**self.client_data)
self.assertEqual(client.phone, self.client_data['phone'])
self.assertEqual(client.whatsapp_number, self.client_data['whatsapp_number'])
# Invalid phone
invalid_data = self.client_data.copy()
invalid_data['phone'] = '12345'
with self.assertRaises(Exception):
Client.objects.create(**invalid_data)
def test_client_medical_information(self):
"""Test client medical information validation"""
client = Client.objects.create(**self.client_data)
self.assertEqual(client.allergies, self.client_data['allergies'])
self.assertEqual(client.skin_conditions, self.client_data['skin_conditions'])
self.assertEqual(client.medications, self.client_data['medications'])
self.assertFalse(client.pregnancy_status)
self.assertFalse(client.breastfeeding)
def test_client_consent_preferences(self):
"""Test client consent preferences"""
client = Client.objects.create(**self.client_data)
self.assertTrue(client.marketing_consent)
self.assertTrue(client.sms_consent)
self.assertTrue(client.email_consent)
self.assertFalse(client.photo_consent)
self.assertTrue(client.medical_consent)
self.assertTrue(client.privacy_consent)
def test_client_loyalty_program(self):
"""Test client loyalty program features"""
client = Client.objects.create(**self.client_data)
self.assertEqual(client.loyalty_points, 0)
self.assertEqual(client.total_spent, Decimal('0.00'))
self.assertEqual(client.visit_count, 0)
# Test tier progression logic
self.assertEqual(client.membership_tier, 'basic')
def test_client_referral_source_choices(self):
"""Test client referral source validation"""
# Test valid referral sources
valid_sources = ['walk_in', 'friend', 'social_media', 'advertisement', 'online', 'other']
for source in valid_sources:
data = self.client_data.copy()
data['referral_source'] = source
client = Client.objects.create(**data)
self.assertEqual(client.referral_source, source)
# Test invalid referral source
invalid_data = self.client_data.copy()
invalid_data['referral_source'] = 'invalid_source'
with self.assertRaises(Exception):
Client.objects.create(**invalid_data)
class ServiceModelTest(TestCase):
"""Test cases for Service model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Beauty Salon',
schema_name='test_beauty',
domain='testbeauty.com',
business_type='beauty'
)
self.user = User.objects.create_user(
username='manager',
email='manager@test.com',
password='test123',
tenant=self.tenant,
role='admin'
)
self.service_data = {
'tenant': self.tenant,
'service_code': 'FAC-BASIC-001',
'name': 'Basic Facial Treatment',
'description': 'A basic facial treatment for all skin types',
'service_category': 'facial',
'duration': 60, # minutes
'base_price': Decimal('80.00'),
'premium_price': Decimal('120.00'),
'vip_price': Decimal('100.00'),
'tax_rate': 6.0, # SST
'is_taxable': True,
'commission_rate': 20.0, # percentage
'difficulty_level': 'basic',
'experience_required': 0, # years
'min_age': 16,
'max_age': 65,
'suitable_for_skin_types': ['normal', 'dry', 'oily', 'combination', 'sensitive'],
'suitable_for_hair_types': [],
'pregnancy_safe': True,
'breastfeeding_safe': True,
'requires_patch_test': False,
'has_contraindications': False,
'contraindications': '',
'equipment_required': ['Facial steamer', 'Cleansing brush', 'Toner'],
'products_used': ['Cleanser', 'Toner', 'Moisturizer', 'Sunscreen'],
'steps': ['Cleansing', 'Exfoliation', 'Massage', 'Mask', 'Moisturizing'],
'aftercare_instructions': 'Avoid direct sunlight for 24 hours',
'frequency_limit_days': 7,
'is_active': True,
'is_popular': True,
'is_new': False,
'is_promotional': False,
'kkm_approval_required': False,
'kkm_approval_number': '',
'min_booking_notice_hours': 2,
'cancellation_policy_hours': 24,
'late_arrival_policy_minutes': 15,
'no_show_policy': 'fee',
'created_by': self.user
}
def test_create_service(self):
"""Test creating a new service"""
service = Service.objects.create(**self.service_data)
self.assertEqual(service.tenant, self.tenant)
self.assertEqual(service.service_code, self.service_data['service_code'])
self.assertEqual(service.name, self.service_data['name'])
self.assertEqual(service.service_category, self.service_data['service_category'])
self.assertEqual(service.duration, self.service_data['duration'])
self.assertEqual(service.base_price, self.service_data['base_price'])
self.assertEqual(service.tax_rate, self.service_data['tax_rate'])
self.assertEqual(service.difficulty_level, self.service_data['difficulty_level'])
self.assertTrue(service.is_active)
self.assertTrue(service.is_popular)
def test_service_string_representation(self):
"""Test service string representation"""
service = Service.objects.create(**self.service_data)
self.assertEqual(str(service), service.name)
def test_service_price_with_tax(self):
"""Test service price calculation with tax"""
service = Service.objects.create(**self.service_data)
# Base price with tax
expected_base_with_tax = service.base_price * (1 + service.tax_rate / 100)
self.assertEqual(service.base_price_with_tax, expected_base_with_tax)
# Premium price with tax
expected_premium_with_tax = service.premium_price * (1 + service.tax_rate / 100)
self.assertEqual(service.premium_price_with_tax, expected_premium_with_tax)
# VIP price with tax
expected_vip_with_tax = service.vip_price * (1 + service.tax_rate / 100)
self.assertEqual(service.vip_price_with_tax, expected_vip_with_tax)
def test_service_category_choices(self):
"""Test service category validation"""
invalid_data = self.service_data.copy()
invalid_data['service_category'] = 'invalid_category'
with self.assertRaises(Exception):
Service.objects.create(**invalid_data)
def test_service_difficulty_level_choices(self):
"""Test service difficulty level validation"""
invalid_data = self.service_data.copy()
invalid_data['difficulty_level'] = 'invalid_difficulty'
with self.assertRaises(Exception):
Service.objects.create(**invalid_data)
def test_service_tax_calculation(self):
"""Test service tax calculation"""
service = Service.objects.create(**self.service_data)
# Tax amount for base price
expected_tax = service.base_price * (service.tax_rate / 100)
self.assertEqual(service.tax_amount, expected_tax)
def test_service_commission_calculation(self):
"""Test service commission calculation"""
service = Service.objects.create(**self.service_data)
# Commission amount for base price
expected_commission = service.base_price * (service.commission_rate / 100)
self.assertEqual(service.commission_amount, expected_commission)
def test_service_age_validation(self):
"""Test service age validation"""
# Valid age range
service = Service.objects.create(**self.service_data)
self.assertEqual(service.min_age, self.service_data['min_age'])
self.assertEqual(service.max_age, self.service_data['max_age'])
# Invalid age range (min > max)
invalid_data = self.service_data.copy()
invalid_data['min_age'] = 30
invalid_data['max_age'] = 20
with self.assertRaises(Exception):
Service.objects.create(**invalid_data)
def test_service_malaysian_sst_validation(self):
"""Test Malaysian SST validation"""
# Valid SST rate
service = Service.objects.create(**self.service_data)
self.assertEqual(service.tax_rate, 6.0) # Standard SST rate
# Invalid SST rate (negative)
invalid_data = self.service_data.copy()
invalid_data['tax_rate'] = -1.0
with self.assertRaises(Exception):
Service.objects.create(**invalid_data)
def test_service_duration_validation(self):
"""Test service duration validation"""
# Valid duration
service = Service.objects.create(**self.service_data)
self.assertEqual(service.duration, self.service_data['duration'])
# Invalid duration (too short)
invalid_data = self.service_data.copy()
invalid_data['duration'] = 0
with self.assertRaises(Exception):
Service.objects.create(**invalid_data)
def test_service_price_validation(self):
"""Test service price validation"""
# Valid prices
service = Service.objects.create(**self.service_data)
self.assertEqual(service.base_price, self.service_data['base_price'])
self.assertEqual(service.premium_price, self.service_data['premium_price'])
self.assertEqual(service.vip_price, self.service_data['vip_price'])
# Invalid price (negative)
invalid_data = self.service_data.copy()
invalid_data['base_price'] = Decimal('-10.00')
with self.assertRaises(Exception):
Service.objects.create(**invalid_data)
def test_service_suitability_validation(self):
"""Test service suitability validation"""
service = Service.objects.create(**self.service_data)
# Check skin type suitability
self.assertIn('normal', service.suitable_for_skin_types)
self.assertIn('sensitive', service.suitable_for_skin_types)
# Check pregnancy safety
self.assertTrue(service.pregnancy_safe)
self.assertTrue(service.breastfeeding_safe)
def test_service_malaysian_beauty_regulations(self):
"""Test Malaysian beauty industry regulations"""
service = Service.objects.create(**self.service_data)
self.assertEqual(service.tax_rate, 6.0) # SST compliance
self.assertFalse(service.kkm_approval_required) # KKM approval status
# Test service requiring KKM approval
data = self.service_data.copy()
data['name'] = 'Advanced Laser Treatment'
data['kkm_approval_required'] = True
data['kkm_approval_number'] = 'KKM/2024/001234'
service_kkm = Service.objects.create(**data)
self.assertTrue(service_kkm.kkm_approval_required)
self.assertEqual(service_kkm.kkm_approval_number, data['kkm_approval_number'])
def test_service_booking_policies(self):
"""Test service booking policies"""
service = Service.objects.create(**self.service_data)
self.assertEqual(service.min_booking_notice_hours, 2)
self.assertEqual(service.cancellation_policy_hours, 24)
self.assertEqual(service.late_arrival_policy_minutes, 15)
self.assertEqual(service.no_show_policy, 'fee')

View File

@@ -0,0 +1,340 @@
"""
Unit tests for Core Models
Tests for all core models:
- Tenant
- User
- Subscription
- Module
- PaymentTransaction
Author: Claude
"""
import pytest
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from decimal import Decimal
from datetime import date, timedelta
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.core.models.subscription import Subscription
from backend.src.core.models.module import Module
from backend.src.core.models.payment import PaymentTransaction
User = get_user_model()
class TenantModelTest(TestCase):
"""Test cases for Tenant model"""
def setUp(self):
self.tenant_data = {
'name': 'Test Business Sdn Bhd',
'schema_name': 'test_business',
'domain': 'testbusiness.com',
'business_type': 'retail',
'registration_number': '202401000001',
'tax_id': 'MY123456789',
'contact_email': 'contact@testbusiness.com',
'contact_phone': '+60123456789',
'address': '123 Test Street, Kuala Lumpur',
'city': 'Kuala Lumpur',
'state': 'KUL',
'postal_code': '50000',
'country': 'Malaysia',
'is_active': True
}
def test_create_tenant(self):
"""Test creating a new tenant"""
tenant = Tenant.objects.create(**self.tenant_data)
self.assertEqual(tenant.name, self.tenant_data['name'])
self.assertEqual(tenant.schema_name, self.tenant_data['schema_name'])
self.assertEqual(tenant.business_type, self.tenant_data['business_type'])
self.assertTrue(tenant.is_active)
self.assertEqual(tenant.subscription_tier, 'free')
self.assertIsNotNone(tenant.created_at)
def test_tenant_string_representation(self):
"""Test tenant string representation"""
tenant = Tenant.objects.create(**self.tenant_data)
self.assertEqual(str(tenant), f"{tenant.name} ({tenant.schema_name})")
def test_tenant_business_type_choices(self):
"""Test tenant business type validation"""
invalid_data = self.tenant_data.copy()
invalid_data['business_type'] = 'invalid_type'
with self.assertRaises(Exception):
Tenant.objects.create(**invalid_data)
def test_tenant_malaysian_business_validation(self):
"""Test Malaysian business registration validation"""
# Valid registration number
tenant = Tenant.objects.create(**self.tenant_data)
self.assertEqual(tenant.registration_number, self.tenant_data['registration_number'])
# Invalid registration number format
invalid_data = self.tenant_data.copy()
invalid_data['registration_number'] = '123'
with self.assertRaises(Exception):
Tenant.objects.create(**invalid_data)
def test_tenant_phone_validation(self):
"""Test Malaysian phone number validation"""
# Valid Malaysian phone number
tenant = Tenant.objects.create(**self.tenant_data)
self.assertEqual(tenant.contact_phone, self.tenant_data['contact_phone'])
# Invalid phone number
invalid_data = self.tenant_data.copy()
invalid_data['contact_phone'] = '12345'
with self.assertRaises(Exception):
Tenant.objects.create(**invalid_data)
class UserModelTest(TestCase):
"""Test cases for User model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Business Sdn Bhd',
schema_name='test_business',
domain='testbusiness.com',
business_type='retail'
)
self.user_data = {
'username': 'testuser',
'email': 'user@test.com',
'first_name': 'Test',
'last_name': 'User',
'phone': '+60123456789',
'ic_number': '000101-01-0001',
'tenant': self.tenant,
'role': 'owner',
'is_active': True
}
def test_create_user(self):
"""Test creating a new user"""
user = User.objects.create_user(**self.user_data)
self.assertEqual(user.username, self.user_data['username'])
self.assertEqual(user.email, self.user_data['email'])
self.assertEqual(user.tenant, self.tenant)
self.assertEqual(user.role, self.user_data['role'])
self.assertTrue(user.is_active)
self.assertFalse(user.is_staff)
def test_create_superuser(self):
"""Test creating a superuser"""
superuser = User.objects.create_superuser(
username='admin',
email='admin@test.com',
password='admin123'
)
self.assertTrue(superuser.is_staff)
self.assertTrue(superuser.is_superuser)
self.assertEqual(superuser.role, 'admin')
def test_user_string_representation(self):
"""Test user string representation"""
user = User.objects.create_user(**self.user_data)
self.assertEqual(str(user), user.email)
def test_user_full_name(self):
"""Test user full name property"""
user = User.objects.create_user(**self.user_data)
self.assertEqual(user.full_name, f"{user.first_name} {user.last_name}")
def test_user_malaysian_ic_validation(self):
"""Test Malaysian IC number validation"""
# Valid IC number
user = User.objects.create_user(**self.user_data)
self.assertEqual(user.ic_number, self.user_data['ic_number'])
# Invalid IC number
invalid_data = self.user_data.copy()
invalid_data['ic_number'] = '123'
with self.assertRaises(Exception):
User.objects.create_user(**invalid_data)
def test_user_role_choices(self):
"""Test user role validation"""
invalid_data = self.user_data.copy()
invalid_data['role'] = 'invalid_role'
with self.assertRaises(Exception):
User.objects.create_user(**invalid_data)
class SubscriptionModelTest(TestCase):
"""Test cases for Subscription model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Business Sdn Bhd',
schema_name='test_business',
domain='testbusiness.com',
business_type='retail'
)
self.subscription_data = {
'tenant': self.tenant,
'plan': 'premium',
'status': 'active',
'start_date': date.today(),
'end_date': date.today() + timedelta(days=30),
'amount': Decimal('299.00'),
'currency': 'MYR',
'billing_cycle': 'monthly',
'auto_renew': True
}
def test_create_subscription(self):
"""Test creating a new subscription"""
subscription = Subscription.objects.create(**self.subscription_data)
self.assertEqual(subscription.tenant, self.tenant)
self.assertEqual(subscription.plan, self.subscription_data['plan'])
self.assertEqual(subscription.status, self.subscription_data['status'])
self.assertEqual(subscription.amount, self.subscription_data['amount'])
self.assertTrue(subscription.auto_renew)
def test_subscription_string_representation(self):
"""Test subscription string representation"""
subscription = Subscription.objects.create(**self.subscription_data)
expected = f"{self.tenant.name} - Premium ({subscription.status})"
self.assertEqual(str(subscription), expected)
def test_subscription_is_active_property(self):
"""Test subscription is_active property"""
# Active subscription
subscription = Subscription.objects.create(**self.subscription_data)
self.assertTrue(subscription.is_active)
# Expired subscription
subscription.end_date = date.today() - timedelta(days=1)
subscription.save()
self.assertFalse(subscription.is_active)
# Cancelled subscription
subscription.status = 'cancelled'
subscription.end_date = date.today() + timedelta(days=30)
subscription.save()
self.assertFalse(subscription.is_active)
def test_subscription_status_choices(self):
"""Test subscription status validation"""
invalid_data = self.subscription_data.copy()
invalid_data['status'] = 'invalid_status'
with self.assertRaises(Exception):
Subscription.objects.create(**invalid_data)
class ModuleModelTest(TestCase):
"""Test cases for Module model"""
def setUp(self):
self.module_data = {
'name': 'Retail Management',
'code': 'retail',
'description': 'Complete retail management solution',
'category': 'industry',
'version': '1.0.0',
'is_active': True,
'is_core': False,
'dependencies': ['core'],
'config_schema': {'features': ['inventory', 'sales']},
'pricing_tier': 'premium'
}
def test_create_module(self):
"""Test creating a new module"""
module = Module.objects.create(**self.module_data)
self.assertEqual(module.name, self.module_data['name'])
self.assertEqual(module.code, self.module_data['code'])
self.assertEqual(module.category, self.module_data['category'])
self.assertTrue(module.is_active)
self.assertFalse(module.is_core)
def test_module_string_representation(self):
"""Test module string representation"""
module = Module.objects.create(**self.module_data)
self.assertEqual(str(module), module.name)
def test_module_category_choices(self):
"""Test module category validation"""
invalid_data = self.module_data.copy()
invalid_data['category'] = 'invalid_category'
with self.assertRaises(Exception):
Module.objects.create(**invalid_data)
class PaymentTransactionModelTest(TestCase):
"""Test cases for PaymentTransaction model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Business Sdn Bhd',
schema_name='test_business',
domain='testbusiness.com',
business_type='retail'
)
self.subscription = Subscription.objects.create(
tenant=self.tenant,
plan='premium',
status='active',
start_date=date.today(),
end_date=date.today() + timedelta(days=30),
amount=Decimal('299.00'),
currency='MYR'
)
self.payment_data = {
'tenant': self.tenant,
'subscription': self.subscription,
'transaction_id': 'PAY-2024010001',
'amount': Decimal('299.00'),
'currency': 'MYR',
'payment_method': 'fpx',
'status': 'completed',
'payment_date': timezone.now(),
'description': 'Monthly subscription payment'
}
def test_create_payment_transaction(self):
"""Test creating a new payment transaction"""
payment = PaymentTransaction.objects.create(**self.payment_data)
self.assertEqual(payment.tenant, self.tenant)
self.assertEqual(payment.subscription, self.subscription)
self.assertEqual(payment.transaction_id, self.payment_data['transaction_id'])
self.assertEqual(payment.amount, self.payment_data['amount'])
self.assertEqual(payment.status, self.payment_data['status'])
def test_payment_string_representation(self):
"""Test payment transaction string representation"""
payment = PaymentTransaction.objects.create(**self.payment_data)
expected = f"PAY-2024010001 - RM299.00 ({payment.status})"
self.assertEqual(str(payment), expected)
def test_payment_method_choices(self):
"""Test payment method validation"""
invalid_data = self.payment_data.copy()
invalid_data['payment_method'] = 'invalid_method'
with self.assertRaises(Exception):
PaymentTransaction.objects.create(**invalid_data)
def test_payment_status_choices(self):
"""Test payment status validation"""
invalid_data = self.payment_data.copy()
invalid_data['status'] = 'invalid_status'
with self.assertRaises(Exception):
PaymentTransaction.objects.create(**invalid_data)

View File

@@ -0,0 +1,413 @@
"""
Unit tests for Education Models
Tests for education module models:
- Student
- Class
Author: Claude
"""
import pytest
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from decimal import Decimal
from datetime import date, time, timedelta
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.modules.education.models.student import Student
from backend.src.modules.education.models.class_model import Class
User = get_user_model()
class StudentModelTest(TestCase):
"""Test cases for Student model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Education Center',
schema_name='test_education',
domain='testeducation.com',
business_type='education'
)
self.user = User.objects.create_user(
username='admin',
email='admin@test.com',
password='test123',
tenant=self.tenant,
role='admin'
)
self.student_data = {
'tenant': self.tenant,
'student_id': 'S2024010001',
'first_name': 'Ahmad',
'last_name': 'Bin Ibrahim',
'ic_number': '000101-01-0001',
'gender': 'male',
'date_of_birth': date(2010, 1, 1),
'nationality': 'Malaysian',
'religion': 'Islam',
'race': 'Malay',
'email': 'ahmad.student@test.com',
'phone': '+60123456789',
'address': '123 Student Street',
'city': 'Kuala Lumpur',
'state': 'KUL',
'postal_code': '50000',
'father_name': 'Ibrahim Bin Ali',
'father_phone': '+60123456788',
'father_occupation': 'Engineer',
'mother_name': 'Aminah Binti Ahmad',
'mother_phone': '+60123456787',
'mother_occupation': 'Teacher',
'emergency_contact_name': 'Ibrahim Bin Ali',
'emergency_contact_phone': '+60123456788',
'emergency_contact_relationship': 'Father',
'previous_school': 'SK Test Primary',
'previous_grade': '6A',
'current_grade': 'Form 1',
'stream': 'science',
'admission_date': date.today(),
'graduation_date': None,
'status': 'active',
'medical_conditions': 'None',
'allergies': 'None',
'special_needs': 'None',
'is_active': True,
'created_by': self.user
}
def test_create_student(self):
"""Test creating a new student"""
student = Student.objects.create(**self.student_data)
self.assertEqual(student.tenant, self.tenant)
self.assertEqual(student.student_id, self.student_data['student_id'])
self.assertEqual(student.first_name, self.student_data['first_name'])
self.assertEqual(student.last_name, self.student_data['last_name'])
self.assertEqual(student.ic_number, self.student_data['ic_number'])
self.assertEqual(student.gender, self.student_data['gender'])
self.assertEqual(student.current_grade, self.student_data['current_grade'])
self.assertEqual(student.stream, self.student_data['stream'])
self.assertEqual(student.status, self.student_data['status'])
self.assertTrue(student.is_active)
def test_student_string_representation(self):
"""Test student string representation"""
student = Student.objects.create(**self.student_data)
self.assertEqual(str(student), f"{student.first_name} {student.last_name} ({student.student_id})")
def test_student_full_name(self):
"""Test student full name property"""
student = Student.objects.create(**self.student_data)
self.assertEqual(student.full_name, f"{student.first_name} {student.last_name}")
def test_student_age(self):
"""Test student age calculation"""
student = Student.objects.create(**self.student_data)
# Age should be calculated based on date of birth
today = date.today()
expected_age = today.year - student.date_of_birth.year
if today.month < student.date_of_birth.month or (today.month == student.date_of_birth.month and today.day < student.date_of_birth.day):
expected_age -= 1
self.assertEqual(student.age, expected_age)
def test_student_malaysian_ic_validation(self):
"""Test Malaysian IC number validation"""
# Valid IC number
student = Student.objects.create(**self.student_data)
self.assertEqual(student.ic_number, self.student_data['ic_number'])
# Invalid IC number format
invalid_data = self.student_data.copy()
invalid_data['ic_number'] = '123'
with self.assertRaises(Exception):
Student.objects.create(**invalid_data)
def test_student_gender_choices(self):
"""Test student gender validation"""
invalid_data = self.student_data.copy()
invalid_data['gender'] = 'invalid_gender'
with self.assertRaises(Exception):
Student.objects.create(**invalid_data)
def test_student_grade_validation(self):
"""Test student grade validation"""
# Test valid grades
valid_grades = ['Form 1', 'Form 2', 'Form 3', 'Form 4', 'Form 5', 'Form 6']
for grade in valid_grades:
data = self.student_data.copy()
data['current_grade'] = grade
student = Student.objects.create(**data)
self.assertEqual(student.current_grade, grade)
# Test invalid grade
invalid_data = self.student_data.copy()
invalid_data['current_grade'] = 'Form 7'
with self.assertRaises(Exception):
Student.objects.create(**invalid_data)
def test_student_stream_choices(self):
"""Test student stream validation"""
# Test valid streams
valid_streams = ['science', 'arts', 'commerce', 'technical']
for stream in valid_streams:
data = self.student_data.copy()
data['stream'] = stream
student = Student.objects.create(**data)
self.assertEqual(student.stream, stream)
# Test invalid stream
invalid_data = self.student_data.copy()
invalid_data['stream'] = 'invalid_stream'
with self.assertRaises(Exception):
Student.objects.create(**invalid_data)
def test_student_status_choices(self):
"""Test student status validation"""
# Test valid statuses
valid_statuses = ['active', 'inactive', 'graduated', 'transferred', 'suspended']
for status in valid_statuses:
data = self.student_data.copy()
data['status'] = status
student = Student.objects.create(**data)
self.assertEqual(student.status, status)
# Test invalid status
invalid_data = self.student_data.copy()
invalid_data['status'] = 'invalid_status'
with self.assertRaises(Exception):
Student.objects.create(**invalid_data)
def test_student_parent_information(self):
"""Test student parent information validation"""
student = Student.objects.create(**self.student_data)
self.assertEqual(student.father_name, self.student_data['father_name'])
self.assertEqual(student.mother_name, self.student_data['mother_name'])
self.assertEqual(student.emergency_contact_name, self.student_data['emergency_contact_name'])
def test_student_malaysian_education_info(self):
"""Test Malaysian education specific information"""
student = Student.objects.create(**self.student_data)
self.assertEqual(student.religion, self.student_data['religion'])
self.assertEqual(student.race, self.student_data['race'])
self.assertEqual(student.previous_school, self.student_data['previous_school'])
self.assertEqual(student.previous_grade, self.student_data['previous_grade'])
class ClassModelTest(TestCase):
"""Test cases for Class model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Education Center',
schema_name='test_education',
domain='testeducation.com',
business_type='education'
)
self.teacher = User.objects.create_user(
username='teacher',
email='teacher@test.com',
password='test123',
tenant=self.tenant,
role='staff'
)
self.student = Student.objects.create(
tenant=self.tenant,
student_id='S2024010001',
first_name='Ahmad',
last_name='Bin Ibrahim',
ic_number='000101-01-0001',
gender='male',
date_of_birth=date(2010, 1, 1),
current_grade='Form 1',
stream='science',
admission_date=date.today(),
status='active'
)
self.class_data = {
'tenant': self.tenant,
'class_name': 'Mathematics Form 1',
'class_code': 'MATH-F1-2024',
'grade': 'Form 1',
'stream': 'science',
'subject': 'Mathematics',
'academic_year': '2024',
'semester': '1',
'teacher': self.teacher,
'room': 'B1-01',
'max_students': 30,
'schedule_days': ['Monday', 'Wednesday', 'Friday'],
'start_time': time(8, 0),
'end_time': time(9, 30),
'start_date': date.today(),
'end_date': date.today() + timedelta(days=180),
'is_active': True,
'syllabus': 'KSSM Mathematics Form 1',
'objectives': 'Complete KSSM Mathematics syllabus',
'assessment_methods': 'Tests, Assignments, Projects',
'created_by': self.teacher
}
def test_create_class(self):
"""Test creating a new class"""
class_obj = Class.objects.create(**self.class_data)
self.assertEqual(class_obj.tenant, self.tenant)
self.assertEqual(class_obj.class_name, self.class_data['class_name'])
self.assertEqual(class_obj.class_code, self.class_data['class_code'])
self.assertEqual(class_obj.grade, self.class_data['grade'])
self.assertEqual(class_obj.stream, self.class_data['stream'])
self.assertEqual(class_obj.subject, self.class_data['subject'])
self.assertEqual(class_obj.teacher, self.teacher)
self.assertEqual(class_obj.max_students, self.class_data['max_students'])
self.assertTrue(class_obj.is_active)
def test_class_string_representation(self):
"""Test class string representation"""
class_obj = Class.objects.create(**self.class_data)
self.assertEqual(str(class_obj), f"{class_obj.class_name} ({class_obj.class_code})")
def test_class_duration(self):
"""Test class duration calculation"""
class_obj = Class.objects.create(**self.class_data)
# Duration should be 90 minutes
self.assertEqual(class_obj.duration, 90)
def test_class_grade_validation(self):
"""Test class grade validation"""
# Test valid grades
valid_grades = ['Form 1', 'Form 2', 'Form 3', 'Form 4', 'Form 5', 'Form 6']
for grade in valid_grades:
data = self.class_data.copy()
data['grade'] = grade
class_obj = Class.objects.create(**data)
self.assertEqual(class_obj.grade, grade)
# Test invalid grade
invalid_data = self.class_data.copy()
invalid_data['grade'] = 'Form 7'
with self.assertRaises(Exception):
Class.objects.create(**invalid_data)
def test_class_stream_choices(self):
"""Test class stream validation"""
# Test valid streams
valid_streams = ['science', 'arts', 'commerce', 'technical']
for stream in valid_streams:
data = self.class_data.copy()
data['stream'] = stream
class_obj = Class.objects.create(**data)
self.assertEqual(class_obj.stream, stream)
# Test invalid stream
invalid_data = self.class_data.copy()
invalid_data['stream'] = 'invalid_stream'
with self.assertRaises(Exception):
Class.objects.create(**invalid_data)
def test_class_semester_choices(self):
"""Test class semester validation"""
# Test valid semesters
valid_semesters = ['1', '2']
for semester in valid_semesters:
data = self.class_data.copy()
data['semester'] = semester
class_obj = Class.objects.create(**data)
self.assertEqual(class_obj.semester, semester)
# Test invalid semester
invalid_data = self.class_data.copy()
invalid_data['semester'] = '3'
with self.assertRaises(Exception):
Class.objects.create(**invalid_data)
def test_class_schedule_validation(self):
"""Test class schedule validation"""
# Valid schedule
class_obj = Class.objects.create(**self.class_data)
self.assertEqual(class_obj.schedule_days, self.class_data['schedule_days'])
self.assertEqual(class_obj.start_time, self.class_data['start_time'])
self.assertEqual(class_obj.end_time, self.class_data['end_time'])
# Invalid time range (end before start)
invalid_data = self.class_data.copy()
invalid_data['start_time'] = time(10, 0)
invalid_data['end_time'] = time(9, 30)
with self.assertRaises(Exception):
Class.objects.create(**invalid_data)
def test_class_student_enrollment(self):
"""Test class student enrollment"""
class_obj = Class.objects.create(**self.class_data)
# Add student to class
class_obj.students.add(self.student)
self.assertIn(self.student, class_obj.students.all())
self.assertEqual(class_obj.students.count(), 1)
def test_class_capacity_validation(self):
"""Test class capacity validation"""
class_obj = Class.objects.create(**self.class_data)
# Test capacity
self.assertEqual(class_obj.max_students, 30)
# Test is_full method
self.assertFalse(class_obj.is_full)
# Add students up to capacity
for i in range(30):
student_data = self.student.__dict__.copy()
student_data['student_id'] = f'S202401{i:04d}'
student_data['first_name'] = f'Student{i}'
student_data.pop('id', None)
student_data.pop('_state', None)
student = Student.objects.create(**student_data)
class_obj.students.add(student)
# Should be full now
self.assertTrue(class_obj.is_full)
def test_class_malaysian_education_features(self):
"""Test Malaysian education specific features"""
class_obj = Class.objects.create(**self.class_data)
self.assertEqual(class_obj.subject, self.class_data['subject'])
self.assertEqual(class_obj.academic_year, self.class_data['academic_year'])
self.assertEqual(class_obj.syllabus, self.class_data['syllabus'])
def test_class_date_validation(self):
"""Test class date validation"""
# Valid date range
class_obj = Class.objects.create(**self.class_data)
self.assertLessEqual(class_obj.start_date, class_obj.end_date)
# Invalid date range (end before start)
invalid_data = self.class_data.copy()
invalid_data['start_date'] = date.today()
invalid_data['end_date'] = date.today() - timedelta(days=1)
with self.assertRaises(Exception):
Class.objects.create(**invalid_data)

View File

@@ -0,0 +1,323 @@
"""
Unit tests for Healthcare Models
Tests for healthcare module models:
- Patient
- Appointment
Author: Claude
"""
import pytest
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from decimal import Decimal
from datetime import date, time, timedelta
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.modules.healthcare.models.patient import Patient
from backend.src.modules.healthcare.models.appointment import Appointment
User = get_user_model()
class PatientModelTest(TestCase):
"""Test cases for Patient model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Healthcare Sdn Bhd',
schema_name='test_healthcare',
domain='testhealthcare.com',
business_type='healthcare'
)
self.user = User.objects.create_user(
username='doctor',
email='doctor@test.com',
password='test123',
tenant=self.tenant,
role='staff'
)
self.patient_data = {
'tenant': self.tenant,
'patient_id': 'P2024010001',
'first_name': 'John',
'last_name': 'Doe',
'ic_number': '000101-01-0001',
'passport_number': '',
'nationality': 'Malaysian',
'gender': 'male',
'date_of_birth': date(1990, 1, 1),
'blood_type': 'O+',
'email': 'john.doe@test.com',
'phone': '+60123456789',
'emergency_contact_name': 'Jane Doe',
'emergency_contact_phone': '+60123456788',
'emergency_contact_relationship': 'Spouse',
'address': '123 Test Street',
'city': 'Kuala Lumpur',
'state': 'KUL',
'postal_code': '50000',
'medical_history': 'No significant medical history',
'allergies': 'None known',
'current_medications': 'None',
'chronic_conditions': 'None',
'last_visit_date': None,
'is_active': True,
'created_by': self.user
}
def test_create_patient(self):
"""Test creating a new patient"""
patient = Patient.objects.create(**self.patient_data)
self.assertEqual(patient.tenant, self.tenant)
self.assertEqual(patient.patient_id, self.patient_data['patient_id'])
self.assertEqual(patient.first_name, self.patient_data['first_name'])
self.assertEqual(patient.last_name, self.patient_data['last_name'])
self.assertEqual(patient.ic_number, self.patient_data['ic_number'])
self.assertEqual(patient.gender, self.patient_data['gender'])
self.assertEqual(patient.blood_type, self.patient_data['blood_type'])
self.assertTrue(patient.is_active)
def test_patient_string_representation(self):
"""Test patient string representation"""
patient = Patient.objects.create(**self.patient_data)
self.assertEqual(str(patient), f"{patient.first_name} {patient.last_name} ({patient.patient_id})")
def test_patient_full_name(self):
"""Test patient full name property"""
patient = Patient.objects.create(**self.patient_data)
self.assertEqual(patient.full_name, f"{patient.first_name} {patient.last_name}")
def test_patient_age(self):
"""Test patient age calculation"""
patient = Patient.objects.create(**self.patient_data)
# Age should be calculated based on date of birth
today = date.today()
expected_age = today.year - patient.date_of_birth.year
if today.month < patient.date_of_birth.month or (today.month == patient.date_of_birth.month and today.day < patient.date_of_birth.day):
expected_age -= 1
self.assertEqual(patient.age, expected_age)
def test_patient_malaysian_ic_validation(self):
"""Test Malaysian IC number validation"""
# Valid IC number
patient = Patient.objects.create(**self.patient_data)
self.assertEqual(patient.ic_number, self.patient_data['ic_number'])
# Invalid IC number format
invalid_data = self.patient_data.copy()
invalid_data['ic_number'] = '123'
with self.assertRaises(Exception):
Patient.objects.create(**invalid_data)
def test_patient_gender_choices(self):
"""Test patient gender validation"""
invalid_data = self.patient_data.copy()
invalid_data['gender'] = 'invalid_gender'
with self.assertRaises(Exception):
Patient.objects.create(**invalid_data)
def test_patient_blood_type_choices(self):
"""Test patient blood type validation"""
invalid_data = self.patient_data.copy()
invalid_data['blood_type'] = 'Z+'
with self.assertRaises(Exception):
Patient.objects.create(**invalid_data)
def test_patient_phone_validation(self):
"""Test Malaysian phone number validation"""
# Valid Malaysian phone number
patient = Patient.objects.create(**self.patient_data)
self.assertEqual(patient.phone, self.patient_data['phone'])
# Invalid phone number
invalid_data = self.patient_data.copy()
invalid_data['phone'] = '12345'
with self.assertRaises(Exception):
Patient.objects.create(**invalid_data)
def test_patient_medical_info_validation(self):
"""Test patient medical information validation"""
# Test with medical conditions
data = self.patient_data.copy()
data['chronic_conditions'] = 'Diabetes, Hypertension'
data['allergies'] = 'Penicillin, Sulfa drugs'
patient = Patient.objects.create(**data)
self.assertEqual(patient.chronic_conditions, 'Diabetes, Hypertension')
self.assertEqual(patient.allergies, 'Penicillin, Sulfa drugs')
class AppointmentModelTest(TestCase):
"""Test cases for Appointment model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Healthcare Sdn Bhd',
schema_name='test_healthcare',
domain='testhealthcare.com',
business_type='healthcare'
)
self.doctor = User.objects.create_user(
username='doctor',
email='doctor@test.com',
password='test123',
tenant=self.tenant,
role='staff'
)
self.patient = Patient.objects.create(
tenant=self.tenant,
patient_id='P2024010001',
first_name='John',
last_name='Doe',
ic_number='000101-01-0001',
gender='male',
date_of_birth=date(1990, 1, 1),
blood_type='O+',
phone='+60123456789',
created_by=self.doctor
)
self.appointment_data = {
'tenant': self.tenant,
'patient': self.patient,
'doctor': self.doctor,
'appointment_number': 'APT-2024010001',
'appointment_date': date.today() + timedelta(days=1),
'appointment_time': time(10, 0),
'end_time': time(10, 30),
'appointment_type': 'consultation',
'status': 'scheduled',
'reason': 'General checkup',
'notes': '',
'is_telemedicine': False,
'telemedicine_link': '',
'reminder_sent': False,
'created_by': self.doctor
}
def test_create_appointment(self):
"""Test creating a new appointment"""
appointment = Appointment.objects.create(**self.appointment_data)
self.assertEqual(appointment.tenant, self.tenant)
self.assertEqual(appointment.patient, self.patient)
self.assertEqual(appointment.doctor, self.doctor)
self.assertEqual(appointment.appointment_number, self.appointment_data['appointment_number'])
self.assertEqual(appointment.status, self.appointment_data['status'])
self.assertEqual(appointment.appointment_type, self.appointment_data['appointment_type'])
self.assertFalse(appointment.is_telemedicine)
def test_appointment_string_representation(self):
"""Test appointment string representation"""
appointment = Appointment.objects.create(**self.appointment_data)
expected = f"{self.patient.full_name} - {appointment.appointment_date} at {appointment.appointment_time}"
self.assertEqual(str(appointment), expected)
def test_appointment_duration(self):
"""Test appointment duration calculation"""
appointment = Appointment.objects.create(**self.appointment_data)
# Duration should be 30 minutes
self.assertEqual(appointment.duration, 30)
def test_appointment_is_upcoming(self):
"""Test appointment upcoming status"""
# Future appointment
appointment = Appointment.objects.create(**self.appointment_data)
self.assertTrue(appointment.is_upcoming)
# Past appointment
appointment.appointment_date = date.today() - timedelta(days=1)
appointment.save()
self.assertFalse(appointment.is_upcoming)
def test_appointment_status_choices(self):
"""Test appointment status validation"""
invalid_data = self.appointment_data.copy()
invalid_data['status'] = 'invalid_status'
with self.assertRaises(Exception):
Appointment.objects.create(**invalid_data)
def test_appointment_type_choices(self):
"""Test appointment type validation"""
invalid_data = self.appointment_data.copy()
invalid_data['appointment_type'] = 'invalid_type'
with self.assertRaises(Exception):
Appointment.objects.create(**invalid_data)
def test_appointment_time_validation(self):
"""Test appointment time validation"""
# Valid time range
appointment = Appointment.objects.create(**self.appointment_data)
self.assertEqual(appointment.appointment_time, self.appointment_data['appointment_time'])
self.assertEqual(appointment.end_time, self.appointment_data['end_time'])
# Invalid time range (end before start)
invalid_data = self.appointment_data.copy()
invalid_data['appointment_time'] = time(11, 0)
invalid_data['end_time'] = time(10, 30)
with self.assertRaises(Exception):
Appointment.objects.create(**invalid_data)
def test_appointment_conflict_detection(self):
"""Test appointment conflict detection"""
# Create first appointment
appointment1 = Appointment.objects.create(**self.appointment_data)
# Try to create conflicting appointment
conflict_data = self.appointment_data.copy()
conflict_data['appointment_number'] = 'APT-2024010002'
conflict_data['appointment_time'] = time(10, 15)
conflict_data['end_time'] = time(10, 45)
# This should not raise an exception but conflict detection should be available
appointment2 = Appointment.objects.create(**conflict_data)
# Check if there's a conflict
self.assertTrue(
appointment1.appointment_date == appointment2.appointment_date and
appointment1.doctor == appointment2.doctor and
(
(appointment1.appointment_time <= appointment2.appointment_time < appointment1.end_time) or
(appointment2.appointment_time <= appointment1.appointment_time < appointment2.end_time)
)
)
def test_telemedicine_appointment(self):
"""Test telemedicine appointment features"""
data = self.appointment_data.copy()
data['is_telemedicine'] = True
data['telemedicine_link'] = 'https://meet.test.com/room/12345'
appointment = Appointment.objects.create(**data)
self.assertTrue(appointment.is_telemedicine)
self.assertEqual(appointment.telemedicine_link, data['telemedicine_link'])
def test_appointment_reminder_features(self):
"""Test appointment reminder features"""
appointment = Appointment.objects.create(**self.appointment_data)
# Initially no reminder sent
self.assertFalse(appointment.reminder_sent)
# Mark reminder as sent
appointment.reminder_sent = True
appointment.reminder_sent_at = timezone.now()
appointment.save()
self.assertTrue(appointment.reminder_sent)
self.assertIsNotNone(appointment.reminder_sent_at)

View File

@@ -0,0 +1,470 @@
"""
Unit tests for Logistics Models
Tests for logistics module models:
- Shipment
- Vehicle
Author: Claude
"""
import pytest
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from decimal import Decimal
from datetime import date, time, timedelta
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.modules.logistics.models.shipment import Shipment
from backend.src.modules.logistics.models.vehicle import Vehicle
User = get_user_model()
class ShipmentModelTest(TestCase):
"""Test cases for Shipment model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Logistics Sdn Bhd',
schema_name='test_logistics',
domain='testlogistics.com',
business_type='logistics'
)
self.user = User.objects.create_user(
username='dispatcher',
email='dispatcher@test.com',
password='test123',
tenant=self.tenant,
role='staff'
)
self.shipment_data = {
'tenant': self.tenant,
'tracking_number': 'TRK-2024010001-MY',
'order_number': 'ORD-2024010001',
'sender_name': 'Test Sender',
'sender_company': 'Test Company',
'sender_phone': '+60123456789',
'sender_email': 'sender@test.com',
'sender_address': '123 Sender Street',
'sender_city': 'Kuala Lumpur',
'sender_state': 'KUL',
'sender_postal_code': '50000',
'receiver_name': 'Test Receiver',
'receiver_company': 'Test Receiver Company',
'receiver_phone': '+60123456788',
'receiver_email': 'receiver@test.com',
'receiver_address': '456 Receiver Street',
'receiver_city': 'Penang',
'receiver_state': 'PNG',
'receiver_postal_code': '10000',
'origin_state': 'KUL',
'destination_state': 'PNG',
'service_type': 'express',
'package_type': 'document',
'weight': Decimal('1.5'),
'length': Decimal('30.0'),
'width': Decimal('20.0'),
'height': Decimal('10.0'),
'declared_value': Decimal('100.00'),
'currency': 'MYR',
'shipping_cost': Decimal('15.00'),
'payment_method': 'cash',
'payment_status': 'paid',
'status': 'processing',
'priority': 'normal',
'special_instructions': 'Handle with care',
'insurance_required': False,
'insurance_amount': Decimal('0.00'),
'estimated_delivery': date.today() + timedelta(days=2),
'actual_delivery': None,
'proof_of_delivery': '',
'delivery_confirmation': False,
'created_by': self.user
}
def test_create_shipment(self):
"""Test creating a new shipment"""
shipment = Shipment.objects.create(**self.shipment_data)
self.assertEqual(shipment.tenant, self.tenant)
self.assertEqual(shipment.tracking_number, self.shipment_data['tracking_number'])
self.assertEqual(shipment.order_number, self.shipment_data['order_number'])
self.assertEqual(shipment.sender_name, self.shipment_data['sender_name'])
self.assertEqual(shipment.receiver_name, self.shipment_data['receiver_name'])
self.assertEqual(shipment.service_type, self.shipment_data['service_type'])
self.assertEqual(shipment.weight, self.shipment_data['weight'])
self.assertEqual(shipment.shipping_cost, self.shipment_data['shipping_cost'])
self.assertEqual(shipment.status, self.shipment_data['status'])
def test_shipment_string_representation(self):
"""Test shipment string representation"""
shipment = Shipment.objects.create(**self.shipment_data)
self.assertEqual(str(shipment), f"{shipment.tracking_number} - {shipment.sender_name} to {shipment.receiver_name}")
def test_shipment_volume_calculation(self):
"""Test shipment volume calculation"""
shipment = Shipment.objects.create(**self.shipment_data)
# Volume = length × width × height (in cm)
expected_volume = Decimal('6000.0') # 30.0 × 20.0 × 10.0
self.assertEqual(shipment.volume, expected_volume)
def test_shipment_delivery_status(self):
"""Test shipment delivery status"""
shipment = Shipment.objects.create(**self.shipment_data)
# Not delivered yet
self.assertFalse(shipment.is_delivered)
# Mark as delivered
shipment.status = 'delivered'
shipment.actual_delivery = date.today()
shipment.delivery_confirmation = True
shipment.save()
self.assertTrue(shipment.is_delivered)
def test_shipment_delayed_status(self):
"""Test shipment delayed status"""
shipment = Shipment.objects.create(**self.shipment_data)
# Not delayed (estimated delivery is in future)
self.assertFalse(shipment.is_delayed)
# Mark as delayed (past estimated delivery)
shipment.estimated_delivery = date.today() - timedelta(days=1)
shipment.status = 'in_transit'
shipment.save()
self.assertTrue(shipment.is_delayed)
def test_shipment_service_type_choices(self):
"""Test shipment service type validation"""
invalid_data = self.shipment_data.copy()
invalid_data['service_type'] = 'invalid_service'
with self.assertRaises(Exception):
Shipment.objects.create(**invalid_data)
def test_shipment_package_type_choices(self):
"""Test shipment package type validation"""
invalid_data = self.shipment_data.copy()
invalid_data['package_type'] = 'invalid_package'
with self.assertRaises(Exception):
Shipment.objects.create(**invalid_data)
def test_shipment_status_choices(self):
"""Test shipment status validation"""
invalid_data = self.shipment_data.copy()
invalid_data['status'] = 'invalid_status'
with self.assertRaises(Exception):
Shipment.objects.create(**invalid_data)
def test_shipment_priority_choices(self):
"""Test shipment priority validation"""
invalid_data = self.shipment_data.copy()
invalid_data['priority'] = 'invalid_priority'
with self.assertRaises(Exception):
Shipment.objects.create(**invalid_data)
def test_shipment_malaysian_phone_validation(self):
"""Test Malaysian phone number validation"""
# Valid Malaysian phone numbers
shipment = Shipment.objects.create(**self.shipment_data)
self.assertEqual(shipment.sender_phone, self.shipment_data['sender_phone'])
self.assertEqual(shipment.receiver_phone, self.shipment_data['receiver_phone'])
# Invalid sender phone
invalid_data = self.shipment_data.copy()
invalid_data['sender_phone'] = '12345'
with self.assertRaises(Exception):
Shipment.objects.create(**invalid_data)
# Invalid receiver phone
invalid_data = self.shipment_data.copy()
invalid_data['receiver_phone'] = '67890'
with self.assertRaises(Exception):
Shipment.objects.create(**invalid_data)
def test_shipment_malaysian_state_validation(self):
"""Test Malaysian state validation"""
# Valid Malaysian states
shipment = Shipment.objects.create(**self.shipment_data)
self.assertEqual(shipment.sender_state, self.shipment_data['sender_state'])
self.assertEqual(shipment.receiver_state, self.shipment_data['receiver_state'])
# Invalid sender state
invalid_data = self.shipment_data.copy()
invalid_data['sender_state'] = 'XX'
with self.assertRaises(Exception):
Shipment.objects.create(**invalid_data)
def test_shipment_weight_validation(self):
"""Test shipment weight validation"""
# Valid weight
shipment = Shipment.objects.create(**self.shipment_data)
self.assertEqual(shipment.weight, self.shipment_data['weight'])
# Invalid weight (negative)
invalid_data = self.shipment_data.copy()
invalid_data['weight'] = Decimal('-1.0')
with self.assertRaises(Exception):
Shipment.objects.create(**invalid_data)
def test_shipment_tracking_number_format(self):
"""Test shipment tracking number format"""
shipment = Shipment.objects.create(**self.shipment_data)
# Should end with -MY for Malaysia
self.assertTrue(shipment.tracking_number.endswith('-MY'))
# Should be unique
with self.assertRaises(Exception):
Shipment.objects.create(**self.shipment_data)
def test_shipment_dimensions_validation(self):
"""Test shipment dimensions validation"""
# Valid dimensions
shipment = Shipment.objects.create(**self.shipment_data)
self.assertEqual(shipment.length, self.shipment_data['length'])
self.assertEqual(shipment.width, self.shipment_data['width'])
self.assertEqual(shipment.height, self.shipment_data['height'])
# Invalid dimensions (negative)
invalid_data = self.shipment_data.copy()
invalid_data['length'] = Decimal('-1.0')
with self.assertRaises(Exception):
Shipment.objects.create(**invalid_data)
class VehicleModelTest(TestCase):
"""Test cases for Vehicle model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Logistics Sdn Bhd',
schema_name='test_logistics',
domain='testlogistics.com',
business_type='logistics'
)
self.user = User.objects.create_user(
username='manager',
email='manager@test.com',
password='test123',
tenant=self.tenant,
role='admin'
)
self.vehicle_data = {
'tenant': self.tenant,
'vehicle_number': 'V1234',
'registration_number': 'WAB1234',
'vehicle_type': 'van',
'make': 'Toyota',
'model': 'Hiace',
'year': 2020,
'color': 'White',
'chassis_number': 'MR0HE3CD5L123456',
'engine_number': '2TR123456',
'capacity': 1000, # kg
'volume_capacity': 10.0, # cubic meters
'fuel_type': 'petrol',
'fuel_capacity': 70, # liters
'current_fuel': 50, # liters
'purchase_date': date(2020, 1, 1),
'purchase_price': Decimal('120000.00'),
'insurance_policy': 'INS-2024-001234',
'insurance_expiry': date.today() + timedelta(days=365),
'road_tax_expiry': date.today() + timedelta(days=180),
'inspection_expiry': date.today() + timedelta(days=90),
'current_mileage': 50000,
'last_service_mileage': 45000,
'next_service_mileage': 55000,
'status': 'active',
'assigned_driver': None,
'gps_device_id': 'GPS001234',
'is_active': True,
'notes': 'Well-maintained vehicle',
'created_by': self.user
}
def test_create_vehicle(self):
"""Test creating a new vehicle"""
vehicle = Vehicle.objects.create(**self.vehicle_data)
self.assertEqual(vehicle.tenant, self.tenant)
self.assertEqual(vehicle.vehicle_number, self.vehicle_data['vehicle_number'])
self.assertEqual(vehicle.registration_number, self.vehicle_data['registration_number'])
self.assertEqual(vehicle.vehicle_type, self.vehicle_data['vehicle_type'])
self.assertEqual(vehicle.make, self.vehicle_data['make'])
self.assertEqual(vehicle.model, self.vehicle_data['model'])
self.assertEqual(vehicle.year, self.vehicle_data['year'])
self.assertEqual(vehicle.capacity, self.vehicle_data['capacity'])
self.assertEqual(vehicle.status, self.vehicle_data['status'])
self.assertTrue(vehicle.is_active)
def test_vehicle_string_representation(self):
"""Test vehicle string representation"""
vehicle = Vehicle.objects.create(**self.vehicle_data)
self.assertEqual(str(vehicle), f"{vehicle.make} {vehicle.model} ({vehicle.registration_number})")
def test_vehicle_age(self):
"""Test vehicle age calculation"""
vehicle = Vehicle.objects.create(**self.vehicle_data)
# Age should be calculated based on purchase date
today = date.today()
expected_age = today.year - vehicle.purchase_date.year
if today.month < vehicle.purchase_date.month or (today.month == vehicle.purchase_date.month and today.day < vehicle.purchase_date.day):
expected_age -= 1
self.assertEqual(vehicle.age, expected_age)
def test_vehicle_service_due(self):
"""Test vehicle service due status"""
vehicle = Vehicle.objects.create(**self.vehicle_data)
# Service not due yet
self.assertFalse(vehicle.service_due)
# Mark as service due
vehicle.current_mileage = 56000
vehicle.save()
self.assertTrue(vehicle.service_due)
def test_vehicle_insurance_expiry_status(self):
"""Test vehicle insurance expiry status"""
vehicle = Vehicle.objects.create(**self.vehicle_data)
# Insurance not expired
self.assertFalse(vehicle.insurance_expired)
# Mark as expired
vehicle.insurance_expiry = date.today() - timedelta(days=1)
vehicle.save()
self.assertTrue(vehicle.insurance_expired)
def test_vehicle_road_tax_expiry_status(self):
"""Test vehicle road tax expiry status"""
vehicle = Vehicle.objects.create(**self.vehicle_data)
# Road tax not expired
self.assertFalse(vehicle.road_tax_expired)
# Mark as expired
vehicle.road_tax_expiry = date.today() - timedelta(days=1)
vehicle.save()
self.assertTrue(vehicle.road_tax_expired)
def test_vehicle_inspection_expiry_status(self):
"""Test vehicle inspection expiry status"""
vehicle = Vehicle.objects.create(**self.vehicle_data)
# Inspection not expired
self.assertFalse(vehicle.inspection_expired)
# Mark as expired
vehicle.inspection_expiry = date.today() - timedelta(days=1)
vehicle.save()
self.assertTrue(vehicle.inspection_expired)
def test_vehicle_type_choices(self):
"""Test vehicle type validation"""
invalid_data = self.vehicle_data.copy()
invalid_data['vehicle_type'] = 'invalid_type'
with self.assertRaises(Exception):
Vehicle.objects.create(**invalid_data)
def test_vehicle_fuel_type_choices(self):
"""Test vehicle fuel type validation"""
invalid_data = self.vehicle_data.copy()
invalid_data['fuel_type'] = 'invalid_fuel'
with self.assertRaises(Exception):
Vehicle.objects.create(**invalid_data)
def test_vehicle_status_choices(self):
"""Test vehicle status validation"""
invalid_data = self.vehicle_data.copy()
invalid_data['status'] = 'invalid_status'
with self.assertRaises(Exception):
Vehicle.objects.create(**invalid_data)
def test_vehicle_malaysian_registration_validation(self):
"""Test Malaysian vehicle registration validation"""
# Valid registration number
vehicle = Vehicle.objects.create(**self.vehicle_data)
self.assertEqual(vehicle.registration_number, self.vehicle_data['registration_number'])
# Invalid registration number format
invalid_data = self.vehicle_data.copy()
invalid_data['registration_number'] = 'ABC123'
with self.assertRaises(Exception):
Vehicle.objects.create(**invalid_data)
def test_vehicle_mileage_validation(self):
"""Test vehicle mileage validation"""
# Valid mileage
vehicle = Vehicle.objects.create(**self.vehicle_data)
self.assertEqual(vehicle.current_mileage, self.vehicle_data['current_mileage'])
# Invalid mileage (negative)
invalid_data = self.vehicle_data.copy()
invalid_data['current_mileage'] = -1000
with self.assertRaises(Exception):
Vehicle.objects.create(**invalid_data)
def test_vehicle_capacity_validation(self):
"""Test vehicle capacity validation"""
# Valid capacity
vehicle = Vehicle.objects.create(**self.vehicle_data)
self.assertEqual(vehicle.capacity, self.vehicle_data['capacity'])
# Invalid capacity (negative)
invalid_data = self.vehicle_data.copy()
invalid_data['capacity'] = -100
with self.assertRaises(Exception):
Vehicle.objects.create(**invalid_data)
def test_vehicle_year_validation(self):
"""Test vehicle year validation"""
# Valid year
vehicle = Vehicle.objects.create(**self.vehicle_data)
self.assertEqual(vehicle.year, self.vehicle_data['year'])
# Invalid year (too old)
invalid_data = self.vehicle_data.copy()
invalid_data['year'] = 1950
with self.assertRaises(Exception):
Vehicle.objects.create(**invalid_data)
def test_vehicle_fuel_level_validation(self):
"""Test vehicle fuel level validation"""
# Valid fuel level
vehicle = Vehicle.objects.create(**self.vehicle_data)
self.assertEqual(vehicle.current_fuel, self.vehicle_data['current_fuel'])
# Invalid fuel level (negative)
invalid_data = self.vehicle_data.copy()
invalid_data['current_fuel'] = -10
with self.assertRaises(Exception):
Vehicle.objects.create(**invalid_data)
# Invalid fuel level (exceeds capacity)
invalid_data = self.vehicle_data.copy()
invalid_data['current_fuel'] = 100
with self.assertRaises(Exception):
Vehicle.objects.create(**invalid_data)

View File

@@ -0,0 +1,350 @@
"""
Unit tests for Retail Models
Tests for retail module models:
- Product
- Sale
Author: Claude
"""
import pytest
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from decimal import Decimal
from datetime import date
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.modules.retail.models.product import Product
from backend.src.modules.retail.models.sale import Sale, SaleItem
User = get_user_model()
class ProductModelTest(TestCase):
"""Test cases for Product model"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Business Sdn Bhd',
schema_name='test_business',
domain='testbusiness.com',
business_type='retail'
)
self.user = User.objects.create_user(
username='testuser',
email='user@test.com',
password='test123',
tenant=self.tenant
)
self.product_data = {
'tenant': self.tenant,
'sku': 'PRD-001',
'name': 'Test Product',
'description': 'A test product for unit testing',
'category': 'electronics',
'brand': 'Test Brand',
'barcode': '1234567890123',
'unit': 'piece',
'current_stock': 100,
'minimum_stock': 10,
'maximum_stock': 500,
'reorder_point': 15,
'purchase_price': Decimal('50.00'),
'selling_price': Decimal('100.00'),
'wholesale_price': Decimal('80.00'),
'tax_rate': 10.0,
'is_taxable': True,
'is_active': True,
'requires_prescription': False,
'is_halal': True,
'msme_certified': True,
'created_by': self.user
}
def test_create_product(self):
"""Test creating a new product"""
product = Product.objects.create(**self.product_data)
self.assertEqual(product.tenant, self.tenant)
self.assertEqual(product.sku, self.product_data['sku'])
self.assertEqual(product.name, self.product_data['name'])
self.assertEqual(product.current_stock, self.product_data['current_stock'])
self.assertEqual(product.purchase_price, self.product_data['purchase_price'])
self.assertEqual(product.selling_price, self.product_data['selling_price'])
self.assertTrue(product.is_active)
self.assertTrue(product.is_halal)
def test_product_string_representation(self):
"""Test product string representation"""
product = Product.objects.create(**self.product_data)
self.assertEqual(str(product), f"{product.name} ({product.sku})")
def test_product_is_low_stock(self):
"""Test product low stock detection"""
product = Product.objects.create(**self.product_data)
# Normal stock level
self.assertFalse(product.is_low_stock)
# Low stock level
product.current_stock = 5
product.save()
self.assertTrue(product.is_low_stock)
def test_product_profit_margin(self):
"""Test product profit margin calculation"""
product = Product.objects.create(**self.product_data)
expected_margin = ((product.selling_price - product.purchase_price) / product.selling_price) * 100
self.assertAlmostEqual(product.profit_margin, expected_margin)
def test_product_category_choices(self):
"""Test product category validation"""
invalid_data = self.product_data.copy()
invalid_data['category'] = 'invalid_category'
with self.assertRaises(Exception):
Product.objects.create(**invalid_data)
def test_product_unit_choices(self):
"""Test product unit validation"""
invalid_data = self.product_data.copy()
invalid_data['unit'] = 'invalid_unit'
with self.assertRaises(Exception):
Product.objects.create(**invalid_data)
def test_product_barcode_validation(self):
"""Test product barcode validation"""
# Valid barcode
product = Product.objects.create(**self.product_data)
self.assertEqual(product.barcode, self.product_data['barcode'])
# Invalid barcode (too long)
invalid_data = self.product_data.copy()
invalid_data['barcode'] = '1' * 14
with self.assertRaises(Exception):
Product.objects.create(**invalid_data)
def test_product_stock_validation(self):
"""Test product stock validation"""
invalid_data = self.product_data.copy()
invalid_data['current_stock'] = -1
with self.assertRaises(Exception):
Product.objects.create(**invalid_data)
invalid_data['current_stock'] = 0
invalid_data['minimum_stock'] = -5
with self.assertRaises(Exception):
Product.objects.create(**invalid_data)
class SaleModelTest(TestCase):
"""Test cases for Sale and SaleItem models"""
def setUp(self):
self.tenant = Tenant.objects.create(
name='Test Business Sdn Bhd',
schema_name='test_business',
domain='testbusiness.com',
business_type='retail'
)
self.user = User.objects.create_user(
username='testuser',
email='user@test.com',
password='test123',
tenant=self.tenant
)
self.product1 = Product.objects.create(
tenant=self.tenant,
sku='PRD-001',
name='Product 1',
category='electronics',
unit='piece',
current_stock=100,
minimum_stock=10,
purchase_price=Decimal('50.00'),
selling_price=Decimal('100.00'),
tax_rate=10.0,
created_by=self.user
)
self.product2 = Product.objects.create(
tenant=self.tenant,
sku='PRD-002',
name='Product 2',
category='electronics',
unit='piece',
current_stock=50,
minimum_stock=5,
purchase_price=Decimal('30.00'),
selling_price=Decimal('60.00'),
tax_rate=10.0,
created_by=self.user
)
self.sale_data = {
'tenant': self.tenant,
'invoice_number': 'INV-2024010001',
'customer_name': 'Test Customer',
'customer_email': 'customer@test.com',
'customer_phone': '+60123456789',
'customer_ic': '000101-01-0001',
'sale_date': timezone.now(),
'status': 'completed',
'payment_method': 'cash',
'payment_status': 'paid',
'sales_person': self.user,
'notes': 'Test sale for unit testing'
}
def test_create_sale(self):
"""Test creating a new sale"""
sale = Sale.objects.create(**self.sale_data)
self.assertEqual(sale.tenant, self.tenant)
self.assertEqual(sale.invoice_number, self.sale_data['invoice_number'])
self.assertEqual(sale.customer_name, self.sale_data['customer_name'])
self.assertEqual(sale.status, self.sale_data['status'])
self.assertEqual(sale.payment_status, self.sale_data['payment_status'])
self.assertEqual(sale.sales_person, self.user)
def test_sale_string_representation(self):
"""Test sale string representation"""
sale = Sale.objects.create(**self.sale_data)
self.assertEqual(str(sale), f"Invoice #{sale.invoice_number} - {sale.customer_name}")
def test_create_sale_item(self):
"""Test creating a sale item"""
sale = Sale.objects.create(**self.sale_data)
sale_item_data = {
'sale': sale,
'product': self.product1,
'quantity': 2,
'unit_price': Decimal('100.00'),
'discount_percentage': 0.0,
'tax_rate': 10.0,
'notes': 'Test sale item'
}
sale_item = SaleItem.objects.create(**sale_item_data)
self.assertEqual(sale_item.sale, sale)
self.assertEqual(sale_item.product, self.product1)
self.assertEqual(sale_item.quantity, 2)
self.assertEqual(sale_item.unit_price, Decimal('100.00'))
def test_sale_item_subtotal(self):
"""Test sale item subtotal calculation"""
sale = Sale.objects.create(**self.sale_data)
sale_item = SaleItem.objects.create(
sale=sale,
product=self.product1,
quantity=2,
unit_price=Decimal('100.00'),
tax_rate=10.0
)
expected_subtotal = Decimal('200.00') # 2 * 100.00
self.assertEqual(sale_item.subtotal, expected_subtotal)
def test_sale_item_tax_amount(self):
"""Test sale item tax amount calculation"""
sale = Sale.objects.create(**self.sale_data)
sale_item = SaleItem.objects.create(
sale=sale,
product=self.product1,
quantity=2,
unit_price=Decimal('100.00'),
tax_rate=10.0
)
expected_tax = Decimal('20.00') # 200.00 * 0.10
self.assertEqual(sale_item.tax_amount, expected_tax)
def test_sale_item_total_amount(self):
"""Test sale item total amount calculation"""
sale = Sale.objects.create(**self.sale_data)
sale_item = SaleItem.objects.create(
sale=sale,
product=self.product1,
quantity=2,
unit_price=Decimal('100.00'),
tax_rate=10.0
)
expected_total = Decimal('220.00') # 200.00 + 20.00
self.assertEqual(sale_item.total_amount, expected_total)
def test_sale_calculate_totals(self):
"""Test sale total calculations"""
sale = Sale.objects.create(**self.sale_data)
# Create multiple sale items
SaleItem.objects.create(
sale=sale,
product=self.product1,
quantity=2,
unit_price=Decimal('100.00'),
tax_rate=10.0
)
SaleItem.objects.create(
sale=sale,
product=self.product2,
quantity=1,
unit_price=Decimal('60.00'),
tax_rate=10.0
)
# Test the calculate_totals method
sale.calculate_totals()
expected_subtotal = Decimal('260.00') # 200.00 + 60.00
expected_tax = Decimal('26.00') # 20.00 + 6.00
expected_total = Decimal('286.00') # 260.00 + 26.00
self.assertEqual(sale.subtotal_amount, expected_subtotal)
self.assertEqual(sale.tax_amount, expected_tax)
self.assertEqual(sale.total_amount, expected_total)
def test_sale_status_choices(self):
"""Test sale status validation"""
invalid_data = self.sale_data.copy()
invalid_data['status'] = 'invalid_status'
with self.assertRaises(Exception):
Sale.objects.create(**invalid_data)
def test_sale_payment_method_choices(self):
"""Test sale payment method validation"""
invalid_data = self.sale_data.copy()
invalid_data['payment_method'] = 'invalid_method'
with self.assertRaises(Exception):
Sale.objects.create(**invalid_data)
def test_malaysian_customer_validation(self):
"""Test Malaysian customer validation"""
# Valid Malaysian IC
sale = Sale.objects.create(**self.sale_data)
self.assertEqual(sale.customer_ic, self.sale_data['customer_ic'])
# Valid Malaysian phone
self.assertEqual(sale.customer_phone, self.sale_data['customer_phone'])
# Invalid phone number
invalid_data = self.sale_data.copy()
invalid_data['customer_phone'] = '12345'
with self.assertRaises(Exception):
Sale.objects.create(**invalid_data)

View File

View File

@@ -0,0 +1,638 @@
"""
Unit tests for Core Services
Tests for all core services:
- TenantService
- UserService
- SubscriptionService
- ModuleService
- PaymentService
Author: Claude
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from decimal import Decimal
from datetime import date, timedelta
from backend.src.core.models.tenant import Tenant
from backend.src.core.models.user import User
from backend.src.core.models.subscription import Subscription
from backend.src.core.models.module import Module
from backend.src.core.models.payment import PaymentTransaction
from backend.src.core.services.tenant_service import TenantService
from backend.src.core.services.user_service import UserService
from backend.src.core.services.subscription_service import SubscriptionService
from backend.src.core.services.module_service import ModuleService
from backend.src.core.services.payment_service import PaymentService
User = get_user_model()
class TenantServiceTest(TestCase):
"""Test cases for TenantService"""
def setUp(self):
self.service = TenantService()
self.tenant_data = {
'name': 'Test Business Sdn Bhd',
'schema_name': 'test_business',
'domain': 'testbusiness.com',
'business_type': 'retail',
'registration_number': '202401000001',
'tax_id': 'MY123456789',
'contact_email': 'contact@testbusiness.com',
'contact_phone': '+60123456789',
'address': '123 Test Street',
'city': 'Kuala Lumpur',
'state': 'KUL',
'postal_code': '50000'
}
def test_create_tenant_success(self):
"""Test successful tenant creation"""
tenant = self.service.create_tenant(self.tenant_data)
self.assertEqual(tenant.name, self.tenant_data['name'])
self.assertEqual(tenant.schema_name, self.tenant_data['schema_name'])
self.assertTrue(tenant.is_active)
self.assertEqual(tenant.subscription_tier, 'free')
def test_create_tenant_invalid_data(self):
"""Test tenant creation with invalid data"""
invalid_data = self.tenant_data.copy()
invalid_data['name'] = '' # Empty name
with self.assertRaises(Exception):
self.service.create_tenant(invalid_data)
def test_get_tenant_by_id(self):
"""Test getting tenant by ID"""
tenant = self.service.create_tenant(self.tenant_data)
retrieved_tenant = self.service.get_tenant_by_id(tenant.id)
self.assertEqual(retrieved_tenant, tenant)
def test_get_tenant_by_schema_name(self):
"""Test getting tenant by schema name"""
tenant = self.service.create_tenant(self.tenant_data)
retrieved_tenant = self.service.get_tenant_by_schema_name(tenant.schema_name)
self.assertEqual(retrieved_tenant, tenant)
def test_update_tenant(self):
"""Test updating tenant information"""
tenant = self.service.create_tenant(self.tenant_data)
update_data = {'name': 'Updated Business Name'}
updated_tenant = self.service.update_tenant(tenant.id, update_data)
self.assertEqual(updated_tenant.name, 'Updated Business Name')
def test_activate_tenant(self):
"""Test tenant activation"""
tenant = self.service.create_tenant(self.tenant_data)
tenant.is_active = False
tenant.save()
activated_tenant = self.service.activate_tenant(tenant.id)
self.assertTrue(activated_tenant.is_active)
def test_deactivate_tenant(self):
"""Test tenant deactivation"""
tenant = self.service.create_tenant(self.tenant_data)
deactivated_tenant = self.service.deactivate_tenant(tenant.id)
self.assertFalse(deactivated_tenant.is_active)
def test_get_tenant_statistics(self):
"""Test getting tenant statistics"""
tenant = self.service.create_tenant(self.tenant_data)
stats = self.service.get_tenant_statistics(tenant.id)
self.assertIn('total_users', stats)
self.assertIn('active_subscriptions', stats)
self.assertIn('total_modules', stats)
class UserServiceTest(TestCase):
"""Test cases for UserService"""
def setUp(self):
self.service = UserService()
self.tenant = Tenant.objects.create(
name='Test Business Sdn Bhd',
schema_name='test_business',
domain='testbusiness.com',
business_type='retail'
)
def test_create_user_success(self):
"""Test successful user creation"""
user_data = {
'username': 'testuser',
'email': 'user@test.com',
'password': 'test123',
'first_name': 'Test',
'last_name': 'User',
'phone': '+60123456789',
'tenant': self.tenant,
'role': 'staff'
}
user = self.service.create_user(user_data)
self.assertEqual(user.username, user_data['username'])
self.assertEqual(user.email, user_data['email'])
self.assertEqual(user.tenant, self.tenant)
self.assertTrue(user.check_password('test123'))
def test_create_superuser(self):
"""Test superuser creation"""
user_data = {
'username': 'admin',
'email': 'admin@test.com',
'password': 'admin123',
'first_name': 'Admin',
'last_name': 'User'
}
user = self.service.create_superuser(user_data)
self.assertTrue(user.is_staff)
self.assertTrue(user.is_superuser)
self.assertEqual(user.role, 'admin')
def test_authenticate_user_success(self):
"""Test successful user authentication"""
user_data = {
'username': 'testuser',
'email': 'user@test.com',
'password': 'test123',
'tenant': self.tenant,
'role': 'staff'
}
self.service.create_user(user_data)
authenticated_user = self.service.authenticate_user(
user_data['username'],
user_data['password']
)
self.assertIsNotNone(authenticated_user)
self.assertEqual(authenticated_user.username, user_data['username'])
def test_authenticate_user_failure(self):
"""Test failed user authentication"""
user_data = {
'username': 'testuser',
'email': 'user@test.com',
'password': 'test123',
'tenant': self.tenant,
'role': 'staff'
}
self.service.create_user(user_data)
authenticated_user = self.service.authenticate_user(
user_data['username'],
'wrongpassword'
)
self.assertIsNone(authenticated_user)
def test_update_user_profile(self):
"""Test updating user profile"""
user_data = {
'username': 'testuser',
'email': 'user@test.com',
'password': 'test123',
'tenant': self.tenant,
'role': 'staff'
}
user = self.service.create_user(user_data)
update_data = {'first_name': 'Updated', 'last_name': 'Name'}
updated_user = self.service.update_user(user.id, update_data)
self.assertEqual(updated_user.first_name, 'Updated')
self.assertEqual(updated_user.last_name, 'Name')
def test_change_password(self):
"""Test changing user password"""
user_data = {
'username': 'testuser',
'email': 'user@test.com',
'password': 'test123',
'tenant': self.tenant,
'role': 'staff'
}
user = self.service.create_user(user_data)
success = self.service.change_password(user.id, 'newpassword123')
self.assertTrue(success)
self.assertTrue(user.check_password('newpassword123'))
def test_deactivate_user(self):
"""Test user deactivation"""
user_data = {
'username': 'testuser',
'email': 'user@test.com',
'password': 'test123',
'tenant': self.tenant,
'role': 'staff'
}
user = self.service.create_user(user_data)
deactivated_user = self.service.deactivate_user(user.id)
self.assertFalse(deactivated_user.is_active)
def test_get_users_by_tenant(self):
"""Test getting users by tenant"""
user_data = {
'username': 'testuser',
'email': 'user@test.com',
'password': 'test123',
'tenant': self.tenant,
'role': 'staff'
}
self.service.create_user(user_data)
users = self.service.get_users_by_tenant(self.tenant.id)
self.assertEqual(len(users), 1)
self.assertEqual(users[0].username, user_data['username'])
class SubscriptionServiceTest(TestCase):
"""Test cases for SubscriptionService"""
def setUp(self):
self.service = SubscriptionService()
self.tenant = Tenant.objects.create(
name='Test Business Sdn Bhd',
schema_name='test_business',
domain='testbusiness.com',
business_type='retail'
)
def test_create_subscription_success(self):
"""Test successful subscription creation"""
subscription_data = {
'tenant': self.tenant,
'plan': 'premium',
'amount': Decimal('299.00'),
'currency': 'MYR',
'billing_cycle': 'monthly',
'start_date': date.today(),
'end_date': date.today() + timedelta(days=30)
}
subscription = self.service.create_subscription(subscription_data)
self.assertEqual(subscription.tenant, self.tenant)
self.assertEqual(subscription.plan, 'premium')
self.assertEqual(subscription.status, 'active')
def test_upgrade_subscription(self):
"""Test subscription upgrade"""
subscription_data = {
'tenant': self.tenant,
'plan': 'basic',
'amount': Decimal('99.00'),
'currency': 'MYR',
'billing_cycle': 'monthly',
'start_date': date.today(),
'end_date': date.today() + timedelta(days=30)
}
subscription = self.service.create_subscription(subscription_data)
upgraded_subscription = self.service.upgrade_subscription(
subscription.id,
'premium',
Decimal('299.00')
)
self.assertEqual(upgraded_subscription.plan, 'premium')
self.assertEqual(upgraded_subscription.amount, Decimal('299.00'))
def test_cancel_subscription(self):
"""Test subscription cancellation"""
subscription_data = {
'tenant': self.tenant,
'plan': 'premium',
'amount': Decimal('299.00'),
'currency': 'MYR',
'billing_cycle': 'monthly',
'start_date': date.today(),
'end_date': date.today() + timedelta(days=30)
}
subscription = self.service.create_subscription(subscription_data)
cancelled_subscription = self.service.cancel_subscription(subscription.id)
self.assertEqual(cancelled_subscription.status, 'cancelled')
def test_renew_subscription(self):
"""Test subscription renewal"""
subscription_data = {
'tenant': self.tenant,
'plan': 'premium',
'amount': Decimal('299.00'),
'currency': 'MYR',
'billing_cycle': 'monthly',
'start_date': date.today() - timedelta(days=29),
'end_date': date.today() + timedelta(days=1)
}
subscription = self.service.create_subscription(subscription_data)
renewed_subscription = self.service.renew_subscription(subscription.id)
self.assertEqual(renewed_subscription.status, 'active')
self.assertGreater(renewed_subscription.end_date, subscription.end_date)
def test_check_subscription_status(self):
"""Test subscription status check"""
subscription_data = {
'tenant': self.tenant,
'plan': 'premium',
'amount': Decimal('299.00'),
'currency': 'MYR',
'billing_cycle': 'monthly',
'start_date': date.today(),
'end_date': date.today() + timedelta(days=30)
}
subscription = self.service.create_subscription(subscription_data)
status = self.service.check_subscription_status(self.tenant.id)
self.assertTrue(status['is_active'])
self.assertEqual(status['plan'], 'premium')
def test_get_subscription_history(self):
"""Test getting subscription history"""
subscription_data = {
'tenant': self.tenant,
'plan': 'premium',
'amount': Decimal('299.00'),
'currency': 'MYR',
'billing_cycle': 'monthly',
'start_date': date.today(),
'end_date': date.today() + timedelta(days=30)
}
self.service.create_subscription(subscription_data)
history = self.service.get_subscription_history(self.tenant.id)
self.assertEqual(len(history), 1)
self.assertEqual(history[0]['plan'], 'premium')
class ModuleServiceTest(TestCase):
"""Test cases for ModuleService"""
def setUp(self):
self.service = ModuleService()
def test_create_module_success(self):
"""Test successful module creation"""
module_data = {
'name': 'Test Module',
'code': 'test',
'description': 'A test module',
'category': 'industry',
'version': '1.0.0',
'is_active': True,
'config_schema': {'features': ['test']},
'pricing_tier': 'premium'
}
module = self.service.create_module(module_data)
self.assertEqual(module.name, module_data['name'])
self.assertEqual(module.code, module_data['code'])
self.assertTrue(module.is_active)
def test_get_module_by_code(self):
"""Test getting module by code"""
module_data = {
'name': 'Test Module',
'code': 'test',
'description': 'A test module',
'category': 'industry',
'version': '1.0.0',
'is_active': True,
'config_schema': {'features': ['test']},
'pricing_tier': 'premium'
}
module = self.service.create_module(module_data)
retrieved_module = self.service.get_module_by_code('test')
self.assertEqual(retrieved_module, module)
def test_get_modules_by_category(self):
"""Test getting modules by category"""
module_data = {
'name': 'Test Module',
'code': 'test',
'description': 'A test module',
'category': 'industry',
'version': '1.0.0',
'is_active': True,
'config_schema': {'features': ['test']},
'pricing_tier': 'premium'
}
self.service.create_module(module_data)
modules = self.service.get_modules_by_category('industry')
self.assertEqual(len(modules), 1)
self.assertEqual(modules[0].code, 'test')
def test_activate_module(self):
"""Test module activation"""
module_data = {
'name': 'Test Module',
'code': 'test',
'description': 'A test module',
'category': 'industry',
'version': '1.0.0',
'is_active': False,
'config_schema': {'features': ['test']},
'pricing_tier': 'premium'
}
module = self.service.create_module(module_data)
activated_module = self.service.activate_module(module.id)
self.assertTrue(activated_module.is_active)
def test_deactivate_module(self):
"""Test module deactivation"""
module_data = {
'name': 'Test Module',
'code': 'test',
'description': 'A test module',
'category': 'industry',
'version': '1.0.0',
'is_active': True,
'config_schema': {'features': ['test']},
'pricing_tier': 'premium'
}
module = self.service.create_module(module_data)
deactivated_module = self.service.deactivate_module(module.id)
self.assertFalse(deactivated_module.is_active)
def test_check_module_dependencies(self):
"""Test module dependency checking"""
# Create dependent module first
dependent_module_data = {
'name': 'Core Module',
'code': 'core',
'description': 'Core module',
'category': 'core',
'version': '1.0.0',
'is_active': True,
'config_schema': {'features': ['core']},
'pricing_tier': 'free'
}
self.service.create_module(dependent_module_data)
# Create module with dependency
module_data = {
'name': 'Test Module',
'code': 'test',
'description': 'A test module',
'category': 'industry',
'version': '1.0.0',
'is_active': True,
'dependencies': ['core'],
'config_schema': {'features': ['test']},
'pricing_tier': 'premium'
}
module = self.service.create_module(module_data)
dependencies = self.service.check_module_dependencies(module.id)
self.assertTrue(dependencies['dependencies_met'])
self.assertEqual(len(dependencies['dependencies']), 1)
class PaymentServiceTest(TestCase):
"""Test cases for PaymentService"""
def setUp(self):
self.service = PaymentService()
self.tenant = Tenant.objects.create(
name='Test Business Sdn Bhd',
schema_name='test_business',
domain='testbusiness.com',
business_type='retail'
)
self.subscription = Subscription.objects.create(
tenant=self.tenant,
plan='premium',
status='active',
start_date=date.today(),
end_date=date.today() + timedelta(days=30),
amount=Decimal('299.00'),
currency='MYR'
)
@patch('backend.src.core.services.payment_service.PaymentService.process_payment_gateway')
def test_create_payment_success(self, mock_process_payment):
"""Test successful payment creation"""
mock_process_payment.return_value = {'success': True, 'transaction_id': 'TX123456'}
payment_data = {
'tenant': self.tenant,
'subscription': self.subscription,
'amount': Decimal('299.00'),
'currency': 'MYR',
'payment_method': 'fpx',
'description': 'Monthly subscription payment'
}
payment = self.service.create_payment(payment_data)
self.assertEqual(payment.tenant, self.tenant)
self.assertEqual(payment.amount, Decimal('299.00'))
self.assertEqual(payment.status, 'completed')
def test_create_payment_invalid_amount(self):
"""Test payment creation with invalid amount"""
payment_data = {
'tenant': self.tenant,
'subscription': self.subscription,
'amount': Decimal('-100.00'),
'currency': 'MYR',
'payment_method': 'fpx',
'description': 'Invalid payment'
}
with self.assertRaises(Exception):
self.service.create_payment(payment_data)
@patch('backend.src.core.services.payment_service.PaymentService.process_payment_gateway')
def test_process_payment_refund(self, mock_process_payment):
"""Test payment refund processing"""
mock_process_payment.return_value = {'success': True, 'refund_id': 'RF123456'}
payment = PaymentTransaction.objects.create(
tenant=self.tenant,
subscription=self.subscription,
transaction_id='TX123456',
amount=Decimal('299.00'),
currency='MYR',
payment_method='fpx',
status='completed',
payment_date=timezone.now()
)
refund_result = self.service.process_refund(payment.id, Decimal('100.00'))
self.assertTrue(refund_result['success'])
self.assertEqual(refund_result['refund_id'], 'RF123456')
def test_get_payment_history(self):
"""Test getting payment history"""
payment = PaymentTransaction.objects.create(
tenant=self.tenant,
subscription=self.subscription,
transaction_id='TX123456',
amount=Decimal('299.00'),
currency='MYR',
payment_method='fpx',
status='completed',
payment_date=timezone.now()
)
history = self.service.get_payment_history(self.tenant.id)
self.assertEqual(len(history), 1)
self.assertEqual(history[0]['transaction_id'], 'TX123456')
def test_check_payment_status(self):
"""Test checking payment status"""
payment = PaymentTransaction.objects.create(
tenant=self.tenant,
subscription=self.subscription,
transaction_id='TX123456',
amount=Decimal('299.00'),
currency='MYR',
payment_method='fpx',
status='completed',
payment_date=timezone.now()
)
status = self.service.check_payment_status(payment.transaction_id)
self.assertEqual(status['status'], 'completed')
self.assertEqual(status['amount'], Decimal('299.00'))
def test_validate_payment_method(self):
"""Test payment method validation"""
valid_methods = ['fpx', 'credit_card', 'debit_card', 'ewallet', 'cash']
for method in valid_methods:
is_valid = self.service.validate_payment_method(method)
self.assertTrue(is_valid)
invalid_method = 'invalid_method'
is_valid = self.service.validate_payment_method(invalid_method)
self.assertFalse(is_valid)

View File

@@ -0,0 +1,686 @@
"""
Unit tests for caching strategies and managers.
"""
import json
import time
from datetime import datetime, timedelta
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase, override_settings
from django.core.cache import cache
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import connection
from django.http import HttpRequest, HttpResponse
from django.test import RequestFactory
from rest_framework.test import APITestCase
from core.caching.cache_manager import (
CacheManager, CacheKeyGenerator, MalaysianDataCache,
QueryCache, TenantCacheManager, CacheWarmer
)
from core.caching.strategies import (
WriteThroughCache, WriteBehindCache, ReadThroughCache,
RefreshAheadCache, CacheAsidePattern, MultiLevelCache,
MalaysianCacheStrategies, CacheEvictionPolicy,
cache_view_response, cache_query_results
)
from core.caching.django_integration import (
TenantCacheMiddleware, CacheMiddleware, DatabaseCacheMiddleware,
MalaysianCacheMiddleware, get_cache_config
)
from core.caching.config import CacheConfig
User = get_user_model()
class CacheKeyGeneratorTest(TestCase):
"""Test cache key generation."""
def setUp(self):
self.generator = CacheKeyGenerator()
def test_generate_basic_key(self):
"""Test basic key generation."""
key = self.generator.generate_key("test", "123")
self.assertIn("my_sme", key)
self.assertIn("test", key)
self.assertIn("123", key)
def test_generate_key_with_context(self):
"""Test key generation with context."""
context = {"filter": "active", "sort": "name"}
key = self.generator.generate_key("test", "123", context=context)
self.assertIn("my_sme", key)
self.assertIn("test", key)
self.assertIn("123", key)
def test_generate_malaysian_key(self):
"""Test Malaysian-specific key generation."""
key = self.generator.generate_malaysian_key("ic", "1234567890")
self.assertIn("my_sme", key)
self.assertIn("ic_1234567890", key)
self.assertIn("my", key)
def test_tenant_prefix_inclusion(self):
"""Test tenant prefix inclusion in keys."""
key = self.generator.generate_key("test", "123")
self.assertIn("tenant_", key)
class CacheManagerTest(TestCase):
"""Test cache manager operations."""
def setUp(self):
self.manager = CacheManager()
def test_set_and_get(self):
"""Test basic set and get operations."""
key = "test_key"
value = {"data": "test_value"}
result = self.manager.set(key, value)
self.assertTrue(result)
retrieved = self.manager.get(key)
self.assertEqual(retrieved, value)
def test_get_default_value(self):
"""Test get with default value."""
key = "nonexistent_key"
default = {"default": "value"}
result = self.manager.get(key, default)
self.assertEqual(result, default)
def test_delete_key(self):
"""Test key deletion."""
key = "test_key"
value = "test_value"
self.manager.set(key, value)
result = self.manager.delete(key)
self.assertTrue(result)
retrieved = self.manager.get(key)
self.assertIsNone(retrieved)
def test_clear_tenant_cache(self):
"""Test tenant cache clearing."""
result = self.manager.clear_tenant_cache()
self.assertTrue(result)
def test_get_cache_stats(self):
"""Test cache statistics."""
stats = self.manager.get_cache_stats()
self.assertIn("tenant", stats)
self.assertIn("redis_available", stats)
self.assertIn("default_timeout", stats)
@patch('core.caching.cache_manager.get_redis_connection')
def test_redis_connection_failure(self, mock_get_redis):
"""Test graceful handling of Redis connection failure."""
mock_get_redis.side_effect = Exception("Connection failed")
manager = CacheManager()
self.assertIsNone(manager.redis_client)
stats = manager.get_cache_stats()
self.assertFalse(stats["redis_available"])
class MalaysianDataCacheTest(TestCase):
"""Test Malaysian data caching."""
def setUp(self):
self.cache_manager = CacheManager()
self.malaysian_cache = MalaysianDataCache(self.cache_manager)
def test_ic_validation_caching(self):
"""Test IC validation caching."""
ic_number = "1234567890"
validation_result = {"valid": True, "age": 30}
result = self.malaysian_cache.set_cached_ic_validation(ic_number, validation_result)
self.assertTrue(result)
retrieved = self.malaysian_cache.get_cached_ic_validation(ic_number)
self.assertEqual(retrieved, validation_result)
def test_sst_rate_caching(self):
"""Test SST rate caching."""
state = "Johor"
category = "standard"
rate = 0.06
result = self.malaysian_cache.set_cached_sst_rate(state, category, rate)
self.assertTrue(result)
retrieved = self.malaysian_cache.get_cached_sst_rate(state, category)
self.assertEqual(retrieved, rate)
def test_postcode_data_caching(self):
"""Test postcode data caching."""
postcode = "50000"
postcode_data = {"city": "Kuala Lumpur", "state": "WP Kuala Lumpur"}
result = self.malaysian_cache.set_cached_postcode_data(postcode, postcode_data)
self.assertTrue(result)
retrieved = self.malaysian_cache.get_cached_postcode_data(postcode)
self.assertEqual(retrieved, postcode_data)
class QueryCacheTest(TestCase):
"""Test query caching."""
def setUp(self):
self.cache_manager = CacheManager()
self.query_cache = QueryCache(self.cache_manager)
def test_query_hash_generation(self):
"""Test query hash generation."""
query = "SELECT * FROM users WHERE id = %s"
params = (1,)
hash1 = self.query_cache.generate_query_hash(query, params)
hash2 = self.query_cache.generate_query_hash(query, params)
self.assertEqual(hash1, hash2)
# Different params should produce different hash
hash3 = self.query_cache.generate_query_hash(query, (2,))
self.assertNotEqual(hash1, hash3)
def test_query_result_caching(self):
"""Test query result caching."""
query = "SELECT * FROM test_table"
result = [{"id": 1, "name": "test"}]
success = self.query_cache.cache_query_result(query, result)
self.assertTrue(success)
retrieved = self.query_cache.get_cached_query_result(query)
self.assertEqual(retrieved, result)
def test_model_cache_invalidation(self):
"""Test model cache invalidation."""
# Add some query hashes
self.query_cache.query_hashes.add("user_query_123")
self.query_cache.query_hashes.add("product_query_456")
invalidated = self.query_cache.invalidate_model_cache("user")
self.assertEqual(invalidated, 1)
self.assertIn("product_query_456", self.query_cache.query_hashes)
self.assertNotIn("user_query_123", self.query_cache.query_hashes)
class TenantCacheManagerTest(TestCase):
"""Test tenant cache management."""
def setUp(self):
self.tenant_manager = TenantCacheManager()
def test_get_cache_manager(self):
"""Test getting cache manager for tenant."""
manager = self.tenant_manager.get_cache_manager(1)
self.assertIsInstance(manager, CacheManager)
self.assertEqual(manager.config.tenant_prefix, "tenant_1")
def test_cache_manager_reuse(self):
"""Test cache manager reuse for same tenant."""
manager1 = self.tenant_manager.get_cache_manager(1)
manager2 = self.tenant_manager.get_cache_manager(1)
self.assertIs(manager1, manager2)
def test_get_tenant_cache_stats(self):
"""Test tenant cache statistics."""
self.tenant_manager.get_cache_manager(1)
stats = self.tenant_manager.get_tenant_cache_stats()
self.assertIn("tenants", stats)
self.assertIn("total_tenants", stats)
self.assertEqual(stats["total_tenants"], 1)
class CacheWarmerTest(TestCase):
"""Test cache warming."""
def setUp(self):
self.cache_manager = CacheManager()
self.warmer = CacheWarmer(self.cache_manager)
def test_warm_malaysian_data(self):
"""Test warming Malaysian data."""
result = self.warmer.warm_malaysian_data()
self.assertIn("sst_rates", result)
self.assertIn("postcodes", result)
self.assertGreater(result["sst_rates"], 0)
self.assertGreater(result["postcodes"], 0)
def test_warm_user_data(self):
"""Test warming user data."""
user = User.objects.create_user(
username="testuser",
email="test@example.com",
password="testpass123"
)
warmed = self.warmer.warm_user_data([user.id])
self.assertEqual(warmed, 1)
# Verify user data is cached
key = self.cache_manager.key_generator.generate_key("user", str(user.id))
cached_data = self.cache_manager.get(key)
self.assertIsNotNone(cached_data)
self.assertEqual(cached_data["id"], user.id)
class WriteThroughCacheTest(TestCase):
"""Test write-through caching."""
def setUp(self):
self.cache_manager = CacheManager()
self.write_through = WriteThroughCache(self.cache_manager)
def test_write_through_operation(self):
"""Test write-through operation."""
key = "test_key"
value = "test_value"
def db_operation():
return value
result = self.write_through.write_through(key, value, db_operation)
self.assertEqual(result, value)
# Verify cache is populated
cached_value = self.cache_manager.get(key)
self.assertEqual(cached_value, value)
class ReadThroughCacheTest(TestCase):
"""Test read-through caching."""
def setUp(self):
self.cache_manager = CacheManager()
self.read_through = ReadThroughCache(self.cache_manager)
def test_read_through_operation(self):
"""Test read-through operation."""
key = "test_key"
value = "test_value"
def db_operation():
return value
# First read - should hit database and cache
result1 = self.read_through.read_through(key, db_operation)
self.assertEqual(result1, value)
# Second read - should hit cache
result2 = self.read_through.read_through(key, db_operation)
self.assertEqual(result2, value)
# Verify cache was populated
cached_value = self.cache_manager.get(key)
self.assertEqual(cached_value, value)
class CacheAsidePatternTest(TestCase):
"""Test cache-aside pattern."""
def setUp(self):
self.cache_manager = CacheManager()
self.cache_aside = CacheAsidePattern(self.cache_manager)
def test_get_or_set_operation(self):
"""Test get-or-set operation."""
key = "test_key"
value = "test_value"
def db_operation():
return value
# First call - should set cache
result1 = self.cache_aside.get_or_set(key, db_operation)
self.assertEqual(result1, value)
# Second call - should get from cache
result2 = self.cache_aside.get_or_set(key, db_operation)
self.assertEqual(result2, value)
def test_invalidate_operation(self):
"""Test cache invalidation."""
key = "test_key"
value = "test_value"
def db_operation():
return value
# Set cache
self.cache_aside.get_or_set(key, db_operation)
# Invalidate
result = self.cache_aside.invalidate(key)
self.assertTrue(result)
# Verify cache is cleared
cached_value = self.cache_manager.get(key)
self.assertIsNone(cached_value)
class MultiLevelCacheTest(TestCase):
"""Test multi-level caching."""
def setUp(self):
self.l1_cache = CacheManager()
self.l2_cache = CacheManager()
self.multi_cache = MultiLevelCache(self.l1_cache, self.l2_cache)
def test_multi_level_get_set(self):
"""Test multi-level get and set operations."""
key = "test_key"
value = "test_value"
# Set value
result = self.multi_cache.set(key, value)
self.assertTrue(result)
# Get from multi-level cache
retrieved = self.multi_cache.get(key)
self.assertEqual(retrieved, value)
def test_l1_promotion(self):
"""Test L1 cache promotion."""
key = "test_key"
value = "test_value"
# Set only in L2 cache
self.l2_cache.set(key, value)
# Get from multi-level cache - should promote to L1
retrieved = self.multi_cache.get(key)
self.assertEqual(retrieved, value)
# Verify it's now in L1 cache
l1_value = self.l1_cache.get(key)
self.assertEqual(l1_value, value)
def test_cache_statistics(self):
"""Test cache statistics."""
key = "test_key"
value = "test_value"
# Initial stats
stats = self.multi_cache.get_stats()
self.assertEqual(stats["l1_hits"], 0)
self.assertEqual(stats["l2_hits"], 0)
self.assertEqual(stats["misses"], 0)
# Set and get
self.multi_cache.set(key, value)
self.multi_cache.get(key) # L1 hit
stats = self.multi_cache.get_stats()
self.assertEqual(stats["l1_hits"], 1)
self.assertEqual(stats["misses"], 0)
class MalaysianCacheStrategiesTest(TestCase):
"""Test Malaysian cache strategies."""
def setUp(self):
self.cache_manager = CacheManager()
self.malaysian_strategies = MalaysianCacheStrategies(self.cache_manager)
def test_ic_validation_caching(self):
"""Test IC validation caching."""
ic_number = "1234567890"
def validation_func(ic):
return {"valid": True, "age": 30}
result = self.malaysian_strategies.cache_ic_validation(ic_number, validation_func)
self.assertEqual(result["valid"], True)
# Verify cached
cached = self.cache_manager.get(f"*:my:ic_validation_{ic_number}")
self.assertIsNotNone(cached)
def test_sst_calculation_caching(self):
"""Test SST calculation caching."""
calculation_key = "johor_standard"
def calculation_func():
return 0.06
result = self.malaysian_strategies.cache_sst_calculation(calculation_key, calculation_func)
self.assertEqual(result, 0.06)
def test_postcode_lookup_caching(self):
"""Test postcode lookup caching."""
postcode = "50000"
def lookup_func(pc):
return {"city": "Kuala Lumpur", "state": "WP Kuala Lumpur"}
result = self.malaysian_strategies.cache_postcode_lookup(postcode, lookup_func)
self.assertEqual(result["city"], "Kuala Lumpur")
class CacheEvictionPolicyTest(TestCase):
"""Test cache eviction policies."""
def setUp(self):
self.cache_manager = CacheManager()
self.eviction_policy = CacheEvictionPolicy(self.cache_manager)
def test_lru_eviction(self):
"""Test LRU eviction."""
keys = ["key1", "key2", "key3"]
# Record access with different times
self.eviction_policy.record_access("key1")
time.sleep(0.1)
self.eviction_policy.record_access("key2")
time.sleep(0.1)
self.eviction_policy.record_access("key3")
# LRU should evict key1 (oldest access)
evicted = self.eviction_policy.lru_eviction(keys, 1)
self.assertEqual(evicted, ["key1"])
def test_lfu_eviction(self):
"""Test LFU eviction."""
keys = ["key1", "key2", "key3"]
# Record different access frequencies
self.eviction_policy.record_access("key1")
self.eviction_policy.record_access("key2")
self.eviction_policy.record_access("key2") # Access twice
self.eviction_policy.record_access("key3")
self.eviction_policy.record_access("key3")
self.eviction_policy.record_access("key3") # Access three times
# LFU should evict key1 (least frequent)
evicted = self.eviction_policy.lfu_eviction(keys, 1)
self.assertEqual(evicted, ["key1"])
def test_fifo_eviction(self):
"""Test FIFO eviction."""
keys = ["key1", "key2", "key3"]
evicted = self.eviction_policy.fifo_eviction(keys, 1)
self.assertEqual(evicted, ["key1"])
class CacheMiddlewareTest(TestCase):
"""Test cache middleware."""
def setUp(self):
self.factory = RequestFactory()
self.middleware = CacheMiddleware(self.get_response)
def get_response(self, request):
return HttpResponse("test response")
def test_middleware_process_request_cacheable(self):
"""Test middleware process request for cacheable path."""
request = self.factory.get('/api/products/')
request.user = Mock()
request.user.is_authenticated = False
response = self.middleware.process_request(request)
self.assertIsNone(response) # Should not return cached response
def test_middleware_process_request_non_cacheable(self):
"""Test middleware process request for non-cacheable path."""
request = self.factory.get('/api/auth/login/')
request.user = Mock()
request.user.is_authenticated = False
response = self.middleware.process_request(request)
self.assertIsNone(response) # Should bypass cache
def test_middleware_should_bypass_cache(self):
"""Test cache bypass logic."""
request = self.factory.get('/api/products/')
request.user = Mock()
request.user.is_authenticated = True
should_bypass = self.middleware._should_bypass_cache(request)
self.assertTrue(should_bypass) # Should bypass for authenticated users
def test_cache_key_generation(self):
"""Test cache key generation."""
request = self.factory.get('/api/products/', {'category': 'electronics'})
request.user = Mock()
request.user.is_authenticated = False
request.tenant = Mock()
request.tenant.id = 1
key = self.middleware._generate_cache_key(request)
self.assertIn('/api/products/', key)
self.assertIn('tenant_1', key)
class CacheConfigurationTest(TestCase):
"""Test cache configuration."""
def test_cache_config_initialization(self):
"""Test cache configuration initialization."""
config = CacheConfig()
self.assertIsInstance(config.default_timeout, int)
self.assertIsInstance(config.use_redis, bool)
self.assertIsInstance(config.tenant_isolation, bool)
def test_get_cache_config(self):
"""Test getting cache configuration."""
config = get_cache_config()
self.assertIn('CACHES', config)
self.assertIn('CACHE_MIDDLEWARE_ALIAS', config)
self.assertIn('CACHE_MIDDLEWARE_SECONDS', config)
class CacheManagementCommandTest(TestCase):
"""Test cache management command."""
@patch('core.management.commands.cache_management.Command._output_results')
def test_command_initialization(self, mock_output):
"""Test command initialization."""
from core.management.commands.cache_management import Command
command = Command()
self.assertIsNotNone(command.cache_manager)
self.assertIsNotNone(command.malaysian_cache)
self.assertIsNotNone(command.query_cache)
@patch('core.management.commands.cache_management.Command._output_results')
def test_stats_action(self, mock_output):
"""Test stats action."""
from core.management.commands.cache_management import Command
command = Command()
command.action = 'stats'
command.cache_type = 'all'
command.output_format = 'table'
command.handle_stats()
# Verify _output_results was called
mock_output.assert_called_once()
@patch('core.management.commands.cache_management.Command._output_results')
def test_health_check_action(self, mock_output):
"""Test health check action."""
from core.management.commands.cache_management import Command
command = Command()
command.action = 'health-check'
command.output_format = 'table'
command.handle_health_check()
# Verify _output_results was called
mock_output.assert_called_once()
class CacheIntegrationTest(TestCase):
"""Integration tests for caching system."""
def test_full_cache_workflow(self):
"""Test complete cache workflow."""
# Create cache manager
cache_manager = CacheManager()
# Test Malaysian data caching
malaysian_cache = MalaysianDataCache(cache_manager)
# Cache IC validation
ic_result = {"valid": True, "age": 25}
malaysian_cache.set_cached_ic_validation("1234567890", ic_result)
# Retrieve cached result
cached_result = malaysian_cache.get_cached_ic_validation("1234567890")
self.assertEqual(cached_result, ic_result)
# Test query caching
query_cache = QueryCache(cache_manager)
query = "SELECT * FROM users WHERE id = %s"
result = [{"id": 1, "name": "test"}]
query_cache.cache_query_result(query, result)
cached_query_result = query_cache.get_cached_query_result(query)
self.assertEqual(cached_query_result, result)
# Test tenant isolation
tenant_manager = TenantCacheManager()
tenant1_cache = tenant_manager.get_cache_manager(1)
tenant2_cache = tenant_manager.get_cache_manager(2)
# Different tenants should have different cache managers
self.assertIsNot(tenant1_cache, tenant2_cache)
# Test cache warming
cache_warmer = CacheWarmer(cache_manager)
warmed = cache_warmer.warm_malaysian_data()
self.assertGreater(warmed["sst_rates"], 0)
def test_cache_error_handling(self):
"""Test cache error handling."""
cache_manager = CacheManager()
# Test get with non-existent key
result = cache_manager.get("nonexistent_key")
self.assertIsNone(result)
# Test get with default value
result = cache_manager.get("nonexistent_key", "default")
self.assertEqual(result, "default")
# Test error handling in operations
with patch.object(cache_manager, 'set', side_effect=Exception("Cache error")):
result = cache_manager.set("test_key", "test_value")
self.assertFalse(result)

View File

@@ -0,0 +1,682 @@
"""
Unit tests for database optimization components.
This module tests the database optimization functionality including query optimization,
index management, configuration management, and performance monitoring specifically
designed for the multi-tenant SaaS platform with Malaysian market requirements.
"""
import unittest
from unittest.mock import Mock, patch, MagicMock
from django.test import TestCase, override_settings
from django.db import connection, models
from django.core.cache import cache
from django.utils import timezone
from django.contrib.auth import get_user_model
from django_tenants.utils import schema_context
from core.optimization.query_optimization import (
DatabaseOptimizer,
QueryOptimizer,
CacheManager,
DatabaseMaintenance,
OptimizationLevel,
QueryMetrics,
IndexRecommendation
)
from core.optimization.index_manager import (
IndexManager,
IndexType,
IndexStatus,
IndexInfo,
IndexRecommendation as IndexRec
)
from core.optimization.config import (
DatabaseConfig,
ConnectionPoolConfig,
QueryOptimizationConfig,
CacheConfig,
MultiTenantConfig,
MalaysianConfig,
PerformanceConfig,
get_config,
validate_environment_config
)
User = get_user_model()
class DatabaseOptimizerTests(TestCase):
"""Test cases for DatabaseOptimizer class."""
def setUp(self):
"""Set up test environment."""
self.optimizer = DatabaseOptimizer()
self.test_tenant = "test_tenant"
def test_init(self):
"""Test DatabaseOptimizer initialization."""
optimizer = DatabaseOptimizer(self.test_tenant)
self.assertEqual(optimizer.tenant_schema, self.test_tenant)
self.assertIsInstance(optimizer.query_history, list)
self.assertIsInstance(optimizer.optimization_stats, dict)
@patch('core.optimization.query_optimization.connection')
def test_monitor_query_context_manager(self, mock_connection):
"""Test query monitoring context manager."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchone.return_value = ('test_query', 1, 0.5, 10, 1)
with self.optimizer.monitor_query("test query"):
pass
self.assertEqual(len(self.optimizer.query_history), 1)
self.assertEqual(self.optimizer.optimization_stats['queries_analyzed'], 1)
@patch('core.optimization.query_optimization.connection')
def test_optimize_tenant_queries(self, mock_connection):
"""Test tenant query optimization."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchone.return_value = (5,)
# Create a mock model
class TestModel(models.Model):
class Meta:
app_label = 'test'
results = self.optimizer.optimize_tenant_queries(TestModel, self.test_tenant)
self.assertIn('tenant', results)
self.assertIn('queries_optimized', results)
def test_optimize_malaysian_queries(self):
"""Test Malaysian query optimization."""
with patch.object(self.optimizer, '_optimize_sst_queries', return_value=3):
with patch.object(self.optimizer, '_optimize_ic_validation', return_value=True):
with patch.object(self.optimizer, '_optimize_address_queries', return_value=2):
results = self.optimizer.optimize_malaysian_queries()
self.assertEqual(results['sst_queries_optimized'], 3)
self.assertTrue(results['ic_validation_optimized'])
self.assertEqual(results['address_queries_optimized'], 2)
self.assertIn('localization_improvements', results)
@patch('core.optimization.query_optimization.connection')
def test_analyze_query_performance(self, mock_connection):
"""Test query performance analysis."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchall.return_value = [
(100, 0.5, 2),
[('public', 'test_table', 10, 100, 5, 50)]
]
analysis = self.optimizer.analyze_query_performance(24)
self.assertEqual(analysis['total_queries'], 100)
self.assertEqual(analysis['slow_queries'], 2)
self.assertEqual(len(analysis['most_used_tables']), 1)
def test_get_optimization_report(self):
"""Test optimization report generation."""
with patch.object(self.optimizer, 'optimize_malaysian_queries', return_value={}):
with patch.object(self.optimizer, 'analyze_query_performance', return_value={}):
with patch.object(self.optimizer, '_get_suggested_actions', return_value=[]):
report = self.optimizer.get_optimization_report()
self.assertIn('optimization_statistics', report)
self.assertIn('malaysian_optimizations', report)
self.assertIn('suggested_actions', report)
def test_clear_optimization_history(self):
"""Test clearing optimization history."""
self.optimizer.query_history = [Mock()]
self.optimizer.optimization_stats['queries_analyzed'] = 5
self.optimizer.clear_optimization_history()
self.assertEqual(len(self.optimizer.query_history), 0)
self.assertEqual(self.optimizer.optimization_stats['queries_analyzed'], 0)
class QueryOptimizerTests(TestCase):
"""Test cases for QueryOptimizer static methods."""
def test_optimize_tenant_filter(self):
"""Test tenant filter optimization."""
queryset = Mock()
optimized = QueryOptimizer.optimize_tenant_filter(queryset, 1)
queryset.filter.assert_called_once_with(tenant_id=1)
queryset.select_related.assert_called_once_with('tenant')
def test_optimize_pagination(self):
"""Test pagination optimization."""
queryset = Mock()
optimized = QueryOptimizer.optimize_pagination(queryset, 25)
queryset.order_by.assert_called_once_with('id')
queryset.__getitem__.assert_called_once_with(slice(0, 25))
def test_optimize_foreign_key_query(self):
"""Test foreign key query optimization."""
queryset = Mock()
optimized = QueryOptimizer.optimize_foreign_key_query(queryset, ['user', 'profile'])
queryset.select_related.assert_called_once_with('user', 'profile')
def test_optimize_many_to_many_query(self):
"""Test many-to-many query optimization."""
queryset = Mock()
optimized = QueryOptimizer.optimize_many_to_many_query(queryset, ['tags', 'categories'])
queryset.prefetch_related.assert_called_once_with('tags', 'categories')
def test_optimize_date_range_query(self):
"""Test date range query optimization."""
queryset = Mock()
start_date = timezone.now() - timezone.timedelta(days=7)
end_date = timezone.now()
optimized = QueryOptimizer.optimize_date_range_query(
queryset, 'created_at', start_date, end_date
)
expected_filter = {
'created_at__gte': start_date,
'created_at__lte': end_date
}
queryset.filter.assert_called_once_with(**expected_filter)
queryset.order_by.assert_called_once_with('created_at')
@patch('core.optimization.query_optimization.SearchVector')
@patch('core.optimization.query_optimization.SearchQuery')
@patch('core.optimization.query_optimization.SearchRank')
def test_optimize_full_text_search(self, mock_search_rank, mock_search_query, mock_search_vector):
"""Test full-text search optimization."""
queryset = Mock()
mock_search_vector.return_value = Mock()
mock_search_query.return_value = Mock()
mock_search_rank.return_value = Mock()
optimized = QueryOptimizer.optimize_full_text_search(
queryset, ['title', 'content'], 'search term'
)
queryset.annotate.assert_called()
queryset.filter.assert_called()
queryset.order_by.assert_called()
class CacheManagerTests(TestCase):
"""Test cases for CacheManager class."""
def test_get_cache_key(self):
"""Test cache key generation."""
key = CacheManager.get_cache_key("prefix", "arg1", "arg2", 123)
self.assertEqual(key, "prefix_arg1_arg2_123")
def test_cache_query_result(self):
"""Test caching query results."""
cache_key = "test_key"
query_result = {"data": "test"}
CacheManager.cache_query_result(cache_key, query_result, 3600)
# Mock cache.get to return cached result
with patch.object(cache, 'get', return_value=query_result):
cached_result = CacheManager.get_cached_result(cache_key)
self.assertEqual(cached_result, query_result)
@patch('core.optimization.query_optimization.cache')
def test_invalidate_cache_pattern(self, mock_cache):
"""Test cache invalidation by pattern."""
mock_cache.keys.return_value = ['prefix_1', 'prefix_2', 'other_key']
CacheManager.invalidate_cache_pattern('prefix_*')
mock_cache.delete_many.assert_called_once_with(['prefix_1', 'prefix_2'])
class DatabaseMaintenanceTests(TestCase):
"""Test cases for DatabaseMaintenance class."""
@patch('core.optimization.query_optimization.connection')
def test_analyze_tables(self, mock_connection):
"""Test table analysis."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchall.return_value = [
('public', 'test_table1'),
('public', 'test_table2')
]
DatabaseMaintenance.analyze_tables()
self.assertEqual(mock_cursor.execute.call_count, 2) # SELECT + ANALYZE
mock_cursor.execute.assert_any_call("ANALYZE public.test_table1")
mock_cursor.execute.assert_any_call("ANALYZE public.test_table2")
@patch('core.optimization.query_optimization.connection')
def test_vacuum_tables(self, mock_connection):
"""Test table vacuuming."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchall.return_value = [
('public', 'test_table1'),
('public', 'test_table2')
]
DatabaseMaintenance.vacuum_tables()
self.assertEqual(mock_cursor.execute.call_count, 2) # SELECT + VACUUM
mock_cursor.execute.assert_any_call("VACUUM ANALYZE public.test_table1")
mock_cursor.execute.assert_any_call("VACUUM ANALYZE public.test_table2")
@patch('core.optimization.query_optimization.connection')
def test_get_table_sizes(self, mock_connection):
"""Test getting table sizes."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchall.return_value = [
('public', 'test_table1', '10 MB', 10485760),
('public', 'test_table2', '5 MB', 5242880)
]
sizes = DatabaseMaintenance.get_table_sizes()
self.assertEqual(len(sizes), 2)
self.assertEqual(sizes[0]['table'], 'test_table1')
self.assertEqual(sizes[0]['size'], '10 MB')
self.assertEqual(sizes[0]['size_bytes'], 10485760)
class IndexManagerTests(TestCase):
"""Test cases for IndexManager class."""
def setUp(self):
"""Set up test environment."""
self.manager = IndexManager(self.test_tenant)
@patch('core.optimization.index_manager.connection')
def test_get_all_indexes(self, mock_connection):
"""Test getting all indexes."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchall.return_value = [
('idx_test', 'test_table', 'btree', False, False, 'CREATE INDEX idx_test ON test_table (id)', 1024, 'test_tenant')
]
indexes = self.manager.get_all_indexes()
self.assertEqual(len(indexes), 1)
self.assertIsInstance(indexes[0], IndexInfo)
self.assertEqual(indexes[0].name, 'idx_test')
self.assertEqual(indexes[0].table_name, 'test_table')
def test_extract_column_names(self):
"""Test extracting column names from index definition."""
definition = "CREATE INDEX idx_test ON test_table (id, name, created_at)"
columns = self.manager._extract_column_names(definition)
self.assertEqual(columns, ['id', 'name', 'created_at'])
@patch('core.optimization.index_manager.connection')
def test_analyze_index_performance(self, mock_connection):
"""Test index performance analysis."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchall.return_value = [
('test_table1', 5000, 100000, 1024 * 1024 * 10),
('test_table2', 1000, 50000, 1024 * 1024 * 5)
]
analysis = self.manager.analyze_index_performance()
self.assertIn('total_indexes', analysis)
self.assertIn('unused_indexes', analysis)
self.assertIn('recommendations', analysis)
@patch('core.optimization.index_manager.connection')
def test_create_index(self, mock_connection):
"""Test index creation."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
index_name = self.manager.create_index(
table_name='test_table',
columns=['id', 'name'],
index_type=IndexType.BTREE,
unique=True
)
self.assertEqual(index_name, 'unq_test_table_id_name')
mock_cursor.execute.assert_called_once()
self.assertEqual(self.manager.stats['indexes_created'], 1)
@patch('core.optimization.index_manager.connection')
def test_drop_index(self, mock_connection):
"""Test index dropping."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
result = self.manager.drop_index('test_index')
self.assertTrue(result)
mock_cursor.execute.assert_called_once()
self.assertEqual(self.manager.stats['indexes_dropped'], 1)
@patch('core.optimization.index_manager.connection')
def test_rebuild_index(self, mock_connection):
"""Test index rebuilding."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
result = self.manager.rebuild_index('test_index')
self.assertTrue(result)
mock_cursor.execute.assert_called_once_with("REINDEX INDEX test_index")
self.assertEqual(self.manager.stats['indexes_rebuilt'], 1)
@patch('core.optimization.index_manager.connection')
def test_create_malaysian_indexes(self, mock_connection):
"""Test creating Malaysian-specific indexes."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
created = self.manager.create_malaysian_indexes()
self.assertIsInstance(created, list)
# Should create multiple Malaysian indexes
self.assertGreater(len(created), 0)
@patch('core.optimization.index_manager.connection')
def test_get_index_statistics(self, mock_connection):
"""Test getting index statistics."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchall.return_value = [
('btree', 5),
('hash', 2),
('active', 6),
('inactive', 1)
]
stats = self.manager.get_index_statistics()
self.assertIn('total_indexes', stats)
self.assertIn('index_types', stats)
self.assertIn('status_distribution', stats)
self.assertEqual(stats['index_types']['btree'], 5)
self.assertEqual(stats['index_types']['hash'], 2)
class DatabaseConfigTests(TestCase):
"""Test cases for DatabaseConfig class."""
def test_production_config(self):
"""Test production configuration."""
config = DatabaseConfig("production")
self.assertEqual(config.environment, "production")
self.assertIsInstance(config.connection_pool, ConnectionPoolConfig)
self.assertIsInstance(config.query_optimization, QueryOptimizationConfig)
self.assertIsInstance(config.cache, CacheConfig)
self.assertIsInstance(config.multi_tenant, MultiTenantConfig)
self.assertIsInstance(config.malaysian, MalaysianConfig)
self.assertIsInstance(config.performance, PerformanceConfig)
# Check production-specific settings
self.assertGreater(config.connection_pool.max_connections, 50)
self.assertTrue(config.performance.enable_connection_pooling)
self.assertTrue(config.performance.enable_query_optimization)
def test_staging_config(self):
"""Test staging configuration."""
config = DatabaseConfig("staging")
self.assertEqual(config.environment, "staging")
# Should be less aggressive than production
self.assertLess(config.connection_pool.max_connections, 200)
self.assertGreater(config.query_optimization.slow_query_threshold, 0.5)
def test_development_config(self):
"""Test development configuration."""
config = DatabaseConfig("development")
self.assertEqual(config.environment, "development")
# Should have minimal optimization for development
self.assertFalse(config.performance.enable_connection_pooling)
self.assertFalse(config.performance.enable_query_optimization)
def test_get_django_database_config(self):
"""Test Django database configuration generation."""
config = DatabaseConfig("production")
db_config = config.get_django_database_config()
self.assertIn('default', db_config)
self.assertIn('ENGINE', db_config['default'])
self.assertIn('OPTIONS', db_config['default'])
self.assertEqual(db_config['default']['ENGINE'], 'django_tenants.postgresql_backend')
def test_get_django_cache_config(self):
"""Test Django cache configuration generation."""
config = DatabaseConfig("production")
cache_config = config.get_django_cache_config()
self.assertIn('default', cache_config)
self.assertIn('tenant_cache', cache_config)
self.assertIn('malaysian_cache', cache_config)
def test_get_postgresql_settings(self):
"""Test PostgreSQL settings generation."""
config = DatabaseConfig("production")
settings = config.get_postgresql_settings()
self.assertIsInstance(settings, list)
self.assertGreater(len(settings), 0)
# Should contain performance-related settings
settings_str = ' '.join(settings)
self.assertIn('shared_buffers', settings_str)
self.assertIn('effective_cache_size', settings_str)
def test_validate_configuration(self):
"""Test configuration validation."""
config = DatabaseConfig("production")
warnings = config.validate_configuration()
self.assertIsInstance(warnings, list)
# Should not have warnings for valid config
# But will accept empty list as valid
def test_get_performance_recommendations(self):
"""Test performance recommendations."""
config = DatabaseConfig("production")
recommendations = config.get_performance_recommendations()
self.assertIsInstance(recommendations, list)
# Should have recommendations for production
self.assertGreater(len(recommendations), 0)
class ConfigFactoryTests(TestCase):
"""Test cases for configuration factory functions."""
def test_get_config(self):
"""Test configuration factory function."""
config = get_config("production")
self.assertIsInstance(config, DatabaseConfig)
self.assertEqual(config.environment, "production")
def test_get_production_config(self):
"""Test production configuration factory."""
config = get_production_config()
self.assertIsInstance(config, DatabaseConfig)
self.assertEqual(config.environment, "production")
def test_get_staging_config(self):
"""Test staging configuration factory."""
config = get_staging_config()
self.assertIsInstance(config, DatabaseConfig)
self.assertEqual(config.environment, "staging")
def test_get_development_config(self):
"""Test development configuration factory."""
config = get_development_config()
self.assertIsInstance(config, DatabaseConfig)
self.assertEqual(config.environment, "development")
@patch('core.optimization.config.get_config')
def test_validate_environment_config(self, mock_get_config):
"""Test environment configuration validation."""
mock_config = Mock()
mock_config.validate_configuration.return_value = []
mock_get_config.return_value = mock_config
result = validate_environment_config("production")
self.assertTrue(result)
mock_config.validate_configuration.assert_called_once()
class IntegrationTests(TestCase):
"""Integration tests for optimization components."""
@override_settings(CACHES={
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
}
})
def test_cache_manager_integration(self):
"""Test CacheManager integration with Django cache."""
cache_key = CacheManager.get_cache_key("test", "integration")
test_data = {"key": "value"}
CacheManager.cache_query_result(cache_key, test_data)
cached_data = CacheManager.get_cached_result(cache_key)
self.assertEqual(cached_data, test_data)
@patch('core.optimization.query_optimization.connection')
def test_database_optimizer_integration(self, mock_connection):
"""Test DatabaseOptimizer integration."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchall.return_value = [
(100, 0.5, 2),
[('public', 'test_table', 10, 100, 5, 50)]
]
optimizer = DatabaseOptimizer()
analysis = optimizer.analyze_query_performance()
self.assertEqual(analysis['total_queries'], 100)
self.assertEqual(analysis['slow_queries'], 2)
def test_query_optimizer_integration(self):
"""Test QueryOptimizer integration with mock querysets."""
# This test uses mock querysets to test optimization logic
queryset = Mock()
optimized = QueryOptimizer.optimize_tenant_filter(queryset, 1)
queryset.filter.assert_called_with(tenant_id=1)
queryset.select_related.assert_called_with('tenant')
class MalaysianOptimizationTests(TestCase):
"""Test cases for Malaysian-specific optimizations."""
def setUp(self):
"""Set up test environment."""
self.optimizer = DatabaseOptimizer()
@patch('core.optimization.query_optimization.connection')
def test_malaysian_sst_optimization(self, mock_connection):
"""Test SST optimization for Malaysian market."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
result = self.optimizer._optimize_sst_queries()
self.assertIsInstance(result, int)
self.assertGreaterEqual(result, 0)
@patch('core.optimization.query_optimization.connection')
def test_malaysian_ic_validation_optimization(self, mock_connection):
"""Test IC validation optimization for Malaysian market."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
result = self.optimizer._optimize_ic_validation()
self.assertIsInstance(result, bool)
@patch('core.optimization.query_optimization.connection')
def test_malaysian_address_optimization(self, mock_connection):
"""Test address optimization for Malaysian market."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
result = self.optimizer._optimize_address_queries()
self.assertIsInstance(result, int)
self.assertGreaterEqual(result, 0)
def test_malaysian_config(self):
"""Test Malaysian configuration settings."""
config = DatabaseConfig("production")
self.assertEqual(config.malaysian.timezone, "Asia/Kuala_Lumpur")
self.assertEqual(config.malaysian.locale, "ms_MY")
self.assertEqual(config.malaysian.currency, "MYR")
self.assertTrue(config.malaysian.enable_local_caching)
self.assertTrue(config.malaysian.malaysian_indexes_enabled)
class PerformanceTests(TestCase):
"""Performance tests for optimization components."""
@patch('core.optimization.query_optimization.connection')
def test_query_monitoring_performance(self, mock_connection):
"""Test performance of query monitoring."""
mock_cursor = Mock()
mock_connection.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchone.return_value = ('test_query', 1, 0.1, 10, 1)
import time
start_time = time.time()
# Monitor multiple queries
for i in range(100):
with self.optimizer.monitor_query(f"test query {i}"):
pass
end_time = time.time()
execution_time = end_time - start_time
# Should be fast (less than 1 second for 100 queries)
self.assertLess(execution_time, 1.0)
self.assertEqual(len(self.optimizer.query_history), 100)
@patch('core.optimization.query_optimization.connection')
def test_cache_manager_performance(self, mock_connection):
"""Test performance of cache operations."""
import time
start_time = time.time()
# Perform multiple cache operations
for i in range(1000):
key = CacheManager.get_cache_key("perf_test", i)
CacheManager.cache_query_result(key, f"value_{i}")
end_time = time.time()
execution_time = end_time - start_time
# Should be fast (less than 1 second for 1000 operations)
self.assertLess(execution_time, 1.0)
if __name__ == '__main__':
unittest.main()

View File

View File

@@ -0,0 +1,461 @@
"""
Unit tests for General Helper Utilities
Tests for general utility functions:
- Date/time helpers
- String helpers
- Number helpers
- File helpers
- Security helpers
Author: Claude
"""
import pytest
from django.test import TestCase
from django.utils import timezone
from datetime import date, datetime, time, timedelta
from decimal import Decimal
import hashlib
import json
from backend.src.core.utils.helpers import (
format_datetime,
parse_date_string,
generate_unique_id,
sanitize_filename,
calculate_percentage,
format_currency,
truncate_text,
validate_email,
generate_random_string,
hash_password,
verify_password,
get_file_extension,
format_file_size,
is_valid_json,
flatten_dict,
merge_dicts,
retry_function,
cache_result
)
class HelperUtilitiesTest(TestCase):
"""Test cases for helper utilities"""
def test_format_datetime(self):
"""Test datetime formatting"""
test_datetime = datetime(2024, 1, 15, 14, 30, 45, tzinfo=timezone.utc)
# Test default formatting
formatted = format_datetime(test_datetime)
self.assertIn('2024', formatted)
self.assertIn('14:30', formatted)
# Test custom formatting
custom_format = format_datetime(test_datetime, '%Y-%m-%d')
self.assertEqual(custom_format, '2024-01-15')
# Test timezone conversion
local_format = format_datetime(test_datetime, timezone_name='Asia/Kuala_Lumpur')
self.assertIn('22:30', local_format) # UTC+8
def test_parse_date_string(self):
"""Test date string parsing"""
test_cases = [
{'input': '2024-01-15', 'expected': date(2024, 1, 15)},
{'input': '15/01/2024', 'expected': date(2024, 1, 15)},
{'input': '01-15-2024', 'expected': date(2024, 1, 15)},
{'input': '20240115', 'expected': date(2024, 1, 15)},
]
for case in test_cases:
result = parse_date_string(case['input'])
self.assertEqual(result, case['expected'])
def test_parse_date_string_invalid(self):
"""Test invalid date string parsing"""
invalid_dates = [
'invalid-date',
'2024-13-01', # Invalid month
'2024-02-30', # Invalid day
'2024/02/30', # Invalid format
]
for date_str in invalid_dates:
with self.assertRaises(Exception):
parse_date_string(date_str)
def test_generate_unique_id(self):
"""Test unique ID generation"""
# Test default generation
id1 = generate_unique_id()
id2 = generate_unique_id()
self.assertNotEqual(id1, id2)
self.assertEqual(len(id1), 36) # UUID length
# Test with prefix
prefixed_id = generate_unique_id(prefix='USR')
self.assertTrue(prefixed_id.startswith('USR_'))
# Test with custom length
short_id = generate_unique_id(length=8)
self.assertEqual(len(short_id), 8)
def test_sanitize_filename(self):
"""Test filename sanitization"""
test_cases = [
{
'input': 'test file.txt',
'expected': 'test_file.txt'
},
{
'input': 'my*document?.pdf',
'expected': 'my_document.pdf'
},
{
'input': ' spaces file .jpg ',
'expected': 'spaces_file.jpg'
},
{
'input': '../../../malicious/path.txt',
'expected': 'malicious_path.txt'
}
]
for case in test_cases:
result = sanitize_filename(case['input'])
self.assertEqual(result, case['expected'])
def test_calculate_percentage(self):
"""Test percentage calculation"""
test_cases = [
{'part': 50, 'total': 100, 'expected': 50.0},
{'part': 25, 'total': 200, 'expected': 12.5},
{'part': 0, 'total': 100, 'expected': 0.0},
{'part': 100, 'total': 100, 'expected': 100.0},
]
for case in test_cases:
result = calculate_percentage(case['part'], case['total'])
self.assertEqual(result, case['expected'])
def test_calculate_percentage_invalid(self):
"""Test percentage calculation with invalid inputs"""
# Division by zero
with self.assertRaises(Exception):
calculate_percentage(50, 0)
# Negative values
with self.assertRaises(Exception):
calculate_percentage(-10, 100)
def test_format_currency(self):
"""Test currency formatting"""
amount = Decimal('1234.56')
# Test default formatting (MYR)
formatted = format_currency(amount)
self.assertEqual(formatted, 'RM 1,234.56')
# Test different currency
usd_formatted = format_currency(amount, currency='USD')
self.assertEqual(usd_formatted, '$ 1,234.56')
# Test custom locale
custom_locale = format_currency(amount, locale='en_US')
self.assertIn('$', custom_locale)
# Test no decimals
no_decimals = format_currency(amount, decimals=0)
self.assertEqual(no_decimals, 'RM 1,235')
def test_truncate_text(self):
"""Test text truncation"""
text = "This is a long text that needs to be truncated"
# Test basic truncation
truncated = truncate_text(text, 20)
self.assertEqual(len(truncated), 20)
self.assertTrue(truncated.endswith('...'))
# Test with custom suffix
custom_suffix = truncate_text(text, 15, suffix=' [more]')
self.assertTrue(custom_suffix.endswith(' [more]'))
# Test text shorter than limit
short_text = "Short text"
result = truncate_text(short_text, 20)
self.assertEqual(result, short_text)
def test_validate_email(self):
"""Test email validation"""
valid_emails = [
'user@example.com',
'test.email+tag@domain.co.uk',
'user_name@sub.domain.com',
'123user@example.org'
]
invalid_emails = [
'invalid-email',
'@example.com',
'user@',
'user@.com',
'user..name@example.com',
'user@example..com'
]
for email in valid_emails:
self.assertTrue(validate_email(email))
for email in invalid_emails:
self.assertFalse(validate_email(email))
def test_generate_random_string(self):
"""Test random string generation"""
# Test default length
random_str = generate_random_string()
self.assertEqual(len(random_str), 12)
# Test custom length
custom_length = generate_random_string(length=20)
self.assertEqual(len(custom_length), 20)
# Test different character sets
numeric = generate_random_string(length=10, chars='0123456789')
self.assertTrue(numeric.isdigit())
# Test uniqueness
str1 = generate_random_string(length=20)
str2 = generate_random_string(length=20)
self.assertNotEqual(str1, str2)
def test_hash_password(self):
"""Test password hashing"""
password = 'test_password_123'
# Test password hashing
hashed = hash_password(password)
self.assertNotEqual(hashed, password)
self.assertIn('$', hashed) # bcrypt hash format
# Test same password produces different hashes (salt)
hashed2 = hash_password(password)
self.assertNotEqual(hashed, hashed2)
def test_verify_password(self):
"""Test password verification"""
password = 'test_password_123'
hashed = hash_password(password)
# Test correct password
self.assertTrue(verify_password(password, hashed))
# Test incorrect password
self.assertFalse(verify_password('wrong_password', hashed))
# Test invalid hash
self.assertFalse(verify_password(password, 'invalid_hash'))
def test_get_file_extension(self):
"""Test file extension extraction"""
test_cases = [
{'input': 'document.pdf', 'expected': '.pdf'},
{'input': 'image.JPG', 'expected': '.jpg'},
{'input': 'archive.tar.gz', 'expected': '.gz'},
{'input': 'no_extension', 'expected': ''},
{'input': '.hidden_file', 'expected': ''},
]
for case in test_cases:
result = get_file_extension(case['input'])
self.assertEqual(result.lower(), case['expected'].lower())
def test_format_file_size(self):
"""Test file size formatting"""
test_cases = [
{'bytes': 500, 'expected': '500 B'},
{'bytes': 1024, 'expected': '1 KB'},
{'bytes': 1536, 'expected': '1.5 KB'},
{'bytes': 1048576, 'expected': '1 MB'},
{'bytes': 1073741824, 'expected': '1 GB'},
{'bytes': 1099511627776, 'expected': '1 TB'},
]
for case in test_cases:
result = format_file_size(case['bytes'])
self.assertEqual(result, case['expected'])
def test_is_valid_json(self):
"""Test JSON validation"""
valid_jsons = [
'{"key": "value"}',
'[]',
'null',
'123',
'"string"',
'{"nested": {"key": "value"}}'
]
invalid_jsons = [
'{invalid json}',
'undefined',
'function() {}',
'{key: "value"}', # Unquoted key
'["unclosed array"',
]
for json_str in valid_jsons:
self.assertTrue(is_valid_json(json_str))
for json_str in invalid_jsons:
self.assertFalse(is_valid_json(json_str))
def test_flatten_dict(self):
"""Test dictionary flattening"""
nested_dict = {
'user': {
'name': 'John',
'profile': {
'age': 30,
'city': 'KL'
}
},
'settings': {
'theme': 'dark',
'notifications': True
}
}
flattened = flatten_dict(nested_dict)
expected_keys = [
'user_name',
'user_profile_age',
'user_profile_city',
'settings_theme',
'settings_notifications'
]
for key in expected_keys:
self.assertIn(key, flattened)
self.assertEqual(flattened['user_name'], 'John')
self.assertEqual(flattened['user_profile_age'], 30)
def test_merge_dicts(self):
"""Test dictionary merging"""
dict1 = {'a': 1, 'b': 2, 'c': 3}
dict2 = {'b': 20, 'd': 4, 'e': 5}
merged = merge_dicts(dict1, dict2)
self.assertEqual(merged['a'], 1) # From dict1
self.assertEqual(merged['b'], 20) # From dict2 (overwritten)
self.assertEqual(merged['c'], 3) # From dict1
self.assertEqual(merged['d'], 4) # From dict2
self.assertEqual(merged['e'], 5) # From dict2
def test_retry_function(self):
"""Test function retry mechanism"""
# Test successful execution
def successful_function():
return "success"
result = retry_function(successful_function, max_retries=3)
self.assertEqual(result, "success")
# Test function that fails then succeeds
call_count = 0
def flaky_function():
nonlocal call_count
call_count += 1
if call_count < 3:
raise Exception("Temporary failure")
return "eventual_success"
result = retry_function(flaky_function, max_retries=5)
self.assertEqual(result, "eventual_success")
self.assertEqual(call_count, 3)
# Test function that always fails
def failing_function():
raise Exception("Permanent failure")
with self.assertRaises(Exception):
retry_function(failing_function, max_retries=3)
def test_cache_result(self):
"""Test result caching decorator"""
# Create a function that counts calls
call_count = 0
@cache_result(timeout=60) # 60 second cache
def expensive_function(x, y):
nonlocal call_count
call_count += 1
return x + y
# First call should execute function
result1 = expensive_function(2, 3)
self.assertEqual(result1, 5)
self.assertEqual(call_count, 1)
# Second call with same arguments should use cache
result2 = expensive_function(2, 3)
self.assertEqual(result2, 5)
self.assertEqual(call_count, 1) # No additional call
# Call with different arguments should execute function
result3 = expensive_function(3, 4)
self.assertEqual(result3, 7)
self.assertEqual(call_count, 2)
def test_decimal_conversion(self):
"""Test decimal conversion utilities"""
# Test string to decimal
decimal_value = Decimal('123.45')
self.assertEqual(decimal_value, Decimal('123.45'))
# Test float to decimal (with precision warning)
float_value = 123.45
decimal_from_float = Decimal(str(float_value))
self.assertEqual(decimal_from_float, Decimal('123.45'))
def test_timezone_handling(self):
"""Test timezone handling utilities"""
# Test timezone aware datetime
utc_now = timezone.now()
self.assertIsNotNone(utc_now.tzinfo)
# Test timezone conversion
kl_time = format_datetime(utc_now, timezone_name='Asia/Kuala_Lumpur')
self.assertIn('+08', kl_time)
def test_string_manipulation(self):
"""Test string manipulation utilities"""
# Test string cleaning
dirty_string = " Hello World \n\t"
clean_string = " ".join(dirty_string.split())
self.assertEqual(clean_string, "Hello World")
# Test case conversion
test_string = "Hello World"
self.assertEqual(test_string.lower(), "hello world")
self.assertEqual(test_string.upper(), "HELLO WORLD")
self.assertEqual(test_string.title(), "Hello World")
def test_list_operations(self):
"""Test list operation utilities"""
# Test list deduplication
duplicate_list = [1, 2, 2, 3, 4, 4, 5]
unique_list = list(set(duplicate_list))
self.assertEqual(len(unique_list), 5)
# Test list sorting
unsorted_list = [3, 1, 4, 1, 5, 9, 2, 6]
sorted_list = sorted(unsorted_list)
self.assertEqual(sorted_list, [1, 1, 2, 3, 4, 5, 6, 9])

View File

@@ -0,0 +1,387 @@
"""
Unit tests for Malaysian Validators
Tests for Malaysian-specific validation utilities:
- IC number validation
- Phone number validation
- Business registration validation
- Address validation
- SST calculation
Author: Claude
"""
import pytest
from django.test import TestCase
from django.core.exceptions import ValidationError
from backend.src.core.utils.malaysian_validators import (
validate_ic_number,
validate_phone_number,
validate_business_registration,
validate_malaysian_address,
calculate_sst,
validate_postal_code,
format_malaysian_phone,
get_malaysian_states
)
class MalaysianValidatorsTest(TestCase):
"""Test cases for Malaysian validators"""
def test_validate_ic_number_valid(self):
"""Test valid Malaysian IC number validation"""
valid_ic_numbers = [
'000101-01-0001', # Valid format
'900101-10-1234', # Valid format
'851231-12-5678', # Valid format
]
for ic_number in valid_ic_numbers:
result = validate_ic_number(ic_number)
self.assertTrue(result['is_valid'])
self.assertEqual(result['normalized'], ic_number)
def test_validate_ic_number_invalid(self):
"""Test invalid Malaysian IC number validation"""
invalid_ic_numbers = [
'123', # Too short
'000101-01-000', # Wrong length
'000101-01-00012', # Wrong length
'000101-01-000A', # Contains letter
'000101/01/0001', # Wrong separator
'00-01-01-0001', # Wrong format
]
for ic_number in invalid_ic_numbers:
result = validate_ic_number(ic_number)
self.assertFalse(result['is_valid'])
self.assertIsNotNone(result.get('error'))
def test_validate_phone_number_valid(self):
"""Test valid Malaysian phone number validation"""
valid_phones = [
'+60123456789', # Standard mobile
'0123456789', # Mobile without country code
'+60312345678', # Landline
'0312345678', # Landline without country code
'+60111234567', # New mobile prefix
]
for phone in valid_phones:
result = validate_phone_number(phone)
self.assertTrue(result['is_valid'])
self.assertEqual(result['type'], 'mobile' if phone.startswith('01') else 'landline')
def test_validate_phone_number_invalid(self):
"""Test invalid Malaysian phone number validation"""
invalid_phones = [
'12345', # Too short
'0123456789A', # Contains letter
'+6512345678', # Singapore number
'123456789012', # Too long
'0112345678', # Invalid prefix
]
for phone in invalid_phones:
result = validate_phone_number(phone)
self.assertFalse(result['is_valid'])
self.assertIsNotNone(result.get('error'))
def test_validate_business_registration_valid(self):
"""Test valid business registration validation"""
valid_registrations = [
'202401000001', # Company registration
'001234567-K', # Business registration
'SM1234567-K', # Small medium enterprise
]
for reg in valid_registrations:
result = validate_business_registration(reg)
self.assertTrue(result['is_valid'])
self.assertIsNotNone(result.get('type'))
def test_validate_business_registration_invalid(self):
"""Test invalid business registration validation"""
invalid_registrations = [
'123', # Too short
'20240100000', # Missing check digit
'202401000001A', # Contains letter
'0012345678-K', # Too long
]
for reg in invalid_registrations:
result = validate_business_registration(reg)
self.assertFalse(result['is_valid'])
self.assertIsNotNone(result.get('error'))
def test_validate_malaysian_address_valid(self):
"""Test valid Malaysian address validation"""
valid_addresses = [
{
'address': '123 Test Street',
'city': 'Kuala Lumpur',
'state': 'KUL',
'postal_code': '50000'
},
{
'address': '456 Jalan Merdeka',
'city': 'Penang',
'state': 'PNG',
'postal_code': '10000'
}
]
for address in valid_addresses:
result = validate_malaysian_address(address)
self.assertTrue(result['is_valid'])
def test_validate_malaysian_address_invalid(self):
"""Test invalid Malaysian address validation"""
invalid_addresses = [
{
'address': '', # Empty address
'city': 'Kuala Lumpur',
'state': 'KUL',
'postal_code': '50000'
},
{
'address': '123 Test Street',
'city': '', # Empty city
'state': 'KUL',
'postal_code': '50000'
},
{
'address': '123 Test Street',
'city': 'Kuala Lumpur',
'state': 'XX', # Invalid state
'postal_code': '50000'
},
{
'address': '123 Test Street',
'city': 'Kuala Lumpur',
'state': 'KUL',
'postal_code': '123' # Invalid postal code
}
]
for address in invalid_addresses:
result = validate_malaysian_address(address)
self.assertFalse(result['is_valid'])
self.assertIsNotNone(result.get('errors'))
def test_calculate_sst(self):
"""Test SST calculation"""
test_cases = [
{'amount': 100.00, 'expected_sst': 6.00}, # 6% SST
{'amount': 50.00, 'expected_sst': 3.00}, # 6% SST
{'amount': 0.00, 'expected_sst': 0.00}, # Zero amount
{'amount': 999.99, 'expected_sst': 59.9994}, # High amount
]
for case in test_cases:
sst_amount = calculate_sst(case['amount'])
self.assertAlmostEqual(sst_amount, case['expected_sst'], places=4)
def test_calculate_sst_invalid(self):
"""Test SST calculation with invalid inputs"""
invalid_cases = [
-100.00, # Negative amount
None, # None value
'invalid', # String value
]
for amount in invalid_cases:
with self.assertRaises(Exception):
calculate_sst(amount)
def test_validate_postal_code_valid(self):
"""Test valid postal code validation"""
valid_postal_codes = [
'50000', # KL postal code
'10000', # Penang postal code
'80000', # Johor Bahru postal code
'97000', # Sarawak postal code
]
for postal_code in valid_postal_codes:
result = validate_postal_code(postal_code)
self.assertTrue(result['is_valid'])
self.assertEqual(result['state'], result.get('state'))
def test_validate_postal_code_invalid(self):
"""Test invalid postal code validation"""
invalid_postal_codes = [
'1234', # Too short
'123456', # Too long
'ABCDE', # Contains letters
'00000', # Invalid range
'99999', # Invalid range
]
for postal_code in invalid_postal_codes:
result = validate_postal_code(postal_code)
self.assertFalse(result['is_valid'])
self.assertIsNotNone(result.get('error'))
def test_format_malaysian_phone(self):
"""Test Malaysian phone number formatting"""
test_cases = [
{'input': '0123456789', 'expected': '+6012-3456789'},
{'input': '+60123456789', 'expected': '+6012-3456789'},
{'input': '0312345678', 'expected': '+603-12345678'},
{'input': '+60312345678', 'expected': '+603-12345678'},
]
for case in test_cases:
formatted = format_malaysian_phone(case['input'])
self.assertEqual(formatted, case['expected'])
def test_format_malaysian_phone_invalid(self):
"""Test formatting invalid phone numbers"""
invalid_phones = [
'12345', # Too short
'invalid', # Non-numeric
'6512345678', # Singapore number
]
for phone in invalid_phones:
result = format_malaysian_phone(phone)
self.assertEqual(result, phone) # Should return original if invalid
def test_get_malaysian_states(self):
"""Test getting Malaysian states"""
states = get_malaysian_states()
# Check if all expected states are present
expected_states = [
'Johor', 'Kedah', 'Kelantan', 'Malacca', 'Negeri Sembilan',
'Pahang', 'Perak', 'Perlis', 'Penang', 'Sabah', 'Sarawak',
'Selangor', 'Terengganu', 'Kuala Lumpur', 'Labuan', 'Putrajaya'
]
for state in expected_states:
self.assertIn(state, states)
# Check state codes
self.assertEqual(states['Kuala Lumpur'], 'KUL')
self.assertEqual(states['Penang'], 'PNG')
self.assertEqual(states['Johor'], 'JHR')
def test_ic_number_structure_validation(self):
"""Test IC number structure validation"""
# Test age calculation from IC
ic_1990 = '900101-01-0001' # Born 1990
result = validate_ic_number(ic_1990)
self.assertTrue(result['is_valid'])
self.assertEqual(result['birth_year'], 1990)
self.assertEqual(result['birth_date'], '1990-01-01')
# Test gender from IC (last digit: odd = male, even = female)
ic_male = '900101-01-0001' # Odd last digit
ic_female = '900101-01-0002' # Even last digit
result_male = validate_ic_number(ic_male)
result_female = validate_ic_number(ic_female)
self.assertEqual(result_male['gender'], 'male')
self.assertEqual(result_female['gender'], 'female')
def test_phone_number_type_detection(self):
"""Test phone number type detection"""
mobile_numbers = [
'0123456789', # Maxis
'0198765432', # Celcom
'0162345678', # DiGi
'0181234567', # U Mobile
'01112345678', # Yes 4G
]
landline_numbers = [
'0312345678', # KL
'0412345678', # Penang
'0512345678', # Perak
'0612345678', # Melaka
'0712345678', # Johor
]
for number in mobile_numbers:
result = validate_phone_number(number)
self.assertTrue(result['is_valid'])
self.assertEqual(result['type'], 'mobile')
for number in landline_numbers:
result = validate_phone_number(number)
self.assertTrue(result['is_valid'])
self.assertEqual(result['type'], 'landline')
def test_business_registration_type_detection(self):
"""Test business registration type detection"""
company_reg = '202401000001' # Company registration
business_reg = '001234567-K' # Business registration
sme_reg = 'SM1234567-K' # Small medium enterprise
result_company = validate_business_registration(company_reg)
result_business = validate_business_registration(business_reg)
result_sme = validate_business_registration(sme_reg)
self.assertEqual(result_company['type'], 'company')
self.assertEqual(result_business['type'], 'business')
self.assertEqual(result_sme['type'], 'sme')
def test_address_component_validation(self):
"""Test individual address component validation"""
# Test state code validation
valid_states = ['KUL', 'PNG', 'JHR', 'KDH', 'KTN']
invalid_states = ['XX', 'ABC', '123']
for state in valid_states:
address = {
'address': '123 Test Street',
'city': 'Test City',
'state': state,
'postal_code': '50000'
}
result = validate_malaysian_address(address)
self.assertTrue(result['is_valid'])
for state in invalid_states:
address = {
'address': '123 Test Street',
'city': 'Test City',
'state': state,
'postal_code': '50000'
}
result = validate_malaysian_address(address)
self.assertFalse(result['is_valid'])
def test_sst_edge_cases(self):
"""Test SST calculation edge cases"""
# Test very small amounts
sst_small = calculate_sst(0.01)
self.assertAlmostEqual(sst_small, 0.0006, places=4)
# Test very large amounts
sst_large = calculate_sst(1000000.00)
self.assertEqual(sst_large, 60000.00)
# Test decimal places
sst_decimal = calculate_sst(123.45)
self.assertAlmostEqual(sst_decimal, 7.407, places=4)
def test_postal_code_state_mapping(self):
"""Test postal code to state mapping"""
# Test known postal code ranges
test_cases = [
{'postal_code': '50000', 'expected_state': 'KUL'}, # KL
{'postal_code': '10000', 'expected_state': 'PNG'}, # Penang
{'postal_code': '80000', 'expected_state': 'JHR'}, # Johor
{'postal_code': '09000', 'expected_state': 'KDH'}, # Kedah
{'postal_code': '98000', 'expected_state': 'SBH'}, # Sabah
]
for case in test_cases:
result = validate_postal_code(case['postal_code'])
self.assertTrue(result['is_valid'])
self.assertEqual(result['state'], case['expected_state'])