""" 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])