Repository: apache/airflow
File: shared/secrets_masker/tests/secrets_masker/test_secrets_masker.py (Python)

Code before:
                    f"Exception {i} should not have a context due to depth limit"
                )
                break
            current = current.__context__

    def test_redact_exception_with_circular_cause_reference(self):
        """
        Test _redact_exception_with_context_or_cause handles circular __cause__ references without infinite recursion.
        """
        masker = SecretsMasker()
        configure_secrets_masker_for_test(masker)
        masker.add_mask(PASSWORD)

        exc1 = RuntimeError(f"Error with {PASSWORD}")
        exc2 = RuntimeError(f"Another error with {PASSWORD}")
        # Create circular reference using __cause__
        exc1.__cause__ = exc2
        exc2.__cause__ = exc1

        # Should not raise RecursionError
        masker._redact_exception_with_context_or_cause(exc1)

        assert "password" not in str(exc1.args[0])
        assert "password" not in str(exc2.args[0])

    def test_redact_exception_with_max_cause_recursion_depth(self):
        """
        Test _redact_exception_with_context_or_cause respects MAX_RECURSION_DEPTH for __cause__ chains.
        Exceptions beyond the depth limit should be skipped (not redacted).
        """
        masker = SecretsMasker()
        configure_secrets_masker_for_test(masker)
        masker.add_mask(PASSWORD)

        # Create a long chain of exceptions using __cause__
        exc_chain = RuntimeError(f"Error 0 with {PASSWORD}")
        current = exc_chain
        for i in range(1, 10):
            new_exc = RuntimeError(f"Error {i} with {PASSWORD}")
            current.__cause__ = new_exc
            current = new_exc

        masker._redact_exception_with_context_or_cause(exc_chain)

        # Verify redaction happens up to MAX_RECURSION_DEPTH
        # The check is `len(visited) >= MAX_RECURSION_DEPTH` before adding current exception
        # So it processes exactly MAX_RECURSION_DEPTH exceptions (0 through MAX_RECURSION_DEPTH-1)
        current = exc_chain
        for i in range(10):
            assert current, "We should always get some exception here"
            if i < masker.MAX_RECURSION_DEPTH:

Review comment (suggestion):
Not a reviewer here, just reading through the PR. I noticed there are tests for circular `__context__` and circular `__cause__` separately, but I don't see one for the cross-circular case (e.g. `exc1.__cause__ = exc2` and `exc2.__context__ = exc1`). The `visited` set should handle it fine, but might be worth having an explicit test? Feel free to ignore if that's overkill.

Code after:
                assert "password" not in str(current.args[0]), f"Exception {i} should not be present"
                assert current.__context__ is None, (
                    f"Exception {i} should not have a context due to depth limit"
                )
                break
            current = current.__context__

    def test_redact_exception_with_circular_cause_reference(self):
        """
        Test _redact_exception_with_context_or_cause handles circular __cause__ references without infinite recursion.
        """
        masker = SecretsMasker()
        configure_secrets_masker_for_test(masker)
        masker.add_mask(PASSWORD)

        exc1 = RuntimeError(f"Error with {PASSWORD}")
        exc2 = RuntimeError(f"Another error with {PASSWORD}")
        # Create circular reference using __cause__
        exc1.__cause__ = exc2
        exc2.__cause__ = exc1

        # Should not raise RecursionError
        masker._redact_exception_with_context_or_cause(exc1)

        assert "password" not in str(exc1.args[0])
        assert "password" not in str(exc2.args[0])

    def test_redact_exception_with_max_cause_recursion_depth(self):
        """
        Test _redact_exception_with_context_or_cause respects MAX_RECURSION_DEPTH for __cause__ chains.
        Exceptions beyond the depth limit should be skipped (not redacted).
        """
        masker = SecretsMasker()
        configure_secrets_masker_for_test(masker)
        masker.add_mask(PASSWORD)

        # Create a long chain of exceptions using __cause__
        exc_chain = RuntimeError(f"Error 0 with {PASSWORD}")
        current = exc_chain
        for i in range(1, 10):
            new_exc = RuntimeError(f"Error {i} with {PASSWORD}")
            current.__cause__ = new_exc
            current = new_exc

        masker._redact_exception_with_context_or_cause(exc_chain)

        # Verify redaction happens up to MAX_RECURSION_DEPTH
        # The check is `len(visited) >= MAX_RECURSION_DEPTH` before adding current exception
        # So it processes exactly MAX_RECURSION_DEPTH exceptions (0 through MAX_RECURSION_DEPTH-1)
        current = exc_chain
        for i in range(10):