| """ |
| Unit tests for API key redaction functionality |
| """ |
|
|
| import unittest |
| import logging |
| from unittest.mock import patch, MagicMock |
|
|
| from app.utils.helpers import redact_key_for_logging |
| from app.log.logger import AccessLogFormatter |
|
|
|
|
| class TestKeyRedaction(unittest.TestCase): |
| """Test cases for the redact_key_for_logging function""" |
|
|
| def test_valid_long_key_redaction(self): |
| """Test redaction of valid long API keys""" |
| |
| |
| gemini_key = "AIzaSyDhKGfJ8xYzQwErTyUiOpLkMnBvCxDfGhI" |
| result = redact_key_for_logging(gemini_key) |
| expected = "AIzaSy...xDfGhI" |
| self.assertEqual(result, expected) |
|
|
| |
| |
| openai_key = "sk-1234567890abcdef1234567890abcdef1234567890abcdef" |
| result = redact_key_for_logging(openai_key) |
| expected = "sk-123...abcdef" |
| self.assertEqual(result, expected) |
|
|
| def test_short_key_handling(self): |
| """Test handling of short keys""" |
| short_key = "short" |
| result = redact_key_for_logging(short_key) |
| self.assertEqual(result, "[SHORT_KEY]") |
|
|
| |
| boundary_key = "123456789012" |
| result = redact_key_for_logging(boundary_key) |
| self.assertEqual(result, "[SHORT_KEY]") |
|
|
| def test_empty_and_none_keys(self): |
| """Test handling of empty and None keys""" |
| |
| result = redact_key_for_logging("") |
| self.assertEqual(result, "[INVALID_KEY]") |
|
|
| |
| result = redact_key_for_logging(None) |
| self.assertEqual(result, "[INVALID_KEY]") |
|
|
| def test_invalid_input_types(self): |
| """Test handling of invalid input types""" |
| |
| result = redact_key_for_logging(123) |
| self.assertEqual(result, "[INVALID_KEY]") |
|
|
| |
| result = redact_key_for_logging(["key"]) |
| self.assertEqual(result, "[INVALID_KEY]") |
|
|
| |
| result = redact_key_for_logging({"key": "value"}) |
| self.assertEqual(result, "[INVALID_KEY]") |
|
|
| def test_boundary_cases(self): |
| """Test boundary cases for key length""" |
| |
| key_13 = "1234567890123" |
| result = redact_key_for_logging(key_13) |
| expected = "123456...890123" |
| self.assertEqual(result, expected) |
|
|
| |
| long_key = "a" * 100 |
| result = redact_key_for_logging(long_key) |
| expected = "aaaaaa...aaaaaa" |
| self.assertEqual(result, expected) |
|
|
|
|
| class TestAccessLogFormatter(unittest.TestCase): |
| """Test cases for the AccessLogFormatter class""" |
|
|
| def setUp(self): |
| """Set up test fixtures""" |
| self.formatter = AccessLogFormatter() |
|
|
| def test_gemini_key_redaction_in_url(self): |
| """Test redaction of Gemini API keys in URLs""" |
| log_message = ( |
| 'POST /verify-key/AIzaSyDhKGfJ8xYzQwErTyUiOpLkMnBvCxDfGhI HTTP/1.1" 200' |
| ) |
| result = self.formatter._redact_api_keys_in_message(log_message) |
| self.assertIn("AIzaSy...xDfGhI", result) |
| self.assertNotIn("AIzaSyDhKGfJ8xYzQwErTyUiOpLkMnBvCxDfGhI", result) |
|
|
| def test_openai_key_redaction_in_url(self): |
| """Test redaction of OpenAI API keys in URLs""" |
| log_message = 'GET /api/models?key=sk-1234567890abcdef1234567890abcdef1234567890abcdef HTTP/1.1" 200' |
| result = self.formatter._redact_api_keys_in_message(log_message) |
| self.assertIn("sk-123...abcdef", result) |
| self.assertNotIn("sk-1234567890abcdef1234567890abcdef1234567890abcdef", result) |
|
|
| def test_multiple_keys_in_message(self): |
| """Test redaction of multiple API keys in a single message""" |
| log_message = "Request with keys: AIzaSyDhKGfJ8xYzQwErTyUiOpLkMnBvCxDfGhI and sk-1234567890abcdef1234567890abcdef1234567890abcdef" |
| result = self.formatter._redact_api_keys_in_message(log_message) |
| self.assertIn("AIzaSy...xDfGhI", result) |
| self.assertIn("sk-123...abcdef", result) |
| self.assertNotIn("AIzaSyDhKGfJ8xYzQwErTyUiOpLkMnBvCxDfGhI", result) |
| self.assertNotIn("sk-1234567890abcdef1234567890abcdef1234567890abcdef", result) |
|
|
| def test_no_keys_in_message(self): |
| """Test that messages without API keys are unchanged""" |
| log_message = 'GET /api/health HTTP/1.1" 200' |
| result = self.formatter._redact_api_keys_in_message(log_message) |
| self.assertEqual(result, log_message) |
|
|
| def test_partial_key_patterns_not_redacted(self): |
| """Test that partial key patterns are not redacted""" |
| log_message = "Message with partial patterns: AIza sk- incomplete" |
| result = self.formatter._redact_api_keys_in_message(log_message) |
| self.assertEqual(result, log_message) |
|
|
| def test_error_handling_in_redaction(self): |
| """Test error handling in the redaction process""" |
| |
| original_patterns = self.formatter.compiled_patterns |
| |
| mock_pattern = MagicMock() |
| mock_pattern.sub.side_effect = Exception("Regex error") |
| self.formatter.compiled_patterns = [mock_pattern] |
|
|
| try: |
| log_message = ( |
| 'POST /verify-key/AIzaSyDhKGfJ8xYzQwErTyUiOpLkMnBvCxDfGhI HTTP/1.1" 200' |
| ) |
| result = self.formatter._redact_api_keys_in_message(log_message) |
| self.assertEqual(result, "[LOG_REDACTION_ERROR]") |
| finally: |
| |
| self.formatter.compiled_patterns = original_patterns |
|
|
| def test_format_method(self): |
| """Test the format method of AccessLogFormatter""" |
| |
| record = MagicMock() |
| record.getMessage.return_value = ( |
| 'POST /verify-key/AIzaSyDhKGfJ8xYzQwErTyUiOpLkMnBvCxDfGhI HTTP/1.1" 200' |
| ) |
|
|
| |
| with patch( |
| "logging.Formatter.format", |
| return_value='2025-01-01 12:00:00 | INFO | POST /verify-key/AIzaSyDhKGfJ8xYzQwErTyUiOpLkMnBvCxDfGhI HTTP/1.1" 200', |
| ): |
| result = self.formatter.format(record) |
| self.assertIn("AIzaSy...xDfGhI", result) |
| self.assertNotIn("AIzaSyDhKGfJ8xYzQwErTyUiOpLkMnBvCxDfGhI", result) |
|
|
| def test_regex_patterns_compilation(self): |
| """Test that regex patterns are properly compiled""" |
| formatter = AccessLogFormatter() |
| self.assertEqual(len(formatter.compiled_patterns), 2) |
| self.assertTrue( |
| all(hasattr(pattern, "sub") for pattern in formatter.compiled_patterns) |
| ) |
|
|
| def test_flexible_openai_pattern(self): |
| """Test the flexible OpenAI pattern matches various formats""" |
| test_cases = [ |
| "sk-1234567890abcdef1234567890abcdef1234567890abcdef", |
| "sk-proj-1234567890abcdef1234567890abcdef1234567890abcdef", |
| "sk-1234567890abcdef_1234567890abcdef-1234567890abcdef", |
| "sk-12345678901234567890", |
| ] |
|
|
| for test_key in test_cases: |
| log_message = f"Request with key: {test_key}" |
| result = self.formatter._redact_api_keys_in_message(log_message) |
| self.assertNotIn(test_key, result) |
| self.assertIn("sk-", result) |
|
|
|
|
| if __name__ == "__main__": |
| unittest.main() |
|
|