File size: 12,880 Bytes
59bd45e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
"""Tests for logging configuration module.

Requirements: 10.5, 9.5
"""

import logging
import pytest
from pathlib import Path

from app.logging_config import (
    SensitiveDataFilter,
    RequestIdFilter,
    setup_logging,
    get_logger,
    set_request_id,
    clear_request_id
)


class TestRequestIdFilter:
    """Test request_id filtering in logs.
    
    Requirement 9.5: Logs should include request_id for tracing.
    """
    
    def test_filter_adds_request_id(self):
        """Test that request_id is added to log records."""
        filter_obj = RequestIdFilter()
        
        # Set request_id in context
        set_request_id("test-request-123")
        
        # Create a log record
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="Test message",
            args=(),
            exc_info=None
        )
        
        filter_obj.filter(record)
        
        assert hasattr(record, 'request_id')
        assert record.request_id == "test-request-123"
        
        # Clean up
        clear_request_id()
    
    def test_filter_uses_default_when_no_request_id(self):
        """Test that filter uses '-' when no request_id is set."""
        filter_obj = RequestIdFilter()
        
        # Ensure no request_id is set
        clear_request_id()
        
        # Create a log record
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="Test message",
            args=(),
            exc_info=None
        )
        
        filter_obj.filter(record)
        
        assert hasattr(record, 'request_id')
        assert record.request_id == "-"


class TestSensitiveDataFilter:
    """Test sensitive data filtering in logs.
    
    Requirement 10.5: System should not output sensitive information in logs.
    """
    
    def test_filter_api_key(self):
        """Test that API keys are masked in log messages."""
        filter_obj = SensitiveDataFilter()
        
        # Create a log record with API key
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="Using api_key=sk_1234567890abcdef for request",
            args=(),
            exc_info=None
        )
        
        filter_obj.filter(record)
        
        assert "sk_1234567890abcdef" not in record.msg
        assert "***REDACTED***" in record.msg
    
    def test_filter_zhipu_api_key(self):
        """Test that Zhipu API keys are masked."""
        filter_obj = SensitiveDataFilter()
        
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="zhipu_api_key: abc123def456ghi789",
            args=(),
            exc_info=None
        )
        
        filter_obj.filter(record)
        
        assert "abc123def456ghi789" not in record.msg
        assert "***REDACTED***" in record.msg
    
    def test_filter_bearer_token(self):
        """Test that bearer tokens are masked."""
        filter_obj = SensitiveDataFilter()
        
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
            args=(),
            exc_info=None
        )
        
        filter_obj.filter(record)
        
        assert "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" not in record.msg
        assert "***REDACTED***" in record.msg
    
    def test_filter_password(self):
        """Test that passwords are masked."""
        filter_obj = SensitiveDataFilter()
        
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg='Login attempt with password="secret123"',
            args=(),
            exc_info=None
        )
        
        filter_obj.filter(record)
        
        assert "secret123" not in record.msg
        assert "***REDACTED***" in record.msg
    
    def test_filter_authorization_header(self):
        """Test that authorization headers are masked."""
        filter_obj = SensitiveDataFilter()
        
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="Headers: authorization: Basic_dXNlcjpwYXNz",
            args=(),
            exc_info=None
        )
        
        filter_obj.filter(record)
        
        assert "Basic_dXNlcjpwYXNz" not in record.msg
        assert "***REDACTED***" in record.msg
    
    def test_filter_preserves_normal_text(self):
        """Test that normal text is not affected by filtering."""
        filter_obj = SensitiveDataFilter()
        
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="Processing request for user authentication",
            args=(),
            exc_info=None
        )
        
        original_msg = record.msg
        filter_obj.filter(record)
        
        assert record.msg == original_msg
    
    def test_filter_with_args_dict(self):
        """Test filtering with dictionary arguments."""
        filter_obj = SensitiveDataFilter()
        
        # Create record with dict args
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="Request data",
            args=(),
            exc_info=None
        )
        # Manually set args after creation
        record.args = {"data": "api_key=secret123456"}
        
        filter_obj.filter(record)
        
        assert "secret123456" not in record.args["data"]
        assert "***REDACTED***" in record.args["data"]
    
    def test_filter_with_args_tuple(self):
        """Test filtering with tuple arguments."""
        filter_obj = SensitiveDataFilter()
        
        record = logging.LogRecord(
            name="test",
            level=logging.INFO,
            pathname="",
            lineno=0,
            msg="Config: %s",
            args=("api_key=secret123456",),
            exc_info=None
        )
        
        filter_obj.filter(record)
        
        assert "secret123456" not in record.args[0]
        assert "***REDACTED***" in record.args[0]


