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
467 lines
15 KiB
Python
467 lines
15 KiB
Python
"""
|
|
Comprehensive tests for security/file_write_verifier.py
|
|
|
|
Tests cover:
|
|
- _sanitize_sensitive_data function
|
|
- write_file_verified function
|
|
- write_json_verified function
|
|
- FileWriteSecurityError exception
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
|
|
class TestSanitizeSensitiveData:
|
|
"""Tests for the _sanitize_sensitive_data function."""
|
|
|
|
def test_sanitizes_password_key(self):
|
|
"""Test that password keys are redacted."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
data = {"username": "admin", "password": "secret123"}
|
|
result = _sanitize_sensitive_data(data)
|
|
|
|
assert result["username"] == "admin"
|
|
assert result["password"] == "[REDACTED]"
|
|
|
|
def test_sanitizes_api_key(self):
|
|
"""Test that api_key is redacted."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
data = {"api_key": "sk-123456", "name": "test"}
|
|
result = _sanitize_sensitive_data(data)
|
|
|
|
assert result["api_key"] == "[REDACTED]"
|
|
assert result["name"] == "test"
|
|
|
|
def test_sanitizes_various_sensitive_keys(self):
|
|
"""Test that all sensitive key variations are redacted."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
data = {
|
|
"apikey": "value1",
|
|
"api-key": "value2",
|
|
"secret": "value3",
|
|
"secret_key": "value4",
|
|
"token": "value5",
|
|
"access_token": "value6",
|
|
"refresh_token": "value7",
|
|
"private_key": "value8",
|
|
"credentials": "value9",
|
|
"auth": "value10",
|
|
"authorization": "value11",
|
|
}
|
|
result = _sanitize_sensitive_data(data)
|
|
|
|
for key in data:
|
|
assert result[key] == "[REDACTED]"
|
|
|
|
def test_case_insensitive_matching(self):
|
|
"""Test that matching is case insensitive."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
data = {"PASSWORD": "secret", "Api_Key": "key123", "TOKEN": "tok"}
|
|
result = _sanitize_sensitive_data(data)
|
|
|
|
assert result["PASSWORD"] == "[REDACTED]"
|
|
assert result["Api_Key"] == "[REDACTED]"
|
|
assert result["TOKEN"] == "[REDACTED]"
|
|
|
|
def test_sanitizes_nested_dicts(self):
|
|
"""Test that nested dictionaries are sanitized."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
data = {
|
|
"config": {
|
|
"api_key": "secret",
|
|
"settings": {"password": "hidden", "name": "test"},
|
|
}
|
|
}
|
|
result = _sanitize_sensitive_data(data)
|
|
|
|
assert result["config"]["api_key"] == "[REDACTED]"
|
|
assert result["config"]["settings"]["password"] == "[REDACTED]"
|
|
assert result["config"]["settings"]["name"] == "test"
|
|
|
|
def test_sanitizes_lists_of_dicts(self):
|
|
"""Test that lists containing dicts are sanitized."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
data = {
|
|
"users": [
|
|
{"name": "user1", "password": "pass1"},
|
|
{"name": "user2", "password": "pass2"},
|
|
]
|
|
}
|
|
result = _sanitize_sensitive_data(data)
|
|
|
|
assert result["users"][0]["password"] == "[REDACTED]"
|
|
assert result["users"][1]["password"] == "[REDACTED]"
|
|
assert result["users"][0]["name"] == "user1"
|
|
|
|
def test_preserves_non_sensitive_data(self):
|
|
"""Test that non-sensitive data is preserved."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
data = {"name": "test", "value": 123, "flag": True, "items": [1, 2, 3]}
|
|
result = _sanitize_sensitive_data(data)
|
|
|
|
assert result == data
|
|
|
|
def test_handles_primitive_values(self):
|
|
"""Test that primitive values pass through unchanged."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
assert _sanitize_sensitive_data("string") == "string"
|
|
assert _sanitize_sensitive_data(123) == 123
|
|
assert _sanitize_sensitive_data(True) is True
|
|
assert _sanitize_sensitive_data(None) is None
|
|
|
|
def test_handles_list_of_primitives(self):
|
|
"""Test that lists of primitives pass through."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
data = [1, 2, "test", True]
|
|
result = _sanitize_sensitive_data(data)
|
|
|
|
assert result == data
|
|
|
|
def test_handles_non_string_keys(self):
|
|
"""Test handling of non-string dictionary keys."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
_sanitize_sensitive_data,
|
|
)
|
|
|
|
data = {1: "value1", "password": "secret"}
|
|
result = _sanitize_sensitive_data(data)
|
|
|
|
assert result[1] == "value1"
|
|
assert result["password"] == "[REDACTED]"
|
|
|
|
|
|
class TestFileWriteSecurityError:
|
|
"""Tests for the FileWriteSecurityError exception."""
|
|
|
|
def test_exception_can_be_raised(self):
|
|
"""Test that the exception can be raised and caught."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
FileWriteSecurityError,
|
|
)
|
|
|
|
with pytest.raises(FileWriteSecurityError) as exc_info:
|
|
raise FileWriteSecurityError("Test error message")
|
|
|
|
assert "Test error message" in str(exc_info.value)
|
|
|
|
def test_exception_inherits_from_exception(self):
|
|
"""Test that FileWriteSecurityError inherits from Exception."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
FileWriteSecurityError,
|
|
)
|
|
|
|
assert issubclass(FileWriteSecurityError, Exception)
|
|
|
|
|
|
class TestWriteFileVerified:
|
|
"""Tests for the write_file_verified function."""
|
|
|
|
@pytest.fixture
|
|
def mock_get_setting(self):
|
|
"""Fixture to mock the get_setting_from_snapshot function."""
|
|
with patch(
|
|
"local_deep_research.config.search_config.get_setting_from_snapshot"
|
|
) as mock:
|
|
yield mock
|
|
|
|
def test_writes_file_when_setting_matches(self, tmp_path, mock_get_setting):
|
|
"""Test that file is written when setting matches required value."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_file_verified,
|
|
)
|
|
|
|
filepath = tmp_path / "test.txt"
|
|
content = "test content"
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_file_verified(
|
|
filepath, content, "test.setting", required_value=True
|
|
)
|
|
|
|
assert filepath.exists()
|
|
assert filepath.read_text() == content
|
|
|
|
def test_raises_error_when_setting_mismatch(
|
|
self, tmp_path, mock_get_setting
|
|
):
|
|
"""Test that error is raised when setting doesn't match."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_file_verified,
|
|
FileWriteSecurityError,
|
|
)
|
|
|
|
filepath = tmp_path / "test.txt"
|
|
mock_get_setting.return_value = False
|
|
|
|
with pytest.raises(FileWriteSecurityError) as exc_info:
|
|
write_file_verified(
|
|
filepath,
|
|
"content",
|
|
"test.setting",
|
|
required_value=True,
|
|
context="test operation",
|
|
)
|
|
|
|
assert "test operation" in str(exc_info.value)
|
|
assert "test.setting=True" in str(exc_info.value)
|
|
|
|
def test_raises_error_when_setting_not_found(
|
|
self, tmp_path, mock_get_setting
|
|
):
|
|
"""Test that error is raised when setting doesn't exist."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_file_verified,
|
|
FileWriteSecurityError,
|
|
)
|
|
|
|
filepath = tmp_path / "test.txt"
|
|
mock_get_setting.side_effect = KeyError("Setting not found")
|
|
|
|
with pytest.raises(FileWriteSecurityError):
|
|
write_file_verified(filepath, "content", "nonexistent.setting")
|
|
|
|
def test_writes_binary_file(self, tmp_path, mock_get_setting):
|
|
"""Test writing binary content."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_file_verified,
|
|
)
|
|
|
|
filepath = tmp_path / "test.bin"
|
|
content = b"\x00\x01\x02\x03"
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_file_verified(filepath, content, "test.setting", mode="wb")
|
|
|
|
assert filepath.exists()
|
|
assert filepath.read_bytes() == content
|
|
|
|
def test_uses_custom_encoding(self, tmp_path, mock_get_setting):
|
|
"""Test writing with custom encoding."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_file_verified,
|
|
)
|
|
|
|
filepath = tmp_path / "test.txt"
|
|
content = "日本語テスト"
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_file_verified(filepath, content, "test.setting", encoding="utf-8")
|
|
|
|
assert filepath.exists()
|
|
assert filepath.read_text(encoding="utf-8") == content
|
|
|
|
def test_passes_settings_snapshot(self, tmp_path, mock_get_setting):
|
|
"""Test that settings_snapshot is passed to get_setting_from_snapshot."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_file_verified,
|
|
)
|
|
|
|
filepath = tmp_path / "test.txt"
|
|
snapshot = {"test.setting": True}
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_file_verified(
|
|
filepath, "content", "test.setting", settings_snapshot=snapshot
|
|
)
|
|
|
|
mock_get_setting.assert_called_once_with(
|
|
"test.setting", settings_snapshot=snapshot
|
|
)
|
|
|
|
def test_accepts_path_object(self, tmp_path, mock_get_setting):
|
|
"""Test that Path objects are accepted."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_file_verified,
|
|
)
|
|
|
|
filepath = Path(tmp_path) / "test.txt"
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_file_verified(filepath, "content", "test.setting")
|
|
|
|
assert filepath.exists()
|
|
|
|
|
|
class TestWriteJsonVerified:
|
|
"""Tests for the write_json_verified function."""
|
|
|
|
@pytest.fixture
|
|
def mock_get_setting(self):
|
|
"""Fixture to mock the get_setting_from_snapshot function."""
|
|
with patch(
|
|
"local_deep_research.config.search_config.get_setting_from_snapshot"
|
|
) as mock:
|
|
yield mock
|
|
|
|
def test_writes_json_when_setting_matches(self, tmp_path, mock_get_setting):
|
|
"""Test that JSON is written when setting matches."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_json_verified,
|
|
)
|
|
|
|
filepath = tmp_path / "test.json"
|
|
data = {"key": "value", "number": 42}
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_json_verified(filepath, data, "test.setting")
|
|
|
|
assert filepath.exists()
|
|
written_data = json.loads(filepath.read_text())
|
|
assert written_data == data
|
|
|
|
def test_sanitizes_sensitive_data(self, tmp_path, mock_get_setting):
|
|
"""Test that sensitive data is sanitized before writing."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_json_verified,
|
|
)
|
|
|
|
filepath = tmp_path / "test.json"
|
|
data = {"name": "test", "password": "secret123", "api_key": "key456"}
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_json_verified(filepath, data, "test.setting")
|
|
|
|
written_data = json.loads(filepath.read_text())
|
|
assert written_data["name"] == "test"
|
|
assert written_data["password"] == "[REDACTED]"
|
|
assert written_data["api_key"] == "[REDACTED]"
|
|
|
|
def test_uses_default_indent(self, tmp_path, mock_get_setting):
|
|
"""Test that default indent of 2 is used."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_json_verified,
|
|
)
|
|
|
|
filepath = tmp_path / "test.json"
|
|
data = {"key": "value"}
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_json_verified(filepath, data, "test.setting")
|
|
|
|
content = filepath.read_text()
|
|
# With indent=2, there should be newlines and spaces
|
|
assert "\n" in content
|
|
assert " " in content
|
|
|
|
def test_custom_json_kwargs(self, tmp_path, mock_get_setting):
|
|
"""Test that custom JSON kwargs are passed through."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_json_verified,
|
|
)
|
|
|
|
filepath = tmp_path / "test.json"
|
|
data = {"b": 2, "a": 1}
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_json_verified(
|
|
filepath, data, "test.setting", sort_keys=True, indent=4
|
|
)
|
|
|
|
content = filepath.read_text()
|
|
# With sort_keys, 'a' should come before 'b'
|
|
assert content.index('"a"') < content.index('"b"')
|
|
# With indent=4, should have 4 spaces
|
|
assert " " in content
|
|
|
|
def test_writes_list_data(self, tmp_path, mock_get_setting):
|
|
"""Test writing list data."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_json_verified,
|
|
)
|
|
|
|
filepath = tmp_path / "test.json"
|
|
data = [{"name": "item1"}, {"name": "item2", "password": "secret"}]
|
|
|
|
mock_get_setting.return_value = True
|
|
|
|
write_json_verified(filepath, data, "test.setting")
|
|
|
|
written_data = json.loads(filepath.read_text())
|
|
assert len(written_data) == 2
|
|
assert written_data[1]["password"] == "[REDACTED]"
|
|
|
|
def test_raises_error_when_setting_mismatch(
|
|
self, tmp_path, mock_get_setting
|
|
):
|
|
"""Test that error is raised when setting doesn't match."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
write_json_verified,
|
|
FileWriteSecurityError,
|
|
)
|
|
|
|
filepath = tmp_path / "test.json"
|
|
|
|
mock_get_setting.return_value = False
|
|
|
|
with pytest.raises(FileWriteSecurityError):
|
|
write_json_verified(filepath, {"key": "value"}, "test.setting")
|
|
|
|
|
|
class TestSensitiveKeys:
|
|
"""Tests for SENSITIVE_KEYS constant."""
|
|
|
|
def test_sensitive_keys_is_frozenset(self):
|
|
"""Test that SENSITIVE_KEYS is a frozenset."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
SENSITIVE_KEYS,
|
|
)
|
|
|
|
assert isinstance(SENSITIVE_KEYS, frozenset)
|
|
|
|
def test_sensitive_keys_contains_expected_values(self):
|
|
"""Test that SENSITIVE_KEYS contains expected sensitive key names."""
|
|
from local_deep_research.security.file_write_verifier import (
|
|
SENSITIVE_KEYS,
|
|
)
|
|
|
|
expected_keys = {
|
|
"password",
|
|
"api_key",
|
|
"secret",
|
|
"token",
|
|
"credentials",
|
|
}
|
|
|
|
for key in expected_keys:
|
|
assert key in SENSITIVE_KEYS
|