Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

191 lines
7.2 KiB
Python

"""Tests for DataSanitizer.redact_settings_snapshot.
The plain DataSanitizer.redact() does NOT redact secrets in nested-with-metadata
snapshots like {"llm.openai.api_key": {"value": "sk-...", "ui_element": "password"}}
— the outer compound key isn't in the sensitive-name set, and the inner key
"value" isn't sensitive either. redact_settings_snapshot fixes that for the
specific shape produced by SettingsManager.get_all_settings().
"""
from local_deep_research.security.data_sanitizer import DataSanitizer
def test_redacts_value_when_ui_element_is_password():
snap = {
"llm.openai.api_key": {
"value": "sk-secret123",
"ui_element": "password",
"type": "LLM",
}
}
out = DataSanitizer.redact_settings_snapshot(snap)
assert out["llm.openai.api_key"]["value"] == "[REDACTED]"
def test_preserves_metadata_alongside_redacted_value():
"""ui_element/type/etc. survive so YAML diffs still show the field exists."""
snap = {
"llm.openai.api_key": {
"value": "sk-secret",
"ui_element": "password",
"type": "LLM",
"description": "OpenAI key",
}
}
out = DataSanitizer.redact_settings_snapshot(snap)
assert out["llm.openai.api_key"]["ui_element"] == "password"
assert out["llm.openai.api_key"]["type"] == "LLM"
assert out["llm.openai.api_key"]["description"] == "OpenAI key"
def test_does_not_redact_non_secret_settings():
snap = {
"search.fetch.mode": {
"value": "summary_focus_query",
"ui_element": "select",
"type": "SEARCH",
}
}
out = DataSanitizer.redact_settings_snapshot(snap)
assert out["search.fetch.mode"]["value"] == "summary_focus_query"
def test_empty_secret_value_stays_empty():
"""An unset API key reads as "" — leaving it empty is more useful than
"[REDACTED]" when diffing two runs to spot which had the key set."""
snap = {"llm.lmstudio.api_key": {"value": "", "ui_element": "password"}}
out = DataSanitizer.redact_settings_snapshot(snap)
assert out["llm.lmstudio.api_key"]["value"] == ""
def test_none_secret_value_stays_none():
snap = {"llm.lmstudio.api_key": {"value": None, "ui_element": "password"}}
out = DataSanitizer.redact_settings_snapshot(snap)
assert out["llm.lmstudio.api_key"]["value"] is None
def test_defense_in_depth_via_key_suffix():
"""ui_element=text + key suffix in DEFAULT_SENSITIVE_KEYS still redacts.
Catches developer error: a future plugin author who registers
`plugin.x.api_key` with the wrong ui_element should still have the
secret redacted because `api_key` is in DEFAULT_SENSITIVE_KEYS.
"""
snap = {"plugin.x.api_key": {"value": "sk-leaked", "ui_element": "text"}}
out = DataSanitizer.redact_settings_snapshot(snap)
assert out["plugin.x.api_key"]["value"] == "[REDACTED]"
def test_input_is_not_mutated():
"""Pure-function contract — the in-memory snapshot used by the running
benchmark thread must remain unredacted; only the persisted copy is
redacted."""
snap = {
"llm.openai.api_key": {
"value": "sk-secret",
"ui_element": "password",
}
}
DataSanitizer.redact_settings_snapshot(snap)
assert snap["llm.openai.api_key"]["value"] == "sk-secret"
def test_passes_through_non_metadata_entries():
"""Tolerates flat-shape entries (bare key→value) so the helper is safe
on mixed snapshots without crashing."""
snap = {"flat_key": "raw_value", "no_value_key": {"ui_element": "text"}}
out = DataSanitizer.redact_settings_snapshot(snap)
assert out["flat_key"] == "raw_value"
assert out["no_value_key"] == {"ui_element": "text"}
def test_passes_through_non_dict_input():
"""Defensive — a None or list snapshot returns unchanged."""
assert DataSanitizer.redact_settings_snapshot(None) is None
assert DataSanitizer.redact_settings_snapshot([1, 2, 3]) == [1, 2, 3]
def test_custom_redaction_text():
snap = {"llm.openai.api_key": {"value": "sk-x", "ui_element": "password"}}
out = DataSanitizer.redact_settings_snapshot(snap, redaction_text="***")
assert out["llm.openai.api_key"]["value"] == "***"
def test_is_sensitive_setting_predicate():
"""The shared predicate the GET redactor and the write-back guards both
use: password ui_element OR a sensitive key suffix."""
# ui_element wins regardless of key
assert DataSanitizer.is_sensitive_setting("anything.here", "password")
# sensitive suffix wins regardless of ui_element
assert DataSanitizer.is_sensitive_setting("llm.openai.api_key", "text")
assert DataSanitizer.is_sensitive_setting("x.password", None)
assert DataSanitizer.is_sensitive_setting("x.secret", "textarea")
# neither -> not sensitive
assert not DataSanitizer.is_sensitive_setting("llm.model", "text")
assert not DataSanitizer.is_sensitive_setting("search.tool", None)
def test_redactor_and_predicate_agree():
"""Anything the predicate calls sensitive (and has a non-empty value)
must be redacted by redact_settings_snapshot — they share one source."""
snap = {
"llm.openai.api_key": {"value": "sk-x", "ui_element": "text"},
"foo.password": {"value": "pw", "ui_element": "text"},
"llm.model": {"value": "gpt", "ui_element": "text"},
}
out = DataSanitizer.redact_settings_snapshot(snap)
for key, entry in snap.items():
sensitive = DataSanitizer.is_sensitive_setting(key, entry["ui_element"])
if sensitive:
assert out[key]["value"] == DataSanitizer.REDACTION_TEXT
else:
assert out[key]["value"] == entry["value"]
# ---------------------------------------------------------------------------
# redact_value — the single-value primitive that the singular GET, the bulk
# GET and redact_settings_snapshot all delegate to.
# ---------------------------------------------------------------------------
def test_redact_value_masks_password_ui_element():
"""A set secret on a password-typed setting returns the sentinel."""
assert (
DataSanitizer.redact_value("llm.openai.api_key", "password", "sk-real")
== DataSanitizer.REDACTION_TEXT
)
def test_redact_value_masks_by_suffix_when_no_ui_element():
"""The bulk GET has no ui_element, so redact_value falls back to the
suffix arm of the predicate (key ends in a sensitive name)."""
assert (
DataSanitizer.redact_value("llm.openai.api_key", None, "sk-real")
== DataSanitizer.REDACTION_TEXT
)
def test_redact_value_passes_through_non_secret():
"""A non-sensitive setting is returned unchanged."""
assert DataSanitizer.redact_value("llm.model", "text", "gpt") == "gpt"
def test_redact_value_leaves_empty_values_readable():
"""Empty values are never masked — the UI must tell 'unset' from 'set'
without the sentinel implying a secret exists."""
for empty in (None, "", [], {}):
assert (
DataSanitizer.redact_value("llm.openai.api_key", "password", empty)
== empty
)
def test_redact_value_respects_custom_redaction_text():
"""Callers can override the sentinel."""
assert (
DataSanitizer.redact_value(
"x.password", None, "pw", redaction_text="***"
)
== "***"
)