"""High-value tests for security/data_sanitizer.py - Data Sanitization.""" import json from local_deep_research.security.data_sanitizer import ( DataSanitizer, sanitize_data, redact_data, filter_research_metadata, strip_settings_snapshot, ) # --------------------------------------------------------------------------- # DataSanitizer.sanitize() # --------------------------------------------------------------------------- class TestSanitizeBasic: """Basic sanitize behavior.""" def test_removes_api_key(self): result = DataSanitizer.sanitize( {"username": "bob", "api_key": "secret"} ) assert result == {"username": "bob"} def test_removes_password(self): result = DataSanitizer.sanitize({"password": "pass123", "name": "x"}) assert result == {"name": "x"} def test_removes_multiple_sensitive_keys(self): data = {"api_key": "k", "secret": "s", "safe": "v"} result = DataSanitizer.sanitize(data) assert result == {"safe": "v"} def test_case_insensitive_removal(self): data = {"API_KEY": "k", "Password": "p", "name": "n"} result = DataSanitizer.sanitize(data) assert result == {"name": "n"} def test_primitive_passthrough_string(self): assert DataSanitizer.sanitize("hello") == "hello" def test_primitive_passthrough_int(self): assert DataSanitizer.sanitize(42) == 42 def test_primitive_passthrough_none(self): assert DataSanitizer.sanitize(None) is None def test_empty_dict(self): assert DataSanitizer.sanitize({}) == {} def test_empty_list(self): assert DataSanitizer.sanitize([]) == [] class TestSanitizeRecursive: """Recursive sanitization through nested structures.""" def test_nested_dict(self): data = {"outer": {"api_key": "secret", "value": 1}} result = DataSanitizer.sanitize(data) assert result == {"outer": {"value": 1}} def test_deeply_nested(self): data = {"a": {"b": {"c": {"password": "p", "data": "d"}}}} result = DataSanitizer.sanitize(data) assert result == {"a": {"b": {"c": {"data": "d"}}}} def test_list_of_dicts(self): data = [{"api_key": "k1", "name": "a"}, {"api_key": "k2", "name": "b"}] result = DataSanitizer.sanitize(data) assert result == [{"name": "a"}, {"name": "b"}] def test_dict_with_list_of_dicts(self): data = {"items": [{"secret": "s", "id": 1}]} result = DataSanitizer.sanitize(data) assert result == {"items": [{"id": 1}]} def test_list_of_primitives_unchanged(self): assert DataSanitizer.sanitize([1, 2, 3]) == [1, 2, 3] class TestSanitizeCustomKeys: """Custom sensitive_keys parameter.""" def test_custom_keys_only(self): data = {"api_key": "keep", "custom_secret": "remove", "name": "n"} result = DataSanitizer.sanitize(data, sensitive_keys={"custom_secret"}) assert result == {"api_key": "keep", "name": "n"} def test_custom_keys_case_insensitive(self): data = {"MY_TOKEN": "t", "name": "n"} result = DataSanitizer.sanitize(data, sensitive_keys={"my_token"}) assert result == {"name": "n"} # --------------------------------------------------------------------------- # DataSanitizer.redact() # --------------------------------------------------------------------------- class TestRedactBasic: """Basic redact behavior.""" def test_replaces_with_redacted(self): data = {"api_key": "secret", "name": "bob"} result = DataSanitizer.redact(data) assert result == {"api_key": "[REDACTED]", "name": "bob"} def test_case_insensitive(self): result = DataSanitizer.redact({"PASSWORD": "p", "x": 1}) assert result == {"PASSWORD": "[REDACTED]", "x": 1} def test_custom_redaction_text(self): result = DataSanitizer.redact({"api_key": "k"}, redaction_text="***") assert result == {"api_key": "***"} def test_primitive_passthrough(self): assert DataSanitizer.redact("hello") == "hello" assert DataSanitizer.redact(42) == 42 assert DataSanitizer.redact(None) is None class TestRedactRecursive: """Recursive redaction.""" def test_nested_dict_redaction(self): data = {"outer": {"password": "secret", "value": 1}} result = DataSanitizer.redact(data) assert result == {"outer": {"password": "[REDACTED]", "value": 1}} def test_list_of_dicts_redaction(self): data = [{"secret": "s1", "id": 1}, {"secret": "s2", "id": 2}] result = DataSanitizer.redact(data) assert result == [ {"secret": "[REDACTED]", "id": 1}, {"secret": "[REDACTED]", "id": 2}, ] def test_list_passthrough(self): assert DataSanitizer.redact([1, "two", 3]) == [1, "two", 3] class TestRedactNonPrimitiveValue: """Redact replaces the value wholesale, even if it's a nested structure.""" def test_nested_dict_value_replaced_not_recursed(self): data = {"api_key": {"nested": "data"}} result = DataSanitizer.redact(data) assert result == {"api_key": "[REDACTED]"} class TestRedactCustomKeys: """Custom sensitive_keys for redact.""" def test_custom_keys(self): data = {"api_key": "keep", "my_field": "redact"} result = DataSanitizer.redact(data, sensitive_keys={"my_field"}) assert result == {"api_key": "keep", "my_field": "[REDACTED]"} # --------------------------------------------------------------------------- # Convenience functions # --------------------------------------------------------------------------- class TestConvenienceFunctions: def test_sanitize_data_delegates(self): data = {"api_key": "k", "name": "n"} assert sanitize_data(data) == {"name": "n"} def test_redact_data_delegates(self): data = {"password": "p", "name": "n"} result = redact_data(data) assert result == {"password": "[REDACTED]", "name": "n"} def test_redact_data_custom_text(self): result = redact_data({"api_key": "k"}, redaction_text="HIDDEN") assert result == {"api_key": "HIDDEN"} # --------------------------------------------------------------------------- # filter_research_metadata() # --------------------------------------------------------------------------- class TestFilterResearchMetadata: """filter_research_metadata extracts only safe fields.""" def test_dict_with_is_news_search_true(self): result = filter_research_metadata( {"is_news_search": True, "settings_snapshot": {"api_key": "x"}} ) assert result == {"is_news_search": True} def test_dict_with_is_news_search_false(self): result = filter_research_metadata({"is_news_search": False}) assert result == {"is_news_search": False} def test_dict_missing_is_news_search(self): result = filter_research_metadata({"other": "stuff"}) assert result == {"is_news_search": False} def test_json_string_input(self): result = filter_research_metadata('{"is_news_search": true}') assert result == {"is_news_search": True} def test_none_input(self): result = filter_research_metadata(None) assert result == {"is_news_search": False} def test_non_dict_input(self): result = filter_research_metadata([1, 2, 3]) assert result == {"is_news_search": False} def test_invalid_json_string(self): result = filter_research_metadata("{bad json") assert result == {"is_news_search": False} def test_empty_dict(self): result = filter_research_metadata({}) assert result == {"is_news_search": False} def test_int_truthy_coerced_to_bool(self): result = filter_research_metadata({"is_news_search": 1}) assert result == {"is_news_search": True} assert type(result["is_news_search"]) is bool def test_string_truthy_coerced_to_bool(self): result = filter_research_metadata({"is_news_search": "yes"}) assert result == {"is_news_search": True} def test_json_string_non_dict_returns_default(self): """JSON string that parses to a list exercises the not-isinstance(meta, dict) branch.""" result = filter_research_metadata("[1, 2]") assert result == {"is_news_search": False} # --------------------------------------------------------------------------- # strip_settings_snapshot() # --------------------------------------------------------------------------- class TestStripSettingsSnapshot: """strip_settings_snapshot removes settings_snapshot key.""" def test_removes_settings_snapshot(self): data = { "phase": "done", "settings_snapshot": {"api_key": "x"}, "duration": 5, } result = strip_settings_snapshot(data) assert result == {"phase": "done", "duration": 5} def test_preserves_other_keys(self): data = {"phase": "done", "error_type": None, "mode": "quick"} result = strip_settings_snapshot(data) assert result == data def test_json_string_input(self): data = json.dumps({"settings_snapshot": {}, "phase": "done"}) result = strip_settings_snapshot(data) assert result == {"phase": "done"} def test_none_input(self): assert strip_settings_snapshot(None) == {} def test_non_dict_input(self): assert strip_settings_snapshot(42) == {} def test_invalid_json(self): assert strip_settings_snapshot("{invalid") == {} def test_nested_settings_snapshot_survives(self): data = { "results": {"settings_snapshot": {"key": "val"}}, "phase": "done", } result = strip_settings_snapshot(data) assert result == { "results": {"settings_snapshot": {"key": "val"}}, "phase": "done", } def test_json_string_non_dict_returns_empty(self): """JSON string that parses to a list exercises the not-isinstance branch.""" assert strip_settings_snapshot("[1, 2]") == {} def test_empty_dict(self): assert strip_settings_snapshot({}) == {}