project initialization
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
This commit is contained in:
0
backend/tests/unit/models/__init__.py
Normal file
0
backend/tests/unit/models/__init__.py
Normal file
459
backend/tests/unit/models/test_beauty_models.py
Normal file
459
backend/tests/unit/models/test_beauty_models.py
Normal 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')
|
||||
340
backend/tests/unit/models/test_core_models.py
Normal file
340
backend/tests/unit/models/test_core_models.py
Normal 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)
|
||||
413
backend/tests/unit/models/test_education_models.py
Normal file
413
backend/tests/unit/models/test_education_models.py
Normal 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)
|
||||
323
backend/tests/unit/models/test_healthcare_models.py
Normal file
323
backend/tests/unit/models/test_healthcare_models.py
Normal 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)
|
||||
470
backend/tests/unit/models/test_logistics_models.py
Normal file
470
backend/tests/unit/models/test_logistics_models.py
Normal 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)
|
||||
350
backend/tests/unit/models/test_retail_models.py
Normal file
350
backend/tests/unit/models/test_retail_models.py
Normal 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)
|
||||
0
backend/tests/unit/services/__init__.py
Normal file
0
backend/tests/unit/services/__init__.py
Normal file
638
backend/tests/unit/services/test_core_services.py
Normal file
638
backend/tests/unit/services/test_core_services.py
Normal 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)
|
||||
686
backend/tests/unit/test_caching.py
Normal file
686
backend/tests/unit/test_caching.py
Normal 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)
|
||||
682
backend/tests/unit/test_optimization.py
Normal file
682
backend/tests/unit/test_optimization.py
Normal 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()
|
||||
0
backend/tests/unit/utils/__init__.py
Normal file
0
backend/tests/unit/utils/__init__.py
Normal file
461
backend/tests/unit/utils/test_helpers.py
Normal file
461
backend/tests/unit/utils/test_helpers.py
Normal 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])
|
||||
387
backend/tests/unit/utils/test_malaysian_validators.py
Normal file
387
backend/tests/unit/utils/test_malaysian_validators.py
Normal 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'])
|
||||
Reference in New Issue
Block a user