project initialization
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

View File

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

View File

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