class TestSetupLogging:
    """Test logging setup and configuration."""
    
    def test_setup_logging_console_only(self):
        """Test setting up logging with console output only."""
        setup_logging(log_level="INFO", log_file=None)
        
        root_logger = logging.getLogger()
        
        # Should have at least one handler (console)
        assert len(root_logger.handlers) >= 1
        
        # Check log level
        assert root_logger.level == logging.INFO
    
    def test_setup_logging_with_file(self, tmp_path):
        """Test setting up logging with file output."""
        log_file = tmp_path / "test.log"
        
        setup_logging(log_level="DEBUG", log_file=log_file)
        
        root_logger = logging.getLogger()
        
        # Should have at least two handlers (console + file)
        assert len(root_logger.handlers) >= 2
        
        # Check log level
        assert root_logger.level == logging.DEBUG
        
        # Test logging to file
        test_logger = logging.getLogger("test")
        test_logger.info("Test message")
        
        # File should exist and contain the message
        assert log_file.exists()
        content = log_file.read_text()
        assert "Test message" in content
    
    def test_setup_logging_custom_format(self, tmp_path):
        """Test setting up logging with custom format."""
        log_file = tmp_path / "test.log"
        custom_format = "%(levelname)s - %(message)s"
        
        setup_logging(
            log_level="INFO",
            log_file=log_file,
            log_format=custom_format
        )
        
        test_logger = logging.getLogger("test_custom")
        test_logger.info("Custom format test")
        
        content = log_file.read_text()
        assert "INFO - Custom format test" in content
    
    def test_setup_logging_applies_sensitive_filter(self, tmp_path):
        """Test that sensitive data filter is applied to all handlers.
        
        Requirement 10.5: Sensitive information should be filtered from logs.
        """
        log_file = tmp_path / "test.log"
        
        setup_logging(log_level="INFO", log_file=log_file)
        
        test_logger = logging.getLogger("test_sensitive")
        test_logger.info("API request with api_key=secret123456789")
        
        # Read log file
        content = log_file.read_text()
        
        # API key should be redacted
        assert "secret123456789" not in content
        assert "***REDACTED***" in content
    
    def test_get_logger(self):
        """Test getting a logger instance."""
        logger = get_logger("test_module")
        
        assert logger is not None
        assert logger.name == "test_module"
        assert isinstance(logger, logging.Logger)


class TestLoggingIntegration:
    """Integration tests for logging system.
    
    Requirement 9.5: System should log all errors with timestamp and stack trace.
    """
    
    def test_error_logging_with_traceback(self, tmp_path):
        """Test that errors are logged with full traceback."""
        log_file = tmp_path / "error.log"
        
        setup_logging(log_level="ERROR", log_file=log_file)
        
        logger = get_logger("test_error")
        
        try:
            # Cause an error
            raise ValueError("Test error for logging")
        except ValueError:
            logger.error("An error occurred", exc_info=True)
        
        # Read log file
        content = log_file.read_text()
        
        # Should contain error message and traceback
        assert "An error occurred" in content
        assert "ValueError: Test error for logging" in content
        assert "Traceback" in content
    
    def test_logging_includes_timestamp(self, tmp_path):
        """Test that log entries include timestamps."""
        log_file = tmp_path / "timestamp.log"
        
        setup_logging(log_level="INFO", log_file=log_file)
        
        logger = get_logger("test_timestamp")
        logger.info("Timestamp test message")
        
        content = log_file.read_text()
        
        # Should contain timestamp in format [YYYY-MM-DD HH:MM:SS]
        import re
        timestamp_pattern = r'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]'
        assert re.search(timestamp_pattern, content)
    
    def test_logging_includes_level_and_module(self, tmp_path):
        """Test that log entries include level and module name."""
        log_file = tmp_path / "format.log"
        
        setup_logging(log_level="INFO", log_file=log_file)
        
        logger = get_logger("test_module")
        logger.warning("Warning message")
        
        content = log_file.read_text()
        
        # Should contain level and module name
        assert "[WARNING]" in content
        assert "[test_module]" in content
        assert "Warning message" in content
    
    def test_logging_includes_request_id(self, tmp_path):
        """Test that log entries include request_id when set.
        
        Requirement 9.5: Logs should include request_id for request tracing.
        """
        log_file = tmp_path / "request_id.log"
        
        setup_logging(log_level="INFO", log_file=log_file)
        
        # Set request_id
        set_request_id("req-12345")
        
        logger = get_logger("test_request")
        logger.info("Request message")
        
        content = log_file.read_text()
        
        # Should contain request_id
        assert "[req-12345]" in content
        assert "Request message" in content
        
        # Clean up
        clear_request_id()
    
    def test_logging_without_request_id(self, tmp_path):
        """Test that log entries use '-' when no request_id is set."""
        log_file = tmp_path / "no_request_id.log"
        
        setup_logging(log_level="INFO", log_file=log_file)
        
        # Ensure no request_id is set
        clear_request_id()
        
        logger = get_logger("test_no_request")
        logger.info("Message without request_id")
        
        content = log_file.read_text()
        
        # Should contain '-' for request_id
        assert "[-]" in content
        assert "Message without request_id" in content