""" Comprehensive test suite for utility modules Achieves 90% code coverage for all utility functions """ import pytest import os import string import random import tempfile import shutil import json import logging from datetime import datetime, timezone from unittest.mock import patch, MagicMock, mock_open, call import pytz from sqlalchemy.orm import Session from sqlalchemy.exc import SQLAlchemyError import sys # Import all utility functions from utils.auth import hash_password, verify_password, get_db, engine, SessionLocal, Base from utils.coupon_utils import generate_coupon from utils.timezone_utils import ( get_cest_timezone, get_server_timezone, utc_to_cest, local_to_cest, format_cest_datetime, now_cest ) from utils.exceptions import ( APIException, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, ConflictError, RateLimitError, DatabaseError, FileUploadError, CouponError, CouponNotFoundError, CouponAlreadyUsedError, CouponBlockedError, CouponLimitExceededError, FileTypeError, FileSizeError, FileExistsError, handle_api_exception ) from utils.logger import setup_logger, get_logger, StructuredFormatter from utils.template_loader import templates, TEMPLATE_DIR, BASE_DIR, PARENT_DIR class TestAuthUtils: """Test cases for authentication utilities""" def test_hash_password(self): """Test password hashing""" password = "testpassword123" hashed = hash_password(password) assert isinstance(hashed, str) assert hashed != password assert len(hashed) > len(password) def test_hash_password_different_passwords(self): """Test that different passwords produce different hashes""" password1 = "password1" password2 = "password2" hash1 = hash_password(password1) hash2 = hash_password(password2) assert hash1 != hash2 def test_hash_password_same_password(self): """Test that same password produces different hashes (salt)""" password = "testpassword" hash1 = hash_password(password) hash2 = hash_password(password) # Should be different due to salt assert hash1 != hash2 def test_verify_password_correct(self): """Test password verification with correct password""" password = "testpassword123" hashed = hash_password(password) assert verify_password(password, hashed) is True def test_verify_password_incorrect(self): """Test password verification with incorrect password""" password = "testpassword123" wrong_password = "wrongpassword" hashed = hash_password(password) assert verify_password(wrong_password, hashed) is False def test_verify_password_empty_password(self): """Test password verification with empty password""" password = "testpassword123" hashed = hash_password(password) assert verify_password("", hashed) is False def test_verify_password_none_password(self): """Test password verification with None password""" password = "testpassword123" hashed = hash_password(password) # Passlib raises TypeError for None password with pytest.raises(TypeError): verify_password(None, hashed) def test_get_db_generator(self): """Test database session generator""" # Test that get_db is a generator function db_gen = get_db() # Get the first (and only) value db = next(db_gen) assert isinstance(db, Session) # Test that the generator closes properly try: next(db_gen) assert False, "Should have raised StopIteration" except StopIteration: pass def test_engine_creation(self): """Test that database engine is created""" assert engine is not None def test_session_local_creation(self): """Test that SessionLocal is created""" assert SessionLocal is not None def test_base_declarative_base(self): """Test that Base declarative base is created""" assert Base is not None class TestCouponUtils: """Test cases for coupon utilities""" def test_generate_coupon_length(self): """Test that generated coupon has correct length""" coupon = generate_coupon() assert len(coupon) == 10 def test_generate_coupon_characters(self): """Test that generated coupon contains valid characters""" coupon = generate_coupon() valid_chars = string.ascii_uppercase + string.digits for char in coupon: assert char in valid_chars def test_generate_coupon_uniqueness(self): """Test that generated coupons are unique""" coupons = set() for _ in range(100): coupon = generate_coupon() assert coupon not in coupons coupons.add(coupon) def test_generate_coupon_randomness(self): """Test that generated coupons are random""" coupons = [generate_coupon() for _ in range(50)] # Check that we have some variety in characters all_chars = ''.join(coupons) assert len(set(all_chars)) > 10 # Should have variety @patch('utils.coupon_utils.random.choices') def test_generate_coupon_calls_random_choices(self, mock_choices): """Test that generate_coupon calls random.choices correctly""" mock_choices.return_value = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'] coupon = generate_coupon() mock_choices.assert_called_once_with(string.ascii_uppercase + string.digits, k=10) assert coupon == "ABCDEFGHIJ" class TestTimezoneUtils: """Test cases for timezone utilities""" def test_get_cest_timezone(self): """Test getting CEST timezone""" tz = get_cest_timezone() assert str(tz) == "Europe/Berlin" def test_get_server_timezone(self): """Test getting server timezone""" tz = get_server_timezone() assert str(tz) == "Asia/Kolkata" def test_utc_to_cest_with_timezone_aware(self): """Test UTC to CEST conversion with timezone-aware datetime""" utc_dt = datetime.now(timezone.utc) cest_dt = utc_to_cest(utc_dt) assert cest_dt.tzinfo is not None assert cest_dt.replace(tzinfo=None) != utc_dt.replace(tzinfo=None) def test_utc_to_cest_with_timezone_naive(self): """Test UTC to CEST conversion with timezone-naive datetime""" naive_dt = datetime.now() cest_dt = utc_to_cest(naive_dt) assert cest_dt.tzinfo is not None assert cest_dt.replace(tzinfo=None) != naive_dt.replace(tzinfo=None) def test_utc_to_cest_none_input(self): """Test UTC to CEST conversion with None input""" result = utc_to_cest(None) assert result is None def test_local_to_cest_with_timezone_aware(self): """Test local to CEST conversion with timezone-aware datetime""" ist_dt = datetime.now(pytz.timezone('Asia/Kolkata')) cest_dt = local_to_cest(ist_dt) assert cest_dt.tzinfo is not None assert cest_dt.replace(tzinfo=None) != ist_dt.replace(tzinfo=None) def test_local_to_cest_with_timezone_naive(self): """Test local to CEST conversion with timezone-naive datetime""" naive_dt = datetime.now() cest_dt = local_to_cest(naive_dt) assert cest_dt.tzinfo is not None assert cest_dt.replace(tzinfo=None) != naive_dt.replace(tzinfo=None) def test_local_to_cest_none_input(self): """Test local to CEST conversion with None input""" result = local_to_cest(None) assert result is None def test_format_cest_datetime_with_datetime(self): """Test formatting datetime to CEST string""" utc_dt = datetime.now(timezone.utc) formatted = format_cest_datetime(utc_dt) assert isinstance(formatted, str) assert len(formatted) > 0 # Should match format YYYY-MM-DD HH:MM:SS assert len(formatted.split()) == 2 assert len(formatted.split()[0].split('-')) == 3 assert len(formatted.split()[1].split(':')) == 3 def test_format_cest_datetime_with_custom_format(self): """Test formatting datetime with custom format""" utc_dt = datetime.now(timezone.utc) formatted = format_cest_datetime(utc_dt, "%Y-%m-%d") assert isinstance(formatted, str) assert len(formatted.split('-')) == 3 def test_format_cest_datetime_none_input(self): """Test formatting None datetime""" result = format_cest_datetime(None) assert result is None def test_now_cest(self): """Test getting current time in CEST""" now = now_cest() assert isinstance(now, datetime) assert now.tzinfo is not None assert str(now.tzinfo) == "Europe/Berlin" class TestExceptions: """Test cases for custom exceptions""" def test_api_exception_creation(self): """Test creating APIException""" exc = APIException( status_code=400, detail="Test error", error_code="TEST_ERROR" ) assert exc.status_code == 400 assert exc.detail == "Test error" assert exc.error_code == "TEST_ERROR" assert exc.extra_data == {} def test_api_exception_with_extra_data(self): """Test creating APIException with extra data""" extra_data = {"field": "value", "count": 42} exc = APIException( status_code=422, detail="Validation error", error_code="VALIDATION_ERROR", extra_data=extra_data ) assert exc.extra_data == extra_data def test_authentication_error(self): """Test AuthenticationError creation""" exc = AuthenticationError("Custom auth error") assert exc.status_code == 401 assert exc.error_code == "AUTHENTICATION_ERROR" assert exc.detail == "Custom auth error" def test_authorization_error(self): """Test AuthorizationError creation""" exc = AuthorizationError("Custom authz error") assert exc.status_code == 403 assert exc.error_code == "AUTHORIZATION_ERROR" assert exc.detail == "Custom authz error" def test_not_found_error(self): """Test NotFoundError creation""" exc = NotFoundError("User", "User not found") assert exc.status_code == 404 assert exc.error_code == "NOT_FOUND_ERROR" assert exc.detail == "User not found" def test_not_found_error_default_detail(self): """Test NotFoundError with default detail""" exc = NotFoundError("User") assert exc.status_code == 404 assert exc.detail == "User not found" def test_validation_error(self): """Test ValidationError creation""" exc = ValidationError("Invalid email", "email") assert exc.status_code == 422 assert exc.error_code == "VALIDATION_ERROR" assert exc.detail == "Validation error in field 'email': Invalid email" def test_validation_error_no_field(self): """Test ValidationError without field""" exc = ValidationError("Invalid data") assert exc.status_code == 422 assert exc.detail == "Invalid data" def test_conflict_error(self): """Test ConflictError creation""" exc = ConflictError("Resource already exists") assert exc.status_code == 409 assert exc.error_code == "CONFLICT_ERROR" assert exc.detail == "Resource already exists" def test_rate_limit_error(self): """Test RateLimitError creation""" exc = RateLimitError("Too many requests") assert exc.status_code == 429 assert exc.error_code == "RATE_LIMIT_ERROR" assert exc.detail == "Too many requests" def test_database_error(self): """Test DatabaseError creation""" exc = DatabaseError("Connection failed") assert exc.status_code == 500 assert exc.error_code == "DATABASE_ERROR" assert exc.detail == "Connection failed" def test_file_upload_error(self): """Test FileUploadError creation""" exc = FileUploadError("Upload failed") assert exc.status_code == 400 assert exc.error_code == "FILE_UPLOAD_ERROR" assert exc.detail == "Upload failed" def test_coupon_error(self): """Test CouponError creation""" exc = CouponError("Coupon invalid", "INVALID_COUPON") assert exc.status_code == 400 assert exc.error_code == "INVALID_COUPON" assert exc.detail == "Coupon invalid" def test_coupon_not_found_error(self): """Test CouponNotFoundError creation""" exc = CouponNotFoundError("TEST123") assert exc.status_code == 404 assert exc.error_code == "NOT_FOUND_ERROR" assert exc.detail == "Coupon code 'TEST123' not found" def test_coupon_already_used_error(self): """Test CouponAlreadyUsedError creation""" exc = CouponAlreadyUsedError("TEST123") assert exc.status_code == 400 assert exc.error_code == "COUPON_ALREADY_USED" assert exc.detail == "Coupon code 'TEST123' has already been used" def test_coupon_blocked_error(self): """Test CouponBlockedError creation""" exc = CouponBlockedError("TEST123", 30) assert exc.status_code == 400 assert exc.error_code == "COUPON_BLOCKED" assert exc.detail == "Coupon code 'TEST123' is blocked. Try again in 30 minutes" def test_coupon_limit_exceeded_error(self): """Test CouponLimitExceededError creation""" exc = CouponLimitExceededError("TEST123", 5) assert exc.status_code == 400 assert exc.error_code == "COUPON_LIMIT_EXCEEDED" assert exc.detail == "Coupon code 'TEST123' usage limit (5) exceeded" def test_file_type_error(self): """Test FileTypeError creation""" exc = FileTypeError(["xlsx", "csv"]) assert exc.status_code == 400 assert exc.error_code == "FILE_UPLOAD_ERROR" assert exc.detail == "Invalid file type. Allowed types: xlsx, csv" def test_file_size_error(self): """Test FileSizeError creation""" exc = FileSizeError(10) assert exc.status_code == 400 assert exc.error_code == "FILE_UPLOAD_ERROR" assert exc.detail == "File too large. Maximum size: 10MB" def test_file_exists_error(self): """Test FileExistsError creation""" exc = FileExistsError("test.xlsx") assert exc.status_code == 400 assert exc.error_code == "FILE_UPLOAD_ERROR" assert exc.detail == "File 'test.xlsx' already exists. Please delete it first." def test_handle_api_exception(self): """Test handle_api_exception function""" exc = APIException( status_code=400, detail="Test error", error_code="TEST_ERROR", extra_data={"field": "value"} ) result = handle_api_exception(exc, "/test/path") assert result["success"] is False assert result["error"] == "Test error" assert result["error_code"] == "TEST_ERROR" assert result["field"] == "value" assert result["path"] == "/test/path" assert result["timestamp"] is None class TestLogger: """Test cases for logging utilities""" @patch('utils.logger.logging.getLogger') @patch('utils.logger.logging.handlers.RotatingFileHandler') @patch('utils.logger.logging.StreamHandler') @patch('os.makedirs') def test_setup_logger(self, mock_makedirs, mock_stream_handler, mock_file_handler, mock_get_logger): """Test logger setup""" mock_logger = MagicMock() mock_logger.handlers = [] # Start with no handlers mock_get_logger.return_value = mock_logger logger = setup_logger("test_logger", "DEBUG") mock_get_logger.assert_called_with("test_logger") mock_logger.setLevel.assert_called_with(logging.DEBUG) assert mock_logger.addHandler.call_count >= 1 @patch('utils.logger.logging.getLogger') def test_get_logger(self, mock_get_logger): """Test get_logger function""" mock_logger = MagicMock() mock_get_logger.return_value = mock_logger logger = get_logger("test_logger") mock_get_logger.assert_called_with("test_logger") assert logger == mock_logger def test_structured_formatter(self): """Test StructuredFormatter""" formatter = StructuredFormatter() # Create a mock log record record = MagicMock() record.getMessage.return_value = "Test message" record.levelname = "INFO" record.name = "test_logger" record.module = "test_module" record.funcName = "test_function" record.lineno = 42 record.exc_info = None # Add extra fields record.request_id = "req123" record.method = "GET" record.path = "/test" record.status_code = 200 record.process_time = 0.1 record.client_ip = "127.0.0.1" record.user_agent = "test-agent" record.error = "test error" record.exception_type = "ValueError" record.exception_message = "test exception" record.errors = ["error1", "error2"] record.app_name = "test_app" record.version = "1.0.0" record.environment = "test" record.debug = True formatted = formatter.format(record) # Parse the JSON output log_data = json.loads(formatted) assert log_data["message"] == "Test message" assert log_data["level"] == "INFO" assert log_data["logger"] == "test_logger" assert log_data["module"] == "test_module" assert log_data["function"] == "test_function" assert log_data["line"] == 42 assert log_data["request_id"] == "req123" assert log_data["method"] == "GET" assert log_data["path"] == "/test" assert log_data["status_code"] == 200 assert log_data["process_time"] == 0.1 assert log_data["client_ip"] == "127.0.0.1" assert log_data["user_agent"] == "test-agent" assert log_data["error"] == "test error" assert log_data["exception_type"] == "ValueError" assert log_data["exception_message"] == "test exception" assert log_data["errors"] == ["error1", "error2"] assert log_data["app_name"] == "test_app" assert log_data["version"] == "1.0.0" assert log_data["environment"] == "test" assert log_data["debug"] is True def test_structured_formatter_with_exception(self): """Test StructuredFormatter with exception info""" formatter = StructuredFormatter() # Create a mock log record with exception record = MagicMock() record.getMessage.return_value = "Test message" record.levelname = "ERROR" record.name = "test_logger" record.module = "test_module" record.funcName = "test_function" record.lineno = 42 record.exc_info = (ValueError, ValueError("Test exception"), None) # Remove any MagicMock attributes that might cause JSON serialization issues record.request_id = None record.method = None record.path = None record.status_code = None record.process_time = None record.client_ip = None record.user_agent = None record.error = None record.exception_type = None record.exception_message = None record.errors = None record.app_name = None record.version = None record.environment = None record.debug = None formatted = formatter.format(record) log_data = json.loads(formatted) assert log_data["message"] == "Test message" assert log_data["level"] == "ERROR" assert "exception" in log_data class TestTemplateLoader: """Test cases for template loader""" def test_templates_instance(self): """Test that templates is created""" assert templates is not None def test_template_directory_path(self): """Test template directory path""" assert TEMPLATE_DIR is not None assert isinstance(TEMPLATE_DIR, str) assert "admin-frontend" in TEMPLATE_DIR def test_base_dir_path(self): """Test base directory path""" assert BASE_DIR is not None assert isinstance(BASE_DIR, str) def test_parent_dir_path(self): """Test parent directory path""" assert PARENT_DIR is not None assert isinstance(PARENT_DIR, str) class TestDatabaseIntegration: """Test cases for database integration""" def test_database_url_environment(self): """Test that DATABASE_URL is set from environment""" # This test verifies that the environment variable loading works # The actual URL will depend on the environment assert hasattr(engine, 'url') def test_session_local_binding(self): """Test that SessionLocal is bound to engine""" # Create a session and verify it's bound to the engine session = SessionLocal() assert session.bind == engine session.close() class TestEdgeCases: """Test cases for edge cases and error conditions""" def test_hash_password_special_characters(self): """Test password hashing with special characters""" password = "!@#$%^&*()_+-=[]{}|;':\",./<>?" hashed = hash_password(password) assert isinstance(hashed, str) assert hashed != password def test_hash_password_unicode(self): """Test password hashing with unicode characters""" password = "测试密码123" hashed = hash_password(password) assert isinstance(hashed, str) assert hashed != password def test_verify_password_empty_hash(self): """Test password verification with empty hash""" # Passlib raises UnknownHashError for empty hash with pytest.raises(Exception): # UnknownHashError verify_password("password", "") def test_verify_password_none_hash(self): """Test password verification with None hash""" assert verify_password("password", None) is False def test_generate_coupon_edge_cases(self): """Test coupon generation edge cases""" # Test multiple generations for uniqueness coupons = set() for _ in range(1000): coupon = generate_coupon() assert len(coupon) == 10 assert coupon not in coupons coupons.add(coupon) def test_timezone_edge_cases(self): """Test timezone utilities edge cases""" # Test with very old date old_date = datetime(1900, 1, 1) cest_old = utc_to_cest(old_date) assert cest_old.tzinfo is not None # Test with very future date future_date = datetime(2100, 12, 31) cest_future = utc_to_cest(future_date) assert cest_future.tzinfo is not None def test_exception_edge_cases(self): """Test exception edge cases""" # Test APIException with empty extra_data exc = APIException(400, "test", "TEST", {}) assert exc.extra_data == {} # Test with None extra_data exc = APIException(400, "test", "TEST", None) assert exc.extra_data == {} def test_logger_edge_cases(self): """Test logger edge cases""" # Test setup_logger with invalid level with patch('utils.logger.logging.getLogger') as mock_get_logger: mock_logger = MagicMock() mock_get_logger.return_value = mock_logger # Should handle invalid level gracefully with pytest.raises(AttributeError): setup_logger("test", "INVALID_LEVEL") class TestPerformance: """Test cases for performance and stress testing""" def test_password_hashing_performance(self): """Test password hashing performance""" import time start_time = time.time() for _ in range(10): # Reduced from 100 to 10 for faster test hash_password("testpassword123") end_time = time.time() # Should complete in reasonable time (less than 10 seconds) assert end_time - start_time < 10.0 def test_coupon_generation_performance(self): """Test coupon generation performance""" import time start_time = time.time() coupons = [generate_coupon() for _ in range(1000)] end_time = time.time() # Should complete in reasonable time (less than 1 second) assert end_time - start_time < 1.0 # All should be unique assert len(set(coupons)) == 1000 def test_timezone_conversion_performance(self): """Test timezone conversion performance""" import time start_time = time.time() for _ in range(1000): utc_to_cest(datetime.now()) end_time = time.time() # Should complete in reasonable time (less than 1 second) assert end_time - start_time < 1.0