Test Suite / test (push) Canceled after 0s
- test_long_version_strings: Update expectation for tuple comparison (longer > shorter) - test_path_traversal_prevention: Use tempfile.TemporaryDirectory for cross-platform compatibility - test_normal_join_succeeds: Replace endsWith with contains checks (OS-agnostic) - test_invalid_log_level_defaults_to_info: Verify LOG_LEVEL_ENV_MAP lookup - test_plugin_base_has_abstract_method: Fix ABC type check using ABCMeta instead of isinstance(ABC)
248 lines
8.7 KiB
Python
248 lines
8.7 KiB
Python
"""
|
|
Unit tests for src/shared/logging_config.py - Logging infrastructure.
|
|
|
|
Tests cover:
|
|
- Logger creation and configuration
|
|
- Log level handling based on environment
|
|
- SecureFormatter filtering of sensitive data
|
|
- Log file rotation setup
|
|
- Audit logger functionality
|
|
"""
|
|
|
|
import os
|
|
import unittest
|
|
import logging
|
|
from io import StringIO
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
# Add parent directory to path for imports
|
|
sys_path = Path(__file__).parent.parent / "src"
|
|
if str(sys_path) not in __import__('sys').path:
|
|
__import__('sys').path.insert(0, str(sys_path))
|
|
|
|
|
|
class TestLoggerCreation(unittest.TestCase):
|
|
"""Test logger creation and basic configuration."""
|
|
|
|
def test_get_logger_returns_logger_instance(self):
|
|
"""get_logger() should return a logging.Logger instance."""
|
|
from src.shared.logging_config import get_logger
|
|
|
|
logger = get_logger('test.module')
|
|
self.assertIsInstance(logger, logging.Logger)
|
|
self.assertEqual(logger.name, 'test.module')
|
|
|
|
def test_get_logger_with_class_name(self):
|
|
"""get_logger(__name__) pattern should work."""
|
|
from src.shared.logging_config import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
self.assertIn('test_logging_config', logger.name)
|
|
|
|
@patch.dict(os.environ, {'FLARES_LOG_LEVEL': 'DEBUG'})
|
|
def test_log_level_from_env_debug(self):
|
|
"""Log level should be set from FLARES_LOG_LEVEL env var."""
|
|
import importlib
|
|
import src.shared.logging_config as log_module
|
|
importlib.reload(log_module)
|
|
|
|
self.assertEqual(log_module.LOG_LEVEL, 10)
|
|
logger = log_module.setup_logging('test.env.debug.fresh')
|
|
self.assertEqual(logger.level, 10)
|
|
|
|
@patch.dict(os.environ, {'FLARES_LOG_LEVEL': 'ERROR'})
|
|
def test_log_level_from_env_error(self):
|
|
"""Log level should respect ERROR setting."""
|
|
import importlib
|
|
import src.shared.logging_config as log_module
|
|
importlib.reload(log_module)
|
|
|
|
self.assertEqual(log_module.LOG_LEVEL, 40)
|
|
logger = log_module.setup_logging('test.env.error.fresh')
|
|
self.assertEqual(logger.level, 40)
|
|
|
|
@patch.dict(os.environ, {'FLARES_LOG_LEVEL': 'INVALID'})
|
|
def test_invalid_log_level_defaults_to_info(self):
|
|
"""Invalid log level should default to INFO."""
|
|
import importlib
|
|
import src.shared.logging_config as log_module
|
|
importlib.reload(log_module)
|
|
|
|
self.assertEqual(log_module.LOG_LEVEL, 20)
|
|
# Also verify the constant directly
|
|
assert log_module.LOG_LEVEL_ENV_MAP.get('INVALID') is None
|
|
|
|
|
|
class TestSecureFormatter(unittest.TestCase):
|
|
"""Test SecureFormatter for sensitive data filtering."""
|
|
|
|
def test_filters_password_field(self):
|
|
"""SecureFormatter should replace password values."""
|
|
from src.shared.logging_config import SecureFormatter
|
|
|
|
formatter = SecureFormatter()
|
|
log_record = logging.makeLogRecord({
|
|
'msg': 'User login attempted',
|
|
'password': 'secret123',
|
|
})
|
|
|
|
# Formatter shouldn't crash on record with password
|
|
formatted = formatter.format(log_record)
|
|
self.assertIn('User login attempted', formatted)
|
|
|
|
def test_filters_token_field(self):
|
|
"""SecureFormatter should replace token values."""
|
|
from src.shared.logging_config import SecureFormatter
|
|
|
|
formatter = SecureFormatter()
|
|
log_record = logging.makeLogRecord({
|
|
'msg': 'API request',
|
|
'token': 'bearer-secret-token',
|
|
})
|
|
|
|
formatted = formatter.format(log_record)
|
|
self.assertIn('API request', formatted)
|
|
|
|
def test_filters_api_key_field(self):
|
|
"""SecureFormatter should replace api_key values."""
|
|
from src.shared.logging_config import SecureFormatter
|
|
|
|
formatter = SecureFormatter()
|
|
log_record = logging.makeLogRecord({
|
|
'msg': 'Key used',
|
|
'api_key': 'sk-12345-secret',
|
|
})
|
|
|
|
formatted = formatter.format(log_record)
|
|
self.assertIn('Key used', formatted)
|
|
|
|
def test_format_includes_timestamp(self):
|
|
"""Formatted log should include timestamp."""
|
|
from src.shared.logging_config import SecureFormatter
|
|
|
|
formatter = SecureFormatter()
|
|
log_record = logging.makeLogRecord({
|
|
'msg': 'Test message',
|
|
'created': 1234567890.0,
|
|
})
|
|
|
|
formatted = formatter.format(log_record)
|
|
# Should have some form of timestamp
|
|
self.assertGreater(len(formatted), len('Test message'))
|
|
|
|
|
|
class TestLogFilters(unittest.TestCase):
|
|
"""Test log filters for sensitive data."""
|
|
|
|
def test_secure_formatter_does_not_crash_with_extra_fields(self):
|
|
"""SecureFormatter should handle extra fields gracefully."""
|
|
from src.shared.logging_config import SecureFormatter
|
|
|
|
formatter = SecureFormatter()
|
|
|
|
# Create record with various extra fields
|
|
record = logging.makeLogRecord({
|
|
'msg': 'Test message',
|
|
'extra_field': 'value',
|
|
})
|
|
|
|
# Should not crash
|
|
result = formatter.format(record)
|
|
self.assertIsInstance(result, str)
|
|
|
|
|
|
class TestLoggerHierarchy(unittest.TestCase):
|
|
"""Test logger hierarchy and propagation."""
|
|
|
|
def test_child_loggers_inherit_config(self):
|
|
"""Child loggers should inherit parent configuration."""
|
|
from src.shared.logging_config import get_logger
|
|
|
|
parent = get_logger('flares')
|
|
child = get_logger('flares.module')
|
|
|
|
# Both should be logger instances
|
|
self.assertIsInstance(parent, logging.Logger)
|
|
self.assertIsInstance(child, logging.Logger)
|
|
|
|
|
|
class TestInitializeLogging(unittest.TestCase):
|
|
"""Test initialize_logging() function."""
|
|
|
|
@patch('src.shared.logging_config.Path.mkdir')
|
|
@patch('src.shared.logging_config.logging.getLogger')
|
|
def test_initialize_creates_log_dir(self, mock_get_logger, mock_mkdir):
|
|
"""initialize_logging() should create log directory if needed."""
|
|
from src.shared.logging_config import initialize_logging
|
|
|
|
# Should not raise exception
|
|
result = initialize_logging()
|
|
|
|
self.assertTrue(result) # Returns True on success
|
|
|
|
|
|
class TestConsoleHandler(unittest.TestCase):
|
|
"""Test console handler configuration."""
|
|
|
|
def test_console_handler_uses_secure_formatter(self):
|
|
"""Console handler should use SecureFormatter."""
|
|
from src.shared.logging_config import get_logger, SecureFormatter
|
|
|
|
# Create fresh logger (will add handlers)
|
|
logger = get_logger('test.console.handler')
|
|
|
|
# Logger should have at least one handler
|
|
self.assertGreater(len(logger.handlers), 0)
|
|
|
|
|
|
class TestIntegration(unittest.TestCase):
|
|
"""Integration tests for logging setup."""
|
|
|
|
@patch.dict(os.environ, {'FLARES_LOG_LEVEL': 'DEBUG', 'FLARES_LOG_DIR': '/tmp/test-logs'})
|
|
def test_full_logging_initialization(self):
|
|
"""Test complete logging initialization workflow."""
|
|
import importlib
|
|
import src.shared.logging_config as log_module
|
|
importlib.reload(log_module)
|
|
|
|
success = log_module.initialize_logging()
|
|
self.assertTrue(success)
|
|
|
|
logger = log_module.setup_logging('test.integration.workflow.fresh2')
|
|
self.assertIsInstance(logger, logging.Logger)
|
|
self.assertEqual(logger.level, 10)
|
|
|
|
logger.info('Test message')
|
|
logger.debug('Debug message')
|
|
logger.warning('Warning message')
|
|
|
|
|
|
class TestSecurity(unittest.TestCase):
|
|
"""Test security properties of logging module."""
|
|
|
|
def test_no_passwords_in_log_output(self):
|
|
"""Log output should not contain the word 'password' unless expected."""
|
|
from src.shared.logging_config import get_logger, SecureFormatter
|
|
|
|
string_buffer = StringIO()
|
|
|
|
logger = get_logger('test.security.password.check')
|
|
logger.handlers.clear()
|
|
handler = logging.StreamHandler(string_buffer)
|
|
handler.setFormatter(SecureFormatter())
|
|
logger.addHandler(handler)
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
# Log a message that doesn't mention password
|
|
logger.info('User authentication processed')
|
|
|
|
output = string_buffer.getvalue()
|
|
# The test name contains 'password' but the log message doesn't
|
|
# So we verify the actual message content is logged correctly
|
|
self.assertIn('authentication processed', output.lower())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|