"""Tests for the Sentry Scrubber.""" import json from unittest.mock import Mock from sentry_scrubber import SentryScrubber def test_sentry_scrubber_init_default_parameters(): """Test SentryScrubber initialization with default parameters.""" scrubber = SentryScrubber(sensitive_fields=["password", "token"]) assert scrubber.sensitive_fields == {"password", "token"} assert scrubber.scrub_marker == "[Scrubbed]" assert scrubber.max_depth == 10 def test_sentry_scrubber_init_custom_parameters(): """Test SentryScrubber initialization with custom parameters.""" scrubber = SentryScrubber( sensitive_fields=["API_KEY", "Secret"], scrub_marker="***REDACTED***", max_depth=5, ) assert scrubber.sensitive_fields == {"api_key", "secret"} assert scrubber.scrub_marker == "***REDACTED***" assert scrubber.max_depth == 5 def test_sensitive_fields_case_insensitive(): """Test that sensitive fields are stored in lowercase for case-insensitive matching.""" scrubber = SentryScrubber(sensitive_fields=["PASSWORD", "Token", "api_key"]) assert scrubber.sensitive_fields == {"password", "token", "api_key"} def test_scrub_simple_dict(): """Test scrubbing sensitive data from a simple dictionary.""" scrubber = SentryScrubber(sensitive_fields=["password", "token"]) data = { "username": "john_doe", "password": "secret123", "token": "abc123def456", "email": "john@example.com", } result = scrubber.scrub_data(data) expected = { "username": "john_doe", "password": "[Scrubbed]", "token": "[Scrubbed]", "email": "john@example.com", } assert result == expected def test_scrub_nested_dict(): """Test scrubbing sensitive data from nested dictionaries.""" scrubber = SentryScrubber(sensitive_fields=["password", "secret_key"]) data = { "user": { "name": "John", "password": "secret123", "preferences": {"theme": "dark", "secret_key": "hidden_value"}, }, "session": {"id": "session123", "password": "another_secret"}, } result = scrubber.scrub_data(data) expected = { "user": { "name": "John", "password": "[Scrubbed]", "preferences": {"theme": "dark", "secret_key": "[Scrubbed]"}, }, "session": {"id": "session123", "password": "[Scrubbed]"}, } assert result == expected def test_scrub_list_with_dicts(): """Test scrubbing sensitive data from lists containing dictionaries.""" scrubber = SentryScrubber(sensitive_fields=["api_key", "password"]) data = [ {"name": "service1", "api_key": "key123"}, {"name": "service2", "password": "pass456"}, {"name": "service3", "config": {"api_key": "nested_key"}}, ] result = scrubber.scrub_data(data) expected = [ {"name": "service1", "api_key": "[Scrubbed]"}, {"name": "service2", "password": "[Scrubbed]"}, {"name": "service3", "config": {"api_key": "[Scrubbed]"}}, ] assert result == expected def test_scrub_mixed_data_types(): """Test scrubbing with mixed data types (strings, numbers, booleans).""" scrubber = SentryScrubber(sensitive_fields=["token"]) data = { "count": 42, "enabled": True, "token": "secret_token", "rate": 3.14, "items": [1, 2, 3], "metadata": None, } result = scrubber.scrub_data(data) expected = { "count": 42, "enabled": True, "token": "[Scrubbed]", "rate": 3.14, "items": [1, 2, 3], "metadata": None, } assert result == expected def test_scrub_json_in_string(): """Test scrubbing JSON content within strings.""" scrubber = SentryScrubber(sensitive_fields=["password", "api_key"]) json_string = '{"username": "john", "password": "secret123", "api_key": "key456"}' result = scrubber._scrub_sensitive_data_from_text(json_string) parsed_result = json.loads(result) expected = { "username": "john", "password": "[Scrubbed]", "api_key": "[Scrubbed]", } assert parsed_result == expected def test_scrub_json_in_error_message(): """Test scrubbing JSON content within error messages.""" scrubber = SentryScrubber(sensitive_fields=["token"]) error_message = ( "Authentication failed with data: {'user': 'john', 'token': 'abc123'}" ) result = scrubber._scrub_sensitive_data_from_text(error_message) assert "Authentication failed with data: " in result assert "'token': '[Scrubbed]'" in result assert "'user': 'john'" in result def test_scrub_invalid_json_in_string(): """Test that invalid JSON in strings is left unchanged.""" scrubber = SentryScrubber(sensitive_fields=["password"]) invalid_json = "{'username': 'john', 'password':}" result = scrubber._scrub_sensitive_data_from_text(invalid_json) assert result == invalid_json def test_max_depth_limiting(): """Test that max_depth parameter limits recursion depth.""" scrubber = SentryScrubber(sensitive_fields=["secret"], max_depth=2) data = { "level1": { "level2": {"level3": {"secret": "should_not_be_scrubbed_due_to_depth"}} } } result = scrubber.scrub_data(data) assert ( result["level1"]["level2"]["level3"]["secret"] == "should_not_be_scrubbed_due_to_depth" ) def test_max_depth_within_limit(): """Test that sensitive data is scrubbed within the max_depth limit.""" scrubber = SentryScrubber(sensitive_fields=["secret"], max_depth=3) data = {"level1": {"level2": {"secret": "should_be_scrubbed"}}} result = scrubber.scrub_data(data) assert result["level1"]["level2"]["secret"] == "[Scrubbed]" def test_before_send_handler_with_valid_event(): """Test the before_send_handler with a valid Sentry event.""" scrubber = SentryScrubber(sensitive_fields=["password", "api_key"]) event = { "message": "User login failed", "extra": { "user_data": {"username": "john", "password": "secret123"}, "api_key": "key456", }, "level": "error", } hint = Mock() result = scrubber.before_send_handler(event, hint) expected = { "message": "User login failed", "extra": { "user_data": {"username": "john", "password": "[Scrubbed]"}, "api_key": "[Scrubbed]", }, "level": "error", } assert result == expected def test_before_send_handler_with_none_event(): """Test the before_send_handler with None event.""" scrubber = SentryScrubber(sensitive_fields=["password"]) hint = Mock() result = scrubber.before_send_handler(None, hint) assert result is None def test_before_send_handler_with_empty_event(): """Test the before_send_handler with empty event.""" scrubber = SentryScrubber(sensitive_fields=["password"]) hint = Mock() result = scrubber.before_send_handler({}, hint) assert result == {} def test_custom_scrub_marker(): """Test using a custom scrub marker.""" scrubber = SentryScrubber(sensitive_fields=["secret"], scrub_marker="***HIDDEN***") data = {"public": "visible", "secret": "hidden_value"} result = scrubber.scrub_data(data) expected = {"public": "visible", "secret": "***HIDDEN***"} assert result == expected def test_case_insensitive_field_matching(): """Test that field matching is case-insensitive.""" scrubber = SentryScrubber(sensitive_fields=["PASSWORD", "api_key"]) data = { "password": "secret1", "PASSWORD": "secret2", "Password": "secret3", "API_KEY": "secret4", "api_key": "secret5", } result = scrubber.scrub_data(data) assert result["password"] == "[Scrubbed]" assert result["PASSWORD"] == "[Scrubbed]" assert result["Password"] == "[Scrubbed]" assert result["API_KEY"] == "[Scrubbed]" assert result["api_key"] == "[Scrubbed]" def test_empty_sensitive_fields(): """Test scrubber with empty sensitive fields list.""" scrubber = SentryScrubber(sensitive_fields=[]) data = {"password": "secret123", "token": "abc456", "username": "john"} result = scrubber.scrub_data(data) assert result == data def test_complex_nested_structure(): """Test scrubbing a complex nested structure with mixed types.""" scrubber = SentryScrubber(sensitive_fields=["password", "token", "secret"]) data = { "users": [ { "id": 1, "name": "John", "credentials": {"password": "secret123", "token": "abc456"}, }, { "id": 2, "name": "Jane", "credentials": {"password": "secret789", "token": "def789"}, }, ], "config": { "database": {"host": "localhost", "password": "db_secret"}, "api": {"endpoint": "https://api.example.com", "secret": "api_secret"}, }, "metadata": {"version": "1.0.0", "debug": True}, } result = scrubber.scrub_data(data) assert len(result["users"]) == 2 assert result["users"][0]["name"] == "John" assert result["users"][0]["credentials"]["password"] == "[Scrubbed]" assert result["users"][0]["credentials"]["token"] == "[Scrubbed]" assert result["users"][1]["credentials"]["password"] == "[Scrubbed]" assert result["config"]["database"]["host"] == "localhost" assert result["config"]["database"]["password"] == "[Scrubbed]" assert result["config"]["api"]["secret"] == "[Scrubbed]" assert result["metadata"]["version"] == "1.0.0" assert result["metadata"]["debug"] is True def test_json_pattern_compilation(): """Test that the JSON regex pattern is properly compiled.""" scrubber = SentryScrubber(sensitive_fields=["test"]) assert scrubber.json_pattern is not None assert hasattr(scrubber.json_pattern, "sub") assert hasattr(scrubber.json_pattern, "match") def test_scrub_json_match_method(): """Test the _scrub_json_match method directly.""" scrubber = SentryScrubber(sensitive_fields=["password"]) class MockMatch: def group(self, n): return '{"username": "john", "password": "secret"}' mock_match = MockMatch() result = scrubber._scrub_json_match(mock_match) parsed_result = json.loads(result) assert parsed_result["username"] == "john" assert parsed_result["password"] == "[Scrubbed]" def test_scrub_json_match_with_invalid_json(): """Test _scrub_json_match with invalid JSON.""" scrubber = SentryScrubber(sensitive_fields=["password"]) class MockMatch: def group(self, n): return "{'invalid': json}" mock_match = MockMatch() result = scrubber._scrub_json_match(mock_match) assert result == "{'invalid': json}" def test_empty_data_structures(): """Test scrubbing empty data structures.""" scrubber = SentryScrubber(sensitive_fields=["password"]) assert scrubber.scrub_data({}) == {} assert scrubber.scrub_data([]) == [] assert scrubber.scrub_data("") == "" def test_circular_reference_protection(): """Test that max_depth protects against potential circular references.""" scrubber = SentryScrubber(sensitive_fields=["secret"], max_depth=3) data = {"level1": {"level2": {"level3": {"level4": {"secret": "deep_secret"}}}}} result = scrubber.scrub_data(data) assert result["level1"]["level2"]["level3"]["level4"]["secret"] == "deep_secret" def test_unicode_and_special_characters(): """Test scrubbing data with unicode and special characters.""" scrubber = SentryScrubber(sensitive_fields=["密码", "contraseña"]) data = { "用户名": "张三", "密码": "secret123", "contraseña": "secreto456", "normal_field": "normal_value", } result = scrubber.scrub_data(data) assert result["用户名"] == "张三" assert result["密码"] == "[Scrubbed]" assert result["contraseña"] == "[Scrubbed]" assert result["normal_field"] == "normal_value" def test_numeric_and_boolean_keys(): """Test that non-string keys are handled properly.""" scrubber = SentryScrubber(sensitive_fields=["password"]) data = { "password": "secret", 123: "numeric_key_value", True: "boolean_key_value", } result = scrubber.scrub_data(data) assert result["password"] == "[Scrubbed]" assert result[123] == "numeric_key_value" assert result[True] == "boolean_key_value" def test_large_data_structure(): """Test performance with large data structures.""" scrubber = SentryScrubber(sensitive_fields=["secret"]) large_data = [{"id": i, "secret": f"secret_{i}"} for i in range(100)] result = scrubber.scrub_data(large_data) assert len(result) == 100 for i, item in enumerate(result): assert item["id"] == i assert item["secret"] == "[Scrubbed]" def test_multiple_json_objects_in_string(): """Test scrubbing multiple JSON objects within a single string.""" scrubber = SentryScrubber(sensitive_fields=["token", "password"]) text = ( "First error: {'user': 'john', 'token': 'abc123'} " "and second error: {'user': 'jane', 'password': 'secret'}" ) result = scrubber._scrub_sensitive_data_from_text(text) assert "'token': '[Scrubbed]'" in result assert "'password': '[Scrubbed]'" in result assert "'user': 'john'" in result assert "'user': 'jane'" in result def test_regex_error_handling(): """Test error handling in regex operations.""" scrubber = SentryScrubber(sensitive_fields=["password"]) problematic_text = "Some text with [brackets] and {braces} but no valid JSON" result = scrubber._scrub_sensitive_data_from_text(problematic_text) assert result == problematic_text def test_nested_json_in_strings(): """Test scrubbing nested JSON within strings.""" scrubber = SentryScrubber(sensitive_fields=["inner_secret"]) json_text = '{"outer": {"inner_secret": "hidden_value", "safe": "visible"}}' result = scrubber._scrub_sensitive_data_from_text(json_text) parsed = json.loads(result) assert parsed["outer"]["inner_secret"] == "[Scrubbed]" assert parsed["outer"]["safe"] == "visible" def test_before_send_handler_preserves_hint(): """Test that before_send_handler doesn't modify the hint parameter.""" scrubber = SentryScrubber(sensitive_fields=["password"]) event = {"message": "Error occurred", "extra": {"password": "secret"}} original_hint = {"original_exception": Exception("test")} hint_copy = original_hint.copy() result = scrubber.before_send_handler(event, original_hint) assert original_hint == hint_copy assert result["message"] == "Error occurred" assert result["extra"]["password"] == "[Scrubbed]" def test_before_send_handler_with_top_level_sensitive_keys(): """Test behavior with top-level sensitive keys in before_send_handler.""" scrubber = SentryScrubber(sensitive_fields=["password", "api_key"]) event = { "password": "top_level_secret", "api_key": "top_level_key", "extra": { "password": "nested_secret", "api_key": "nested_key", }, } result = scrubber.before_send_handler(event, {}) assert result["password"] == "top_level_secret" assert result["api_key"] == "top_level_key" assert result["extra"]["password"] == "[Scrubbed]" assert result["extra"]["api_key"] == "[Scrubbed]" def test_depth_boundary_at_zero(): """Test that scrubbing occurs when current_depth is exactly 0.""" scrubber = SentryScrubber(sensitive_fields=["secret"], max_depth=3) data = {"secret": "should_be_scrubbed_at_depth_0"} result = scrubber._scrub_sensitive_data(data, current_depth=0) assert result["secret"] == "[Scrubbed]" def test_depth_boundary_at_negative_values(): """Test that scrubbing is skipped for negative depth values.""" scrubber = SentryScrubber(sensitive_fields=["secret"], max_depth=3) data = {"secret": "should_not_be_scrubbed_negative_depth"} result = scrubber._scrub_sensitive_data(data, current_depth=-1) assert result["secret"] == "should_not_be_scrubbed_negative_depth" def test_text_with_no_json(): """Test that regular text without JSON is left unchanged.""" scrubber = SentryScrubber(sensitive_fields=["password"]) text = "This is just regular text without any JSON structures" result = scrubber._scrub_sensitive_data_from_text(text) assert result == text def test_malformed_json_like_strings(): """Test handling of malformed JSON-like strings.""" scrubber = SentryScrubber(sensitive_fields=["password"]) malformed_strings = [ "{'missing_closing': 'brace'", '{"missing_closing": "quote}', '{"trailing": "comma",}', ] for malformed in malformed_strings: result = scrubber._scrub_sensitive_data_from_text(malformed) # Should return original string if it can't be parsed assert result == malformed # Test that valid JSON with duplicate keys is handled (Python's json module keeps the last value) duplicate_key_json = '{"duplicate": "key", "duplicate": "value"}' result = scrubber._scrub_sensitive_data_from_text(duplicate_key_json) # JSON parsing will keep the last duplicate key assert result == '{"duplicate": "value"}'