5a558eb09e
Backend Tests / discover-tests (push) Waiting to run
Backend Tests / ${{ matrix.name }} (push) Blocked by required conditions
Build and Publish SDK / build-and-publish (push) Waiting to run
Build Opik Docker Images / set-version (push) Waiting to run
Build Opik Docker Images / build-backend (push) Blocked by required conditions
Build Opik Docker Images / build-sandbox-executor-python (push) Blocked by required conditions
Build Opik Docker Images / build-python-backend (push) Blocked by required conditions
Build Opik Docker Images / build-frontend (push) Blocked by required conditions
Build Opik Docker Images / create-git-tag (push) Blocked by required conditions
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Waiting to run
Docs - Publish / run (push) Waiting to run
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Waiting to run
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Blocked by required conditions
Frontend Unit Tests / Test on Node 20 (push) Waiting to run
Guardrails E2E Tests / Select Python version matrix (push) Waiting to run
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Blocked by required conditions
Guardrails E2E Tests / 📢 Slack Notification (push) Blocked by required conditions
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Waiting to run
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Blocked by required conditions
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Waiting to run
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Waiting to run
Lint Opik Helm Chart / unittest-helm-chart (push) Waiting to run
Lint Opik Helm Chart / render-equality (push) Waiting to run
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Waiting to run
Python BE E2E Tests / Python BE E2E (push) Waiting to run
Python Backend Tests / run-python-backend-tests (push) Waiting to run
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
SDK E2E Libraries Integration Tests / Check Secrets (push) Waiting to run
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Blocked by required conditions
SDK E2E Libraries Integration Tests / build-opik (push) Blocked by required conditions
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Blocked by required conditions
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Waiting to run
TypeScript SDK Build & Publish / build-and-publish (push) Waiting to run
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Waiting to run
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
TypeScript SDK Library Integration Tests / Check Secrets (push) Has been cancelled
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Has been cancelled
SDK Library Integration Tests Runner / Check Secrets (push) Has been cancelled
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Has been cancelled
SDK Library Integration Tests Runner / Build (push) Has been cancelled
SDK Library Integration Tests Runner / openai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_tests (push) Has been cancelled
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / llama_index_tests (push) Has been cancelled
SDK Library Integration Tests Runner / anthropic_tests (push) Has been cancelled
SDK Library Integration Tests Runner / mistral_tests (push) Has been cancelled
SDK Library Integration Tests Runner / groq_tests (push) Has been cancelled
SDK Library Integration Tests Runner / aisuite_tests (push) Has been cancelled
SDK Library Integration Tests Runner / haystack_tests (push) Has been cancelled
SDK Library Integration Tests Runner / dspy_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / crewai_v1_tests (push) Has been cancelled
SDK Library Integration Tests Runner / genai_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_tests (push) Has been cancelled
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Has been cancelled
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Has been cancelled
SDK Library Integration Tests Runner / bedrock_tests (push) Has been cancelled
SDK Library Integration Tests Runner / litellm_tests (push) Has been cancelled
SDK Library Integration Tests Runner / harbor_tests (push) Has been cancelled
SDK Library Integration Tests Runner / Slack Notification (push) Has been cancelled
561 lines
20 KiB
Python
561 lines
20 KiB
Python
from typing import Any, Dict, List
|
|
|
|
from unittest.mock import Mock
|
|
|
|
from opik.anonymizer.recursive_anonymizer import RecursiveAnonymizer
|
|
|
|
|
|
class TestRecursiveAnonymizer:
|
|
"""Test suite for RecursiveAnonymizer parameter handling and nested structure processing."""
|
|
|
|
def test_recursive_anonymizer__simple_string__calls_anonymize_text_with_correct_parameters(
|
|
self,
|
|
):
|
|
"""Test that anonymize_text is called with correct parameters for a simple string."""
|
|
|
|
class MockRecursiveAnonymizer(RecursiveAnonymizer):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.anonymize_text = Mock(return_value="[ANONYMIZED]")
|
|
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
return self.anonymize_text(data, **kwargs)
|
|
|
|
anonymizer = MockRecursiveAnonymizer()
|
|
|
|
# Test with initial parameters
|
|
result = anonymizer.anonymize(
|
|
"sensitive text", field_name="input", object_type=dict
|
|
)
|
|
|
|
# Verify anonymize_text was called correctly
|
|
anonymizer.anonymize_text.assert_called_once_with(
|
|
"sensitive text", field_name="input", object_type=dict
|
|
)
|
|
assert result == "[ANONYMIZED]"
|
|
|
|
def test_recursive_anonymizer__nested_dict__preserves_field_path_in_parameters(
|
|
self,
|
|
):
|
|
"""Test that field paths are correctly built and passed for nested dictionaries."""
|
|
|
|
calls_log = []
|
|
|
|
class ParameterTrackingAnonymizer(RecursiveAnonymizer):
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
calls_log.append(
|
|
{
|
|
"data": data,
|
|
"field_name": kwargs.get("field_name"),
|
|
"object_type": kwargs.get("object_type"),
|
|
}
|
|
)
|
|
return f"[ANON:{data}]"
|
|
|
|
anonymizer = ParameterTrackingAnonymizer()
|
|
|
|
nested_data = {
|
|
"user": {
|
|
"email": "user@example.com",
|
|
"profile": {"name": "John Doe", "phone": "555-1234"},
|
|
},
|
|
"metadata": {"api_key": "secret123"},
|
|
}
|
|
|
|
result: Dict[str, Any] = anonymizer.anonymize(
|
|
nested_data, field_name="trace", object_type="TraceMessage"
|
|
)
|
|
|
|
# Verify the correct field paths were generated
|
|
expected_calls = [
|
|
{
|
|
"data": "user@example.com",
|
|
"field_name": "trace.user.email",
|
|
"object_type": "TraceMessage",
|
|
},
|
|
{
|
|
"data": "John Doe",
|
|
"field_name": "trace.user.profile.name",
|
|
"object_type": "TraceMessage",
|
|
},
|
|
{
|
|
"data": "555-1234",
|
|
"field_name": "trace.user.profile.phone",
|
|
"object_type": "TraceMessage",
|
|
},
|
|
{
|
|
"data": "secret123",
|
|
"field_name": "trace.metadata.api_key",
|
|
"object_type": "TraceMessage",
|
|
},
|
|
]
|
|
|
|
assert len(calls_log) == 4
|
|
for expected_call in expected_calls:
|
|
assert expected_call in calls_log
|
|
|
|
# Verify the structure was preserved with anonymized content
|
|
assert result["user"]["email"] == "[ANON:user@example.com]"
|
|
assert result["user"]["profile"]["name"] == "[ANON:John Doe]"
|
|
assert result["user"]["profile"]["phone"] == "[ANON:555-1234]"
|
|
assert result["metadata"]["api_key"] == "[ANON:secret123]"
|
|
|
|
def test_recursive_anonymizer__nested_list__preserves_field_path_with_indices(self):
|
|
"""Test that field paths include list indices for nested lists."""
|
|
|
|
calls_log = []
|
|
|
|
class ParameterTrackingAnonymizer(RecursiveAnonymizer):
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
calls_log.append(
|
|
{
|
|
"data": data,
|
|
"field_name": kwargs.get("field_name"),
|
|
"object_type": kwargs.get("object_type"),
|
|
}
|
|
)
|
|
return f"[ANON:{data}]"
|
|
|
|
anonymizer = ParameterTrackingAnonymizer()
|
|
|
|
list_data = [
|
|
"first item",
|
|
{"nested": "nested value", "list": ["inner item 1", "inner item 2"]},
|
|
["list item 1", "list item 2"],
|
|
]
|
|
|
|
result: List[Any] = anonymizer.anonymize(
|
|
list_data, field_name="input", object_type="SpanMessage"
|
|
)
|
|
|
|
# Verify the correct field paths were generated with indices
|
|
expected_calls = [
|
|
{
|
|
"data": "first item",
|
|
"field_name": "input.0",
|
|
"object_type": "SpanMessage",
|
|
},
|
|
{
|
|
"data": "nested value",
|
|
"field_name": "input.1.nested",
|
|
"object_type": "SpanMessage",
|
|
},
|
|
{
|
|
"data": "inner item 1",
|
|
"field_name": "input.1.list.0",
|
|
"object_type": "SpanMessage",
|
|
},
|
|
{
|
|
"data": "inner item 2",
|
|
"field_name": "input.1.list.1",
|
|
"object_type": "SpanMessage",
|
|
},
|
|
{
|
|
"data": "list item 1",
|
|
"field_name": "input.2.0",
|
|
"object_type": "SpanMessage",
|
|
},
|
|
{
|
|
"data": "list item 2",
|
|
"field_name": "input.2.1",
|
|
"object_type": "SpanMessage",
|
|
},
|
|
]
|
|
|
|
assert len(calls_log) == 6
|
|
for expected_call in expected_calls:
|
|
assert expected_call in calls_log
|
|
|
|
# Verify the structure was preserved with anonymized content
|
|
assert result[0] == "[ANON:first item]"
|
|
assert result[1]["nested"] == "[ANON:nested value]"
|
|
assert result[1]["list"] == ["[ANON:inner item 1]", "[ANON:inner item 2]"]
|
|
assert result[2] == ["[ANON:list item 1]", "[ANON:list item 2]"]
|
|
|
|
def test_recursive_anonymizer__mixed_complex_structure__handles_all_parameter_combinations(
|
|
self,
|
|
):
|
|
"""Test a complex nested structure with mixed dictionaries, lists, and strings."""
|
|
|
|
calls_log = []
|
|
|
|
class ParameterTrackingAnonymizer(RecursiveAnonymizer):
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
calls_log.append(
|
|
{
|
|
"data": data,
|
|
"field_name": kwargs.get("field_name"),
|
|
"object_type": kwargs.get("object_type"),
|
|
"custom_param": kwargs.get("custom_param"),
|
|
}
|
|
)
|
|
return f"[{kwargs.get('field_name', 'UNKNOWN')}:{data}]"
|
|
|
|
anonymizer = ParameterTrackingAnonymizer()
|
|
|
|
complex_data = {
|
|
"messages": [
|
|
{"role": "user", "content": "Hello, my email is john@example.com"},
|
|
{
|
|
"role": "assistant",
|
|
"content": "I can help you with that",
|
|
"attachments": ["file1.txt", "file2.pdf"],
|
|
},
|
|
],
|
|
"metadata": {
|
|
"session_id": "sess_12345",
|
|
"user_data": {
|
|
"preferences": ["pref1", "pref2"],
|
|
"settings": {"theme": "dark", "language": "en"},
|
|
},
|
|
},
|
|
}
|
|
|
|
result: Dict[str, Any] = anonymizer.anonymize(
|
|
complex_data,
|
|
field_name="output",
|
|
object_type="TraceMessage",
|
|
custom_param="test_value",
|
|
)
|
|
|
|
# Verify all expected calls were made with correct parameters
|
|
expected_field_paths = [
|
|
"output.messages.0.role",
|
|
"output.messages.0.content",
|
|
"output.messages.1.role",
|
|
"output.messages.1.content",
|
|
"output.messages.1.attachments.0",
|
|
"output.messages.1.attachments.1",
|
|
"output.metadata.session_id",
|
|
"output.metadata.user_data.preferences.0",
|
|
"output.metadata.user_data.preferences.1",
|
|
"output.metadata.user_data.settings.theme",
|
|
"output.metadata.user_data.settings.language",
|
|
]
|
|
|
|
assert len(calls_log) == len(expected_field_paths)
|
|
|
|
# Verify each call has the correct parameters
|
|
for call in calls_log:
|
|
assert call["object_type"] == "TraceMessage"
|
|
assert call["custom_param"] == "test_value"
|
|
assert call["field_name"] in expected_field_paths
|
|
|
|
# Verify specific anonymization results
|
|
assert (
|
|
result["messages"][0]["content"]
|
|
== "[output.messages.0.content:Hello, my email is john@example.com]"
|
|
)
|
|
assert (
|
|
result["metadata"]["session_id"]
|
|
== "[output.metadata.session_id:sess_12345]"
|
|
)
|
|
assert (
|
|
result["metadata"]["user_data"]["settings"]["theme"]
|
|
== "[output.metadata.user_data.settings.theme:dark]"
|
|
)
|
|
|
|
def test_recursive_anonymizer__max_depth_limiting__stops_recursion_at_limit(self):
|
|
"""Test that max_depth parameter properly limits recursion depth."""
|
|
|
|
calls_log = []
|
|
|
|
class ParameterTrackingAnonymizer(RecursiveAnonymizer):
|
|
def __init__(self, max_depth: int = 2):
|
|
super().__init__(max_depth=max_depth)
|
|
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
calls_log.append(
|
|
{
|
|
"data": data,
|
|
"field_name": kwargs.get("field_name"),
|
|
}
|
|
)
|
|
return f"[ANON:{data}]"
|
|
|
|
anonymizer = ParameterTrackingAnonymizer(max_depth=2)
|
|
|
|
# Create a structure where strings at different depths can be tested
|
|
deeply_nested = {
|
|
"level1_text": "depth 1 - should be processed",
|
|
"level1": {
|
|
"level2_text": "depth 2 - should be processed",
|
|
"level2": {"level3_text": "depth 3 - should NOT be processed"},
|
|
},
|
|
}
|
|
|
|
result: Dict[str, Any] = anonymizer.anonymize(deeply_nested, field_name="root")
|
|
|
|
field_names = [call["field_name"] for call in calls_log]
|
|
|
|
# The recursion depth starts at 0 for the initial call, so with max_depth=2:
|
|
# - root.level1_text is at depth 1 (should be processed)
|
|
# - root.level1.level2_text is at depth 2 (exceeds max_depth=2, should NOT be processed)
|
|
# - root.level1.level2.level3_text is at depth 3+ (exceeds max_depth=2, should NOT be processed)
|
|
|
|
assert "root.level1_text" in field_names
|
|
assert len([name for name in field_names if "level2_text" in name]) == 0
|
|
assert len([name for name in field_names if "level3_text" in name]) == 0
|
|
|
|
# Verify the results
|
|
assert result["level1_text"] == "[ANON:depth 1 - should be processed]"
|
|
assert (
|
|
result["level1"]["level2_text"] == "depth 2 - should be processed"
|
|
) # Unchanged due to depth limit
|
|
assert (
|
|
result["level1"]["level2"]["level3_text"]
|
|
== "depth 3 - should NOT be processed"
|
|
) # Unchanged
|
|
|
|
def test_recursive_anonymizer__non_string_types__preserves_unchanged(self):
|
|
"""Test that non-string types are preserved without calling anonymize_text."""
|
|
|
|
calls_log = []
|
|
|
|
class ParameterTrackingAnonymizer(RecursiveAnonymizer):
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
calls_log.append(data)
|
|
return f"[ANON:{data}]"
|
|
|
|
anonymizer = ParameterTrackingAnonymizer()
|
|
|
|
mixed_types_data = {
|
|
"string_field": "text to anonymize",
|
|
"int_field": 42,
|
|
"float_field": 3.14,
|
|
"bool_field": True,
|
|
"none_field": None,
|
|
"nested": {
|
|
"another_string": "another text",
|
|
"number": 100,
|
|
"list_with_mixed": ["string in list", 123, False],
|
|
},
|
|
}
|
|
|
|
result: Dict[str, Any] = anonymizer.anonymize(
|
|
mixed_types_data, field_name="data"
|
|
)
|
|
|
|
# Should only anonymize strings (there are 3: "text to anonymize", "another text", "string in list")
|
|
assert len(calls_log) == 3
|
|
assert "text to anonymize" in calls_log
|
|
assert "another text" in calls_log
|
|
assert "string in list" in calls_log
|
|
|
|
# Non-string types should be preserved
|
|
assert result["int_field"] == 42
|
|
assert result["float_field"] == 3.14
|
|
assert result["bool_field"] is True
|
|
assert result["none_field"] is None
|
|
assert result["nested"]["number"] == 100
|
|
assert result["nested"]["list_with_mixed"][1] == 123
|
|
assert result["nested"]["list_with_mixed"][2] is False
|
|
|
|
# Strings should be anonymized
|
|
assert result["string_field"] == "[ANON:text to anonymize]"
|
|
assert result["nested"]["another_string"] == "[ANON:another text]"
|
|
assert result["nested"]["list_with_mixed"][0] == "[ANON:string in list]"
|
|
|
|
def test_recursive_anonymizer__empty_structures__handles_gracefully(self):
|
|
"""Test that empty dictionaries and lists are handled gracefully."""
|
|
|
|
calls_log = []
|
|
|
|
class ParameterTrackingAnonymizer(RecursiveAnonymizer):
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
calls_log.append(data)
|
|
return f"[ANON:{data}]"
|
|
|
|
anonymizer = ParameterTrackingAnonymizer()
|
|
|
|
empty_structures = {
|
|
"empty_dict": {},
|
|
"empty_list": [],
|
|
"mixed": {
|
|
"nested_empty_dict": {},
|
|
"nested_empty_list": [],
|
|
"text": "some text",
|
|
},
|
|
}
|
|
|
|
result: Dict[str, Any] = anonymizer.anonymize(
|
|
empty_structures, field_name="test"
|
|
)
|
|
|
|
# Should only call anonymize_text for the one string
|
|
assert len(calls_log) == 1
|
|
assert "some text" in calls_log
|
|
|
|
# Empty structures should be preserved
|
|
assert result["empty_dict"] == {}
|
|
assert result["empty_list"] == []
|
|
assert result["mixed"]["nested_empty_dict"] == {}
|
|
assert result["mixed"]["nested_empty_list"] == []
|
|
assert result["mixed"]["text"] == "[ANON:some text]"
|
|
|
|
def test_recursive_anonymizer__field_specific_anonymization__uses_field_path_for_logic(
|
|
self,
|
|
):
|
|
"""Test that anonymizers can use field paths to implement field-specific logic."""
|
|
|
|
class FieldSpecificAnonymizer(RecursiveAnonymizer):
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
field_name = kwargs.get("field_name", "")
|
|
|
|
# Different anonymization based on a field path
|
|
if "email" in field_name:
|
|
return "[EMAIL_REDACTED]"
|
|
elif "phone" in field_name:
|
|
return "[PHONE_REDACTED]"
|
|
elif "api_key" in field_name:
|
|
return "[API_KEY_REDACTED]"
|
|
elif field_name.endswith(".name"):
|
|
return "[NAME_REDACTED]"
|
|
else:
|
|
return data # Leave unchanged for other fields
|
|
|
|
anonymizer = FieldSpecificAnonymizer()
|
|
|
|
user_data = {
|
|
"user": {
|
|
"email": "john.doe@example.com",
|
|
"name": "John Doe",
|
|
"phone": "555-1234",
|
|
"notes": "Regular user notes",
|
|
},
|
|
"config": {
|
|
"api_key": "secret123",
|
|
"description": "Configuration description",
|
|
},
|
|
"contacts": [
|
|
{
|
|
"name": "Contact One",
|
|
"email": "contact1@example.com",
|
|
"other_info": "Some other information",
|
|
}
|
|
],
|
|
}
|
|
|
|
result: Dict[str, Any] = anonymizer.anonymize(user_data, field_name="input")
|
|
|
|
# Verify field-specific anonymization
|
|
assert result["user"]["email"] == "[EMAIL_REDACTED]"
|
|
assert result["user"]["name"] == "[NAME_REDACTED]"
|
|
assert result["user"]["phone"] == "[PHONE_REDACTED]"
|
|
assert result["user"]["notes"] == "Regular user notes" # Unchanged
|
|
|
|
assert result["config"]["api_key"] == "[API_KEY_REDACTED]"
|
|
assert (
|
|
result["config"]["description"] == "Configuration description"
|
|
) # Unchanged
|
|
|
|
assert result["contacts"][0]["name"] == "[NAME_REDACTED]"
|
|
assert result["contacts"][0]["email"] == "[EMAIL_REDACTED]"
|
|
assert (
|
|
result["contacts"][0]["other_info"] == "Some other information"
|
|
) # Unchanged
|
|
|
|
def test_recursive_anonymizer__parameter_propagation__all_kwargs_preserved(self):
|
|
"""Test that all custom kwargs are properly propagated to anonymize_text."""
|
|
|
|
calls_log = []
|
|
|
|
class ParameterTrackingAnonymizer(RecursiveAnonymizer):
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
calls_log.append(kwargs.copy())
|
|
return data
|
|
|
|
anonymizer = ParameterTrackingAnonymizer()
|
|
|
|
test_data = {"nested": {"text": "sample text"}}
|
|
|
|
# Pass multiple custom parameters
|
|
anonymizer.anonymize(
|
|
test_data,
|
|
field_name="test_field",
|
|
object_type="TestMessage",
|
|
custom_param1="value1",
|
|
custom_param2=42,
|
|
custom_param3={"nested": "param"},
|
|
)
|
|
|
|
# Should have one call for the text
|
|
assert len(calls_log) == 1
|
|
kwargs = calls_log[0]
|
|
|
|
# Verify all parameters were preserved
|
|
assert kwargs["field_name"] == "test_field.nested.text"
|
|
assert kwargs["object_type"] == "TestMessage"
|
|
assert kwargs["custom_param1"] == "value1"
|
|
assert kwargs["custom_param2"] == 42
|
|
assert kwargs["custom_param3"] == {"nested": "param"}
|
|
|
|
def test_recursive_anonymizer__circular_reference_protection__respects_max_depth(
|
|
self,
|
|
):
|
|
"""Test that max_depth prevents infinite recursion even with circular references."""
|
|
|
|
calls_count = 0
|
|
|
|
class CountingAnonymizer(RecursiveAnonymizer):
|
|
def __init__(self):
|
|
super().__init__(max_depth=3)
|
|
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
nonlocal calls_count
|
|
calls_count += 1
|
|
return f"[CALL_{calls_count}:{data}]"
|
|
|
|
anonymizer = CountingAnonymizer()
|
|
|
|
# Create a structure that tests depth limiting with strings at different levels
|
|
deep_structure = {
|
|
"text_at_level1": "depth 1 - should be processed",
|
|
"level1": {
|
|
"text_at_level2": "depth 2 - should be processed",
|
|
"level2": {
|
|
"text_at_level3": "depth 3 - should be processed",
|
|
"level3": {"text_at_level4": "depth 4 - should NOT be processed"},
|
|
},
|
|
},
|
|
}
|
|
|
|
result: Dict[str, Any] = anonymizer.anonymize(deep_structure, field_name="root")
|
|
|
|
# With max_depth=3, strings at depth 1, 2, 3 should be processed, but depth 4+ should not
|
|
|
|
# Verify the structure - strings beyond max_depth should remain unchanged
|
|
assert (
|
|
"depth 4 - should NOT be processed"
|
|
in result["level1"]["level2"]["level3"]["text_at_level4"]
|
|
)
|
|
|
|
# The strings within max_depth should be processed
|
|
# With max_depth=3, only strings at depth 1 and 2 get processed
|
|
assert calls_count == 2 # Should process the first 2 strings within max_depth
|
|
|
|
def test_recursive_anonymizer__no_field_name_provided__uses_empty_string_as_base(
|
|
self,
|
|
):
|
|
"""Test behavior when no field_name is provided in initial kwargs."""
|
|
|
|
calls_log = []
|
|
|
|
class ParameterTrackingAnonymizer(RecursiveAnonymizer):
|
|
def anonymize_text(self, data: str, **kwargs: Any) -> str:
|
|
calls_log.append(kwargs.get("field_name"))
|
|
return data
|
|
|
|
anonymizer = ParameterTrackingAnonymizer()
|
|
|
|
test_data = {"key1": "value1", "nested": {"key2": "value2"}}
|
|
|
|
# Call without field_name
|
|
anonymizer.anonymize(test_data)
|
|
|
|
# Should use empty string as a base and build paths from there
|
|
expected_field_names = [".key1", ".nested.key2"]
|
|
assert len(calls_log) == 2
|
|
for field_name in calls_log:
|
|
assert field_name in expected_field_names
|