project initialization
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
This commit is contained in:
0
backend/tests/unit/utils/__init__.py
Normal file
0
backend/tests/unit/utils/__init__.py
Normal file
461
backend/tests/unit/utils/test_helpers.py
Normal file
461
backend/tests/unit/utils/test_helpers.py
Normal file
@@ -0,0 +1,461 @@
|
||||
"""
|
||||
Unit tests for General Helper Utilities
|
||||
|
||||
Tests for general utility functions:
|
||||
- Date/time helpers
|
||||
- String helpers
|
||||
- Number helpers
|
||||
- File helpers
|
||||
- Security helpers
|
||||
|
||||
Author: Claude
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from backend.src.core.utils.helpers import (
|
||||
format_datetime,
|
||||
parse_date_string,
|
||||
generate_unique_id,
|
||||
sanitize_filename,
|
||||
calculate_percentage,
|
||||
format_currency,
|
||||
truncate_text,
|
||||
validate_email,
|
||||
generate_random_string,
|
||||
hash_password,
|
||||
verify_password,
|
||||
get_file_extension,
|
||||
format_file_size,
|
||||
is_valid_json,
|
||||
flatten_dict,
|
||||
merge_dicts,
|
||||
retry_function,
|
||||
cache_result
|
||||
)
|
||||
|
||||
|
||||
class HelperUtilitiesTest(TestCase):
|
||||
"""Test cases for helper utilities"""
|
||||
|
||||
def test_format_datetime(self):
|
||||
"""Test datetime formatting"""
|
||||
test_datetime = datetime(2024, 1, 15, 14, 30, 45, tzinfo=timezone.utc)
|
||||
|
||||
# Test default formatting
|
||||
formatted = format_datetime(test_datetime)
|
||||
self.assertIn('2024', formatted)
|
||||
self.assertIn('14:30', formatted)
|
||||
|
||||
# Test custom formatting
|
||||
custom_format = format_datetime(test_datetime, '%Y-%m-%d')
|
||||
self.assertEqual(custom_format, '2024-01-15')
|
||||
|
||||
# Test timezone conversion
|
||||
local_format = format_datetime(test_datetime, timezone_name='Asia/Kuala_Lumpur')
|
||||
self.assertIn('22:30', local_format) # UTC+8
|
||||
|
||||
def test_parse_date_string(self):
|
||||
"""Test date string parsing"""
|
||||
test_cases = [
|
||||
{'input': '2024-01-15', 'expected': date(2024, 1, 15)},
|
||||
{'input': '15/01/2024', 'expected': date(2024, 1, 15)},
|
||||
{'input': '01-15-2024', 'expected': date(2024, 1, 15)},
|
||||
{'input': '20240115', 'expected': date(2024, 1, 15)},
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
result = parse_date_string(case['input'])
|
||||
self.assertEqual(result, case['expected'])
|
||||
|
||||
def test_parse_date_string_invalid(self):
|
||||
"""Test invalid date string parsing"""
|
||||
invalid_dates = [
|
||||
'invalid-date',
|
||||
'2024-13-01', # Invalid month
|
||||
'2024-02-30', # Invalid day
|
||||
'2024/02/30', # Invalid format
|
||||
]
|
||||
|
||||
for date_str in invalid_dates:
|
||||
with self.assertRaises(Exception):
|
||||
parse_date_string(date_str)
|
||||
|
||||
def test_generate_unique_id(self):
|
||||
"""Test unique ID generation"""
|
||||
# Test default generation
|
||||
id1 = generate_unique_id()
|
||||
id2 = generate_unique_id()
|
||||
self.assertNotEqual(id1, id2)
|
||||
self.assertEqual(len(id1), 36) # UUID length
|
||||
|
||||
# Test with prefix
|
||||
prefixed_id = generate_unique_id(prefix='USR')
|
||||
self.assertTrue(prefixed_id.startswith('USR_'))
|
||||
|
||||
# Test with custom length
|
||||
short_id = generate_unique_id(length=8)
|
||||
self.assertEqual(len(short_id), 8)
|
||||
|
||||
def test_sanitize_filename(self):
|
||||
"""Test filename sanitization"""
|
||||
test_cases = [
|
||||
{
|
||||
'input': 'test file.txt',
|
||||
'expected': 'test_file.txt'
|
||||
},
|
||||
{
|
||||
'input': 'my*document?.pdf',
|
||||
'expected': 'my_document.pdf'
|
||||
},
|
||||
{
|
||||
'input': ' spaces file .jpg ',
|
||||
'expected': 'spaces_file.jpg'
|
||||
},
|
||||
{
|
||||
'input': '../../../malicious/path.txt',
|
||||
'expected': 'malicious_path.txt'
|
||||
}
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
result = sanitize_filename(case['input'])
|
||||
self.assertEqual(result, case['expected'])
|
||||
|
||||
def test_calculate_percentage(self):
|
||||
"""Test percentage calculation"""
|
||||
test_cases = [
|
||||
{'part': 50, 'total': 100, 'expected': 50.0},
|
||||
{'part': 25, 'total': 200, 'expected': 12.5},
|
||||
{'part': 0, 'total': 100, 'expected': 0.0},
|
||||
{'part': 100, 'total': 100, 'expected': 100.0},
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
result = calculate_percentage(case['part'], case['total'])
|
||||
self.assertEqual(result, case['expected'])
|
||||
|
||||
def test_calculate_percentage_invalid(self):
|
||||
"""Test percentage calculation with invalid inputs"""
|
||||
# Division by zero
|
||||
with self.assertRaises(Exception):
|
||||
calculate_percentage(50, 0)
|
||||
|
||||
# Negative values
|
||||
with self.assertRaises(Exception):
|
||||
calculate_percentage(-10, 100)
|
||||
|
||||
def test_format_currency(self):
|
||||
"""Test currency formatting"""
|
||||
amount = Decimal('1234.56')
|
||||
|
||||
# Test default formatting (MYR)
|
||||
formatted = format_currency(amount)
|
||||
self.assertEqual(formatted, 'RM 1,234.56')
|
||||
|
||||
# Test different currency
|
||||
usd_formatted = format_currency(amount, currency='USD')
|
||||
self.assertEqual(usd_formatted, '$ 1,234.56')
|
||||
|
||||
# Test custom locale
|
||||
custom_locale = format_currency(amount, locale='en_US')
|
||||
self.assertIn('$', custom_locale)
|
||||
|
||||
# Test no decimals
|
||||
no_decimals = format_currency(amount, decimals=0)
|
||||
self.assertEqual(no_decimals, 'RM 1,235')
|
||||
|
||||
def test_truncate_text(self):
|
||||
"""Test text truncation"""
|
||||
text = "This is a long text that needs to be truncated"
|
||||
|
||||
# Test basic truncation
|
||||
truncated = truncate_text(text, 20)
|
||||
self.assertEqual(len(truncated), 20)
|
||||
self.assertTrue(truncated.endswith('...'))
|
||||
|
||||
# Test with custom suffix
|
||||
custom_suffix = truncate_text(text, 15, suffix=' [more]')
|
||||
self.assertTrue(custom_suffix.endswith(' [more]'))
|
||||
|
||||
# Test text shorter than limit
|
||||
short_text = "Short text"
|
||||
result = truncate_text(short_text, 20)
|
||||
self.assertEqual(result, short_text)
|
||||
|
||||
def test_validate_email(self):
|
||||
"""Test email validation"""
|
||||
valid_emails = [
|
||||
'user@example.com',
|
||||
'test.email+tag@domain.co.uk',
|
||||
'user_name@sub.domain.com',
|
||||
'123user@example.org'
|
||||
]
|
||||
|
||||
invalid_emails = [
|
||||
'invalid-email',
|
||||
'@example.com',
|
||||
'user@',
|
||||
'user@.com',
|
||||
'user..name@example.com',
|
||||
'user@example..com'
|
||||
]
|
||||
|
||||
for email in valid_emails:
|
||||
self.assertTrue(validate_email(email))
|
||||
|
||||
for email in invalid_emails:
|
||||
self.assertFalse(validate_email(email))
|
||||
|
||||
def test_generate_random_string(self):
|
||||
"""Test random string generation"""
|
||||
# Test default length
|
||||
random_str = generate_random_string()
|
||||
self.assertEqual(len(random_str), 12)
|
||||
|
||||
# Test custom length
|
||||
custom_length = generate_random_string(length=20)
|
||||
self.assertEqual(len(custom_length), 20)
|
||||
|
||||
# Test different character sets
|
||||
numeric = generate_random_string(length=10, chars='0123456789')
|
||||
self.assertTrue(numeric.isdigit())
|
||||
|
||||
# Test uniqueness
|
||||
str1 = generate_random_string(length=20)
|
||||
str2 = generate_random_string(length=20)
|
||||
self.assertNotEqual(str1, str2)
|
||||
|
||||
def test_hash_password(self):
|
||||
"""Test password hashing"""
|
||||
password = 'test_password_123'
|
||||
|
||||
# Test password hashing
|
||||
hashed = hash_password(password)
|
||||
self.assertNotEqual(hashed, password)
|
||||
self.assertIn('$', hashed) # bcrypt hash format
|
||||
|
||||
# Test same password produces different hashes (salt)
|
||||
hashed2 = hash_password(password)
|
||||
self.assertNotEqual(hashed, hashed2)
|
||||
|
||||
def test_verify_password(self):
|
||||
"""Test password verification"""
|
||||
password = 'test_password_123'
|
||||
hashed = hash_password(password)
|
||||
|
||||
# Test correct password
|
||||
self.assertTrue(verify_password(password, hashed))
|
||||
|
||||
# Test incorrect password
|
||||
self.assertFalse(verify_password('wrong_password', hashed))
|
||||
|
||||
# Test invalid hash
|
||||
self.assertFalse(verify_password(password, 'invalid_hash'))
|
||||
|
||||
def test_get_file_extension(self):
|
||||
"""Test file extension extraction"""
|
||||
test_cases = [
|
||||
{'input': 'document.pdf', 'expected': '.pdf'},
|
||||
{'input': 'image.JPG', 'expected': '.jpg'},
|
||||
{'input': 'archive.tar.gz', 'expected': '.gz'},
|
||||
{'input': 'no_extension', 'expected': ''},
|
||||
{'input': '.hidden_file', 'expected': ''},
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
result = get_file_extension(case['input'])
|
||||
self.assertEqual(result.lower(), case['expected'].lower())
|
||||
|
||||
def test_format_file_size(self):
|
||||
"""Test file size formatting"""
|
||||
test_cases = [
|
||||
{'bytes': 500, 'expected': '500 B'},
|
||||
{'bytes': 1024, 'expected': '1 KB'},
|
||||
{'bytes': 1536, 'expected': '1.5 KB'},
|
||||
{'bytes': 1048576, 'expected': '1 MB'},
|
||||
{'bytes': 1073741824, 'expected': '1 GB'},
|
||||
{'bytes': 1099511627776, 'expected': '1 TB'},
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
result = format_file_size(case['bytes'])
|
||||
self.assertEqual(result, case['expected'])
|
||||
|
||||
def test_is_valid_json(self):
|
||||
"""Test JSON validation"""
|
||||
valid_jsons = [
|
||||
'{"key": "value"}',
|
||||
'[]',
|
||||
'null',
|
||||
'123',
|
||||
'"string"',
|
||||
'{"nested": {"key": "value"}}'
|
||||
]
|
||||
|
||||
invalid_jsons = [
|
||||
'{invalid json}',
|
||||
'undefined',
|
||||
'function() {}',
|
||||
'{key: "value"}', # Unquoted key
|
||||
'["unclosed array"',
|
||||
]
|
||||
|
||||
for json_str in valid_jsons:
|
||||
self.assertTrue(is_valid_json(json_str))
|
||||
|
||||
for json_str in invalid_jsons:
|
||||
self.assertFalse(is_valid_json(json_str))
|
||||
|
||||
def test_flatten_dict(self):
|
||||
"""Test dictionary flattening"""
|
||||
nested_dict = {
|
||||
'user': {
|
||||
'name': 'John',
|
||||
'profile': {
|
||||
'age': 30,
|
||||
'city': 'KL'
|
||||
}
|
||||
},
|
||||
'settings': {
|
||||
'theme': 'dark',
|
||||
'notifications': True
|
||||
}
|
||||
}
|
||||
|
||||
flattened = flatten_dict(nested_dict)
|
||||
|
||||
expected_keys = [
|
||||
'user_name',
|
||||
'user_profile_age',
|
||||
'user_profile_city',
|
||||
'settings_theme',
|
||||
'settings_notifications'
|
||||
]
|
||||
|
||||
for key in expected_keys:
|
||||
self.assertIn(key, flattened)
|
||||
|
||||
self.assertEqual(flattened['user_name'], 'John')
|
||||
self.assertEqual(flattened['user_profile_age'], 30)
|
||||
|
||||
def test_merge_dicts(self):
|
||||
"""Test dictionary merging"""
|
||||
dict1 = {'a': 1, 'b': 2, 'c': 3}
|
||||
dict2 = {'b': 20, 'd': 4, 'e': 5}
|
||||
|
||||
merged = merge_dicts(dict1, dict2)
|
||||
|
||||
self.assertEqual(merged['a'], 1) # From dict1
|
||||
self.assertEqual(merged['b'], 20) # From dict2 (overwritten)
|
||||
self.assertEqual(merged['c'], 3) # From dict1
|
||||
self.assertEqual(merged['d'], 4) # From dict2
|
||||
self.assertEqual(merged['e'], 5) # From dict2
|
||||
|
||||
def test_retry_function(self):
|
||||
"""Test function retry mechanism"""
|
||||
# Test successful execution
|
||||
def successful_function():
|
||||
return "success"
|
||||
|
||||
result = retry_function(successful_function, max_retries=3)
|
||||
self.assertEqual(result, "success")
|
||||
|
||||
# Test function that fails then succeeds
|
||||
call_count = 0
|
||||
|
||||
def flaky_function():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count < 3:
|
||||
raise Exception("Temporary failure")
|
||||
return "eventual_success"
|
||||
|
||||
result = retry_function(flaky_function, max_retries=5)
|
||||
self.assertEqual(result, "eventual_success")
|
||||
self.assertEqual(call_count, 3)
|
||||
|
||||
# Test function that always fails
|
||||
def failing_function():
|
||||
raise Exception("Permanent failure")
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
retry_function(failing_function, max_retries=3)
|
||||
|
||||
def test_cache_result(self):
|
||||
"""Test result caching decorator"""
|
||||
# Create a function that counts calls
|
||||
call_count = 0
|
||||
|
||||
@cache_result(timeout=60) # 60 second cache
|
||||
def expensive_function(x, y):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
return x + y
|
||||
|
||||
# First call should execute function
|
||||
result1 = expensive_function(2, 3)
|
||||
self.assertEqual(result1, 5)
|
||||
self.assertEqual(call_count, 1)
|
||||
|
||||
# Second call with same arguments should use cache
|
||||
result2 = expensive_function(2, 3)
|
||||
self.assertEqual(result2, 5)
|
||||
self.assertEqual(call_count, 1) # No additional call
|
||||
|
||||
# Call with different arguments should execute function
|
||||
result3 = expensive_function(3, 4)
|
||||
self.assertEqual(result3, 7)
|
||||
self.assertEqual(call_count, 2)
|
||||
|
||||
def test_decimal_conversion(self):
|
||||
"""Test decimal conversion utilities"""
|
||||
# Test string to decimal
|
||||
decimal_value = Decimal('123.45')
|
||||
self.assertEqual(decimal_value, Decimal('123.45'))
|
||||
|
||||
# Test float to decimal (with precision warning)
|
||||
float_value = 123.45
|
||||
decimal_from_float = Decimal(str(float_value))
|
||||
self.assertEqual(decimal_from_float, Decimal('123.45'))
|
||||
|
||||
def test_timezone_handling(self):
|
||||
"""Test timezone handling utilities"""
|
||||
# Test timezone aware datetime
|
||||
utc_now = timezone.now()
|
||||
self.assertIsNotNone(utc_now.tzinfo)
|
||||
|
||||
# Test timezone conversion
|
||||
kl_time = format_datetime(utc_now, timezone_name='Asia/Kuala_Lumpur')
|
||||
self.assertIn('+08', kl_time)
|
||||
|
||||
def test_string_manipulation(self):
|
||||
"""Test string manipulation utilities"""
|
||||
# Test string cleaning
|
||||
dirty_string = " Hello World \n\t"
|
||||
clean_string = " ".join(dirty_string.split())
|
||||
self.assertEqual(clean_string, "Hello World")
|
||||
|
||||
# Test case conversion
|
||||
test_string = "Hello World"
|
||||
self.assertEqual(test_string.lower(), "hello world")
|
||||
self.assertEqual(test_string.upper(), "HELLO WORLD")
|
||||
self.assertEqual(test_string.title(), "Hello World")
|
||||
|
||||
def test_list_operations(self):
|
||||
"""Test list operation utilities"""
|
||||
# Test list deduplication
|
||||
duplicate_list = [1, 2, 2, 3, 4, 4, 5]
|
||||
unique_list = list(set(duplicate_list))
|
||||
self.assertEqual(len(unique_list), 5)
|
||||
|
||||
# Test list sorting
|
||||
unsorted_list = [3, 1, 4, 1, 5, 9, 2, 6]
|
||||
sorted_list = sorted(unsorted_list)
|
||||
self.assertEqual(sorted_list, [1, 1, 2, 3, 4, 5, 6, 9])
|
||||
387
backend/tests/unit/utils/test_malaysian_validators.py
Normal file
387
backend/tests/unit/utils/test_malaysian_validators.py
Normal file
@@ -0,0 +1,387 @@
|
||||
"""
|
||||
Unit tests for Malaysian Validators
|
||||
|
||||
Tests for Malaysian-specific validation utilities:
|
||||
- IC number validation
|
||||
- Phone number validation
|
||||
- Business registration validation
|
||||
- Address validation
|
||||
- SST calculation
|
||||
|
||||
Author: Claude
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from backend.src.core.utils.malaysian_validators import (
|
||||
validate_ic_number,
|
||||
validate_phone_number,
|
||||
validate_business_registration,
|
||||
validate_malaysian_address,
|
||||
calculate_sst,
|
||||
validate_postal_code,
|
||||
format_malaysian_phone,
|
||||
get_malaysian_states
|
||||
)
|
||||
|
||||
|
||||
class MalaysianValidatorsTest(TestCase):
|
||||
"""Test cases for Malaysian validators"""
|
||||
|
||||
def test_validate_ic_number_valid(self):
|
||||
"""Test valid Malaysian IC number validation"""
|
||||
valid_ic_numbers = [
|
||||
'000101-01-0001', # Valid format
|
||||
'900101-10-1234', # Valid format
|
||||
'851231-12-5678', # Valid format
|
||||
]
|
||||
|
||||
for ic_number in valid_ic_numbers:
|
||||
result = validate_ic_number(ic_number)
|
||||
self.assertTrue(result['is_valid'])
|
||||
self.assertEqual(result['normalized'], ic_number)
|
||||
|
||||
def test_validate_ic_number_invalid(self):
|
||||
"""Test invalid Malaysian IC number validation"""
|
||||
invalid_ic_numbers = [
|
||||
'123', # Too short
|
||||
'000101-01-000', # Wrong length
|
||||
'000101-01-00012', # Wrong length
|
||||
'000101-01-000A', # Contains letter
|
||||
'000101/01/0001', # Wrong separator
|
||||
'00-01-01-0001', # Wrong format
|
||||
]
|
||||
|
||||
for ic_number in invalid_ic_numbers:
|
||||
result = validate_ic_number(ic_number)
|
||||
self.assertFalse(result['is_valid'])
|
||||
self.assertIsNotNone(result.get('error'))
|
||||
|
||||
def test_validate_phone_number_valid(self):
|
||||
"""Test valid Malaysian phone number validation"""
|
||||
valid_phones = [
|
||||
'+60123456789', # Standard mobile
|
||||
'0123456789', # Mobile without country code
|
||||
'+60312345678', # Landline
|
||||
'0312345678', # Landline without country code
|
||||
'+60111234567', # New mobile prefix
|
||||
]
|
||||
|
||||
for phone in valid_phones:
|
||||
result = validate_phone_number(phone)
|
||||
self.assertTrue(result['is_valid'])
|
||||
self.assertEqual(result['type'], 'mobile' if phone.startswith('01') else 'landline')
|
||||
|
||||
def test_validate_phone_number_invalid(self):
|
||||
"""Test invalid Malaysian phone number validation"""
|
||||
invalid_phones = [
|
||||
'12345', # Too short
|
||||
'0123456789A', # Contains letter
|
||||
'+6512345678', # Singapore number
|
||||
'123456789012', # Too long
|
||||
'0112345678', # Invalid prefix
|
||||
]
|
||||
|
||||
for phone in invalid_phones:
|
||||
result = validate_phone_number(phone)
|
||||
self.assertFalse(result['is_valid'])
|
||||
self.assertIsNotNone(result.get('error'))
|
||||
|
||||
def test_validate_business_registration_valid(self):
|
||||
"""Test valid business registration validation"""
|
||||
valid_registrations = [
|
||||
'202401000001', # Company registration
|
||||
'001234567-K', # Business registration
|
||||
'SM1234567-K', # Small medium enterprise
|
||||
]
|
||||
|
||||
for reg in valid_registrations:
|
||||
result = validate_business_registration(reg)
|
||||
self.assertTrue(result['is_valid'])
|
||||
self.assertIsNotNone(result.get('type'))
|
||||
|
||||
def test_validate_business_registration_invalid(self):
|
||||
"""Test invalid business registration validation"""
|
||||
invalid_registrations = [
|
||||
'123', # Too short
|
||||
'20240100000', # Missing check digit
|
||||
'202401000001A', # Contains letter
|
||||
'0012345678-K', # Too long
|
||||
]
|
||||
|
||||
for reg in invalid_registrations:
|
||||
result = validate_business_registration(reg)
|
||||
self.assertFalse(result['is_valid'])
|
||||
self.assertIsNotNone(result.get('error'))
|
||||
|
||||
def test_validate_malaysian_address_valid(self):
|
||||
"""Test valid Malaysian address validation"""
|
||||
valid_addresses = [
|
||||
{
|
||||
'address': '123 Test Street',
|
||||
'city': 'Kuala Lumpur',
|
||||
'state': 'KUL',
|
||||
'postal_code': '50000'
|
||||
},
|
||||
{
|
||||
'address': '456 Jalan Merdeka',
|
||||
'city': 'Penang',
|
||||
'state': 'PNG',
|
||||
'postal_code': '10000'
|
||||
}
|
||||
]
|
||||
|
||||
for address in valid_addresses:
|
||||
result = validate_malaysian_address(address)
|
||||
self.assertTrue(result['is_valid'])
|
||||
|
||||
def test_validate_malaysian_address_invalid(self):
|
||||
"""Test invalid Malaysian address validation"""
|
||||
invalid_addresses = [
|
||||
{
|
||||
'address': '', # Empty address
|
||||
'city': 'Kuala Lumpur',
|
||||
'state': 'KUL',
|
||||
'postal_code': '50000'
|
||||
},
|
||||
{
|
||||
'address': '123 Test Street',
|
||||
'city': '', # Empty city
|
||||
'state': 'KUL',
|
||||
'postal_code': '50000'
|
||||
},
|
||||
{
|
||||
'address': '123 Test Street',
|
||||
'city': 'Kuala Lumpur',
|
||||
'state': 'XX', # Invalid state
|
||||
'postal_code': '50000'
|
||||
},
|
||||
{
|
||||
'address': '123 Test Street',
|
||||
'city': 'Kuala Lumpur',
|
||||
'state': 'KUL',
|
||||
'postal_code': '123' # Invalid postal code
|
||||
}
|
||||
]
|
||||
|
||||
for address in invalid_addresses:
|
||||
result = validate_malaysian_address(address)
|
||||
self.assertFalse(result['is_valid'])
|
||||
self.assertIsNotNone(result.get('errors'))
|
||||
|
||||
def test_calculate_sst(self):
|
||||
"""Test SST calculation"""
|
||||
test_cases = [
|
||||
{'amount': 100.00, 'expected_sst': 6.00}, # 6% SST
|
||||
{'amount': 50.00, 'expected_sst': 3.00}, # 6% SST
|
||||
{'amount': 0.00, 'expected_sst': 0.00}, # Zero amount
|
||||
{'amount': 999.99, 'expected_sst': 59.9994}, # High amount
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
sst_amount = calculate_sst(case['amount'])
|
||||
self.assertAlmostEqual(sst_amount, case['expected_sst'], places=4)
|
||||
|
||||
def test_calculate_sst_invalid(self):
|
||||
"""Test SST calculation with invalid inputs"""
|
||||
invalid_cases = [
|
||||
-100.00, # Negative amount
|
||||
None, # None value
|
||||
'invalid', # String value
|
||||
]
|
||||
|
||||
for amount in invalid_cases:
|
||||
with self.assertRaises(Exception):
|
||||
calculate_sst(amount)
|
||||
|
||||
def test_validate_postal_code_valid(self):
|
||||
"""Test valid postal code validation"""
|
||||
valid_postal_codes = [
|
||||
'50000', # KL postal code
|
||||
'10000', # Penang postal code
|
||||
'80000', # Johor Bahru postal code
|
||||
'97000', # Sarawak postal code
|
||||
]
|
||||
|
||||
for postal_code in valid_postal_codes:
|
||||
result = validate_postal_code(postal_code)
|
||||
self.assertTrue(result['is_valid'])
|
||||
self.assertEqual(result['state'], result.get('state'))
|
||||
|
||||
def test_validate_postal_code_invalid(self):
|
||||
"""Test invalid postal code validation"""
|
||||
invalid_postal_codes = [
|
||||
'1234', # Too short
|
||||
'123456', # Too long
|
||||
'ABCDE', # Contains letters
|
||||
'00000', # Invalid range
|
||||
'99999', # Invalid range
|
||||
]
|
||||
|
||||
for postal_code in invalid_postal_codes:
|
||||
result = validate_postal_code(postal_code)
|
||||
self.assertFalse(result['is_valid'])
|
||||
self.assertIsNotNone(result.get('error'))
|
||||
|
||||
def test_format_malaysian_phone(self):
|
||||
"""Test Malaysian phone number formatting"""
|
||||
test_cases = [
|
||||
{'input': '0123456789', 'expected': '+6012-3456789'},
|
||||
{'input': '+60123456789', 'expected': '+6012-3456789'},
|
||||
{'input': '0312345678', 'expected': '+603-12345678'},
|
||||
{'input': '+60312345678', 'expected': '+603-12345678'},
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
formatted = format_malaysian_phone(case['input'])
|
||||
self.assertEqual(formatted, case['expected'])
|
||||
|
||||
def test_format_malaysian_phone_invalid(self):
|
||||
"""Test formatting invalid phone numbers"""
|
||||
invalid_phones = [
|
||||
'12345', # Too short
|
||||
'invalid', # Non-numeric
|
||||
'6512345678', # Singapore number
|
||||
]
|
||||
|
||||
for phone in invalid_phones:
|
||||
result = format_malaysian_phone(phone)
|
||||
self.assertEqual(result, phone) # Should return original if invalid
|
||||
|
||||
def test_get_malaysian_states(self):
|
||||
"""Test getting Malaysian states"""
|
||||
states = get_malaysian_states()
|
||||
|
||||
# Check if all expected states are present
|
||||
expected_states = [
|
||||
'Johor', 'Kedah', 'Kelantan', 'Malacca', 'Negeri Sembilan',
|
||||
'Pahang', 'Perak', 'Perlis', 'Penang', 'Sabah', 'Sarawak',
|
||||
'Selangor', 'Terengganu', 'Kuala Lumpur', 'Labuan', 'Putrajaya'
|
||||
]
|
||||
|
||||
for state in expected_states:
|
||||
self.assertIn(state, states)
|
||||
|
||||
# Check state codes
|
||||
self.assertEqual(states['Kuala Lumpur'], 'KUL')
|
||||
self.assertEqual(states['Penang'], 'PNG')
|
||||
self.assertEqual(states['Johor'], 'JHR')
|
||||
|
||||
def test_ic_number_structure_validation(self):
|
||||
"""Test IC number structure validation"""
|
||||
# Test age calculation from IC
|
||||
ic_1990 = '900101-01-0001' # Born 1990
|
||||
result = validate_ic_number(ic_1990)
|
||||
self.assertTrue(result['is_valid'])
|
||||
self.assertEqual(result['birth_year'], 1990)
|
||||
self.assertEqual(result['birth_date'], '1990-01-01')
|
||||
|
||||
# Test gender from IC (last digit: odd = male, even = female)
|
||||
ic_male = '900101-01-0001' # Odd last digit
|
||||
ic_female = '900101-01-0002' # Even last digit
|
||||
|
||||
result_male = validate_ic_number(ic_male)
|
||||
result_female = validate_ic_number(ic_female)
|
||||
|
||||
self.assertEqual(result_male['gender'], 'male')
|
||||
self.assertEqual(result_female['gender'], 'female')
|
||||
|
||||
def test_phone_number_type_detection(self):
|
||||
"""Test phone number type detection"""
|
||||
mobile_numbers = [
|
||||
'0123456789', # Maxis
|
||||
'0198765432', # Celcom
|
||||
'0162345678', # DiGi
|
||||
'0181234567', # U Mobile
|
||||
'01112345678', # Yes 4G
|
||||
]
|
||||
|
||||
landline_numbers = [
|
||||
'0312345678', # KL
|
||||
'0412345678', # Penang
|
||||
'0512345678', # Perak
|
||||
'0612345678', # Melaka
|
||||
'0712345678', # Johor
|
||||
]
|
||||
|
||||
for number in mobile_numbers:
|
||||
result = validate_phone_number(number)
|
||||
self.assertTrue(result['is_valid'])
|
||||
self.assertEqual(result['type'], 'mobile')
|
||||
|
||||
for number in landline_numbers:
|
||||
result = validate_phone_number(number)
|
||||
self.assertTrue(result['is_valid'])
|
||||
self.assertEqual(result['type'], 'landline')
|
||||
|
||||
def test_business_registration_type_detection(self):
|
||||
"""Test business registration type detection"""
|
||||
company_reg = '202401000001' # Company registration
|
||||
business_reg = '001234567-K' # Business registration
|
||||
sme_reg = 'SM1234567-K' # Small medium enterprise
|
||||
|
||||
result_company = validate_business_registration(company_reg)
|
||||
result_business = validate_business_registration(business_reg)
|
||||
result_sme = validate_business_registration(sme_reg)
|
||||
|
||||
self.assertEqual(result_company['type'], 'company')
|
||||
self.assertEqual(result_business['type'], 'business')
|
||||
self.assertEqual(result_sme['type'], 'sme')
|
||||
|
||||
def test_address_component_validation(self):
|
||||
"""Test individual address component validation"""
|
||||
# Test state code validation
|
||||
valid_states = ['KUL', 'PNG', 'JHR', 'KDH', 'KTN']
|
||||
invalid_states = ['XX', 'ABC', '123']
|
||||
|
||||
for state in valid_states:
|
||||
address = {
|
||||
'address': '123 Test Street',
|
||||
'city': 'Test City',
|
||||
'state': state,
|
||||
'postal_code': '50000'
|
||||
}
|
||||
result = validate_malaysian_address(address)
|
||||
self.assertTrue(result['is_valid'])
|
||||
|
||||
for state in invalid_states:
|
||||
address = {
|
||||
'address': '123 Test Street',
|
||||
'city': 'Test City',
|
||||
'state': state,
|
||||
'postal_code': '50000'
|
||||
}
|
||||
result = validate_malaysian_address(address)
|
||||
self.assertFalse(result['is_valid'])
|
||||
|
||||
def test_sst_edge_cases(self):
|
||||
"""Test SST calculation edge cases"""
|
||||
# Test very small amounts
|
||||
sst_small = calculate_sst(0.01)
|
||||
self.assertAlmostEqual(sst_small, 0.0006, places=4)
|
||||
|
||||
# Test very large amounts
|
||||
sst_large = calculate_sst(1000000.00)
|
||||
self.assertEqual(sst_large, 60000.00)
|
||||
|
||||
# Test decimal places
|
||||
sst_decimal = calculate_sst(123.45)
|
||||
self.assertAlmostEqual(sst_decimal, 7.407, places=4)
|
||||
|
||||
def test_postal_code_state_mapping(self):
|
||||
"""Test postal code to state mapping"""
|
||||
# Test known postal code ranges
|
||||
test_cases = [
|
||||
{'postal_code': '50000', 'expected_state': 'KUL'}, # KL
|
||||
{'postal_code': '10000', 'expected_state': 'PNG'}, # Penang
|
||||
{'postal_code': '80000', 'expected_state': 'JHR'}, # Johor
|
||||
{'postal_code': '09000', 'expected_state': 'KDH'}, # Kedah
|
||||
{'postal_code': '98000', 'expected_state': 'SBH'}, # Sabah
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
result = validate_postal_code(case['postal_code'])
|
||||
self.assertTrue(result['is_valid'])
|
||||
self.assertEqual(result['state'], case['expected_state'])
|
||||
Reference in New Issue
Block a user