"""Tests for the diagnose-gated ``SecureLogger`` wrapper. ``security.secure_logging.logger.exception()`` must log at ERROR level while attaching the active exception to the record ONLY when both ``LDR_APP_DEBUG`` and ``LDR_LOGURU_DIAGNOSE`` are truthy. With the exception stripped, no sink can render the traceback or the ``__cause__`` chain — the #4183 leak class (headers/URLs embedded in wrapped exceptions) cannot reach any log output. """ import pytest from local_deep_research.security.secure_logging import ( SecureLogger, env_truthy, is_diagnose_mode, logger, ) _DEBUG_VAR = "LDR_APP_DEBUG" _DIAGNOSE_VAR = "LDR_LOGURU_DIAGNOSE" def _set_env(monkeypatch, debug, diagnose): """Set/unset both gating vars. ``None`` means unset.""" for var, value in ((_DEBUG_VAR, debug), (_DIAGNOSE_VAR, diagnose)): if value is None: monkeypatch.delenv(var, raising=False) else: monkeypatch.setenv(var, value) def _log_wrapped_exception(message="request failed: scrubbed"): """Log *message* from inside a handler for a cause-chained exception. The inner ``ValueError`` carries a fake credential — the same shape as the #4183 leak, where the secret lived only in the ``__cause__`` chain, not in the logged message. """ try: try: raise ValueError("api_key=sk-LEAK-0123456789abcdef") except ValueError as inner: raise RuntimeError("wrapped") from inner except RuntimeError: logger.exception(message) class TestEnvTruthy: @pytest.mark.parametrize("value", ["1", "true", "TRUE", "yes", " YES "]) def test_truthy_values(self, monkeypatch, value): monkeypatch.setenv(_DEBUG_VAR, value) assert env_truthy(_DEBUG_VAR) is True @pytest.mark.parametrize("value", ["", "0", "false", "no", "on", "2"]) def test_falsy_values(self, monkeypatch, value): monkeypatch.setenv(_DEBUG_VAR, value) assert env_truthy(_DEBUG_VAR) is False def test_unset_is_falsy(self, monkeypatch): monkeypatch.delenv(_DEBUG_VAR, raising=False) assert env_truthy(_DEBUG_VAR) is False class TestIsDiagnoseMode: @pytest.mark.parametrize( ("debug", "diagnose", "expected"), [ (None, None, False), ("1", None, False), (None, "1", False), ("0", "1", False), ("1", "0", False), ("1", "1", True), ("true", "yes", True), ("YES ", "TRUE", True), ], ) def test_truth_table(self, monkeypatch, debug, diagnose, expected): _set_env(monkeypatch, debug, diagnose) assert is_diagnose_mode() is expected class TestExceptionDefaultMode: """Both flags unset — production behaviour.""" def test_logs_at_error_level_without_traceback( self, monkeypatch, loguru_caplog_full ): _set_env(monkeypatch, None, None) with loguru_caplog_full.at_level("ERROR"): _log_wrapped_exception() records = [ r for r in loguru_caplog_full.records if "request failed: scrubbed" in r.getMessage() ] assert records, "the event must still be logged" assert all(r.levelname == "ERROR" for r in records) assert all(r.exc_info is None for r in records) assert "Traceback" not in loguru_caplog_full.text def test_cause_chain_secret_never_reaches_output( self, monkeypatch, loguru_caplog_full ): """#4183 regression: the fixture renders ``{exception}`` with ``backtrace=True``, so if the exception were attached the ``__cause__`` chain (holding the credential) WOULD appear here.""" _set_env(monkeypatch, None, None) with loguru_caplog_full.at_level("ERROR"): _log_wrapped_exception() assert "request failed: scrubbed" in loguru_caplog_full.text assert "sk-LEAK" not in loguru_caplog_full.text assert "api_key" not in loguru_caplog_full.text @pytest.mark.parametrize( ("debug", "diagnose"), [("1", None), (None, "1"), ("1", "0"), ("0", "1")], ) def test_single_flag_does_not_enable_traceback( self, monkeypatch, loguru_caplog_full, debug, diagnose ): _set_env(monkeypatch, debug, diagnose) with loguru_caplog_full.at_level("ERROR"): _log_wrapped_exception() assert "request failed: scrubbed" in loguru_caplog_full.text assert "Traceback" not in loguru_caplog_full.text assert "sk-LEAK" not in loguru_caplog_full.text class TestExceptionDiagnoseMode: def test_both_flags_render_traceback(self, monkeypatch, loguru_caplog_full): _set_env(monkeypatch, "1", "1") with loguru_caplog_full.at_level("ERROR"): _log_wrapped_exception() records = [ r for r in loguru_caplog_full.records if "request failed: scrubbed" in r.getMessage() ] assert records assert all(r.levelname == "ERROR" for r in records) assert "Traceback" in loguru_caplog_full.text assert "RuntimeError" in loguru_caplog_full.text class TestRecordAttribution: def test_record_points_at_caller_not_wrapper(self, monkeypatch): """``depth=1`` must attribute the record to the calling module so log locations stay useful and ``logger.disable()`` namespacing keys off the caller, not ``secure_logging``.""" _set_env(monkeypatch, None, None) captured = [] handler_id = logger.add( lambda message: captured.append(message.record), format="{message}", level="ERROR", ) try: try: raise RuntimeError("boom") except RuntimeError: logger.exception("attribution check") finally: logger.remove(handler_id) records = [r for r in captured if r["message"] == "attribution check"] assert records record = records[-1] assert record["module"] == "test_secure_logging" assert record["function"] == "test_record_points_at_caller_not_wrapper" assert record["exception"] is None class TestWrapperChaining: def test_bind_keeps_gated_exception(self, monkeypatch, loguru_caplog_full): _set_env(monkeypatch, None, None) bound = logger.bind(component="test") assert isinstance(bound, SecureLogger) with loguru_caplog_full.at_level("ERROR"): try: try: raise ValueError("api_key=sk-LEAK-0123456789abcdef") except ValueError as inner: raise RuntimeError("wrapped") from inner except RuntimeError: bound.exception("bound scrubbed message") assert "bound scrubbed message" in loguru_caplog_full.text assert "Traceback" not in loguru_caplog_full.text assert "sk-LEAK" not in loguru_caplog_full.text def test_patch_returns_wrapper(self): patched = logger.patch(lambda record: None) assert isinstance(patched, SecureLogger) def test_no_active_exception_does_not_raise( self, monkeypatch, loguru_caplog_full ): _set_env(monkeypatch, None, None) with loguru_caplog_full.at_level("ERROR"): logger.exception("called outside except block") assert "called outside except block" in loguru_caplog_full.text def test_delegates_other_methods_to_loguru( self, monkeypatch, loguru_caplog_full ): with loguru_caplog_full.at_level("INFO"): logger.info("plain info via wrapper") assert "plain info via wrapper" in loguru_caplog_full.text