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
682 lines
28 KiB
Python
682 lines
28 KiB
Python
"""
|
|
Tests for server_config module.
|
|
|
|
Covers server configuration management for web app startup.
|
|
All settings are read from environment variables (LDR_* naming convention).
|
|
Includes tests for the legacy server_config.json migration path.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.web.server_config import (
|
|
get_server_config_path,
|
|
has_legacy_customizations,
|
|
load_server_config,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared fixture: mock get_typed_setting_value to return defaults
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.fixture()
|
|
def _default_typed_setting():
|
|
"""Mock get_typed_setting_value to always return default."""
|
|
with patch(
|
|
"local_deep_research.web.server_config.get_typed_setting_value",
|
|
side_effect=lambda key, val, typ, default: default,
|
|
):
|
|
yield
|
|
|
|
|
|
@pytest.fixture()
|
|
def _env_typed_setting():
|
|
"""Mock get_typed_setting_value to simulate env var override.
|
|
|
|
Stores overrides in a dict; returns the override if present, else default.
|
|
"""
|
|
overrides = {}
|
|
|
|
def _side_effect(key, val, typ, default):
|
|
return overrides.get(key, default)
|
|
|
|
with patch(
|
|
"local_deep_research.web.server_config.get_typed_setting_value",
|
|
side_effect=_side_effect,
|
|
):
|
|
yield overrides
|
|
|
|
|
|
# ===================================================================
|
|
# load_server_config — structure and defaults
|
|
# ===================================================================
|
|
class TestLoadServerConfig:
|
|
"""Tests for load_server_config function."""
|
|
|
|
# -- keys and types -------------------------------------------------
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_returns_dict(self):
|
|
"""Should return a dict."""
|
|
result = load_server_config()
|
|
assert isinstance(result, dict)
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_contains_expected_keys(self):
|
|
"""Should contain all expected configuration keys."""
|
|
result = load_server_config()
|
|
expected_keys = {
|
|
"host",
|
|
"port",
|
|
"debug",
|
|
"use_https",
|
|
"allow_registrations",
|
|
"rate_limit_default",
|
|
"rate_limit_login",
|
|
"rate_limit_registration",
|
|
"rate_limit_settings",
|
|
"rate_limit_upload_user",
|
|
"rate_limit_upload_ip",
|
|
}
|
|
assert expected_keys.issubset(result.keys())
|
|
|
|
# -- defaults -------------------------------------------------------
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_host(self):
|
|
"""Default host should be 0.0.0.0."""
|
|
assert load_server_config()["host"] == "0.0.0.0"
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_port(self):
|
|
"""Default port should be 5000."""
|
|
assert load_server_config()["port"] == 5000
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_debug(self):
|
|
"""Default debug should be False."""
|
|
assert load_server_config()["debug"] is False
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_use_https(self):
|
|
"""Default use_https should be True."""
|
|
assert load_server_config()["use_https"] is True
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_allow_registrations(self):
|
|
"""Default allow_registrations should be True."""
|
|
assert load_server_config()["allow_registrations"] is True
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_rate_limit_default(self):
|
|
"""Default rate_limit_default value."""
|
|
assert (
|
|
load_server_config()["rate_limit_default"]
|
|
== "5000 per hour;50000 per day"
|
|
)
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_rate_limit_login(self):
|
|
"""Default rate_limit_login value."""
|
|
assert load_server_config()["rate_limit_login"] == "5 per 15 minutes"
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_rate_limit_registration(self):
|
|
"""Default rate_limit_registration value."""
|
|
assert load_server_config()["rate_limit_registration"] == "3 per hour"
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_rate_limit_settings(self):
|
|
"""Default rate_limit_settings value."""
|
|
assert load_server_config()["rate_limit_settings"] == "30 per minute"
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_rate_limit_upload_user(self):
|
|
"""Default rate_limit_upload_user value."""
|
|
assert (
|
|
load_server_config()["rate_limit_upload_user"]
|
|
== "60 per minute;1000 per hour"
|
|
)
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_default_rate_limit_upload_ip(self):
|
|
"""Default rate_limit_upload_ip value."""
|
|
assert (
|
|
load_server_config()["rate_limit_upload_ip"]
|
|
== "60 per minute;1000 per hour"
|
|
)
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_defaults_when_no_env_vars(self):
|
|
"""Should return all defaults when no environment variables are set."""
|
|
result = load_server_config()
|
|
|
|
assert result["host"] == "0.0.0.0"
|
|
assert result["port"] == 5000
|
|
assert result["debug"] is False
|
|
assert result["use_https"] is True
|
|
assert result["allow_registrations"] is True
|
|
assert result["rate_limit_default"] == "5000 per hour;50000 per day"
|
|
assert result["rate_limit_login"] == "5 per 15 minutes"
|
|
assert result["rate_limit_registration"] == "3 per hour"
|
|
assert result["rate_limit_upload_user"] == "60 per minute;1000 per hour"
|
|
assert result["rate_limit_upload_ip"] == "60 per minute;1000 per hour"
|
|
assert result["rate_limit_settings"] == "30 per minute"
|
|
|
|
# -- env var overrides -----------------------------------------------
|
|
|
|
def test_env_var_overrides_host(self, _env_typed_setting):
|
|
"""LDR_WEB_HOST env var should override default host."""
|
|
_env_typed_setting["web.host"] = "127.0.0.1"
|
|
result = load_server_config()
|
|
assert result["host"] == "127.0.0.1"
|
|
|
|
def test_env_var_overrides_port(self, _env_typed_setting):
|
|
"""LDR_WEB_PORT env var should override default port."""
|
|
_env_typed_setting["web.port"] = 8080
|
|
result = load_server_config()
|
|
assert result["port"] == 8080
|
|
|
|
def test_env_var_overrides_debug(self, _env_typed_setting):
|
|
"""LDR_APP_DEBUG env var should override default debug."""
|
|
_env_typed_setting["app.debug"] = True
|
|
result = load_server_config()
|
|
assert result["debug"] is True
|
|
|
|
def test_env_var_overrides_use_https(self, _env_typed_setting):
|
|
"""LDR_WEB_USE_HTTPS env var should override default use_https."""
|
|
_env_typed_setting["web.use_https"] = False
|
|
result = load_server_config()
|
|
assert result["use_https"] is False
|
|
|
|
def test_env_var_overrides_allow_registrations(self, _env_typed_setting):
|
|
"""Env var override for allow_registrations should be reflected in config."""
|
|
_env_typed_setting["app.allow_registrations"] = False
|
|
result = load_server_config()
|
|
assert result["allow_registrations"] is False
|
|
|
|
def test_env_var_overrides_rate_limit_default(self, _env_typed_setting):
|
|
"""LDR_SECURITY_RATE_LIMIT_DEFAULT env var should override default."""
|
|
_env_typed_setting["security.rate_limit_default"] = "100 per hour"
|
|
result = load_server_config()
|
|
assert result["rate_limit_default"] == "100 per hour"
|
|
|
|
def test_env_var_overrides_rate_limit_login(self, _env_typed_setting):
|
|
"""LDR_SECURITY_RATE_LIMIT_LOGIN env var should override default."""
|
|
_env_typed_setting["security.rate_limit_login"] = "10 per minute"
|
|
result = load_server_config()
|
|
assert result["rate_limit_login"] == "10 per minute"
|
|
|
|
def test_env_var_overrides_rate_limit_registration(
|
|
self, _env_typed_setting
|
|
):
|
|
"""LDR_SECURITY_RATE_LIMIT_REGISTRATION env var should override default."""
|
|
_env_typed_setting["security.rate_limit_registration"] = "1 per hour"
|
|
result = load_server_config()
|
|
assert result["rate_limit_registration"] == "1 per hour"
|
|
|
|
def test_env_var_overrides_rate_limit_settings(self, _env_typed_setting):
|
|
"""LDR_SECURITY_RATE_LIMIT_SETTINGS env var should override default."""
|
|
_env_typed_setting["security.rate_limit_settings"] = "60 per minute"
|
|
result = load_server_config()
|
|
assert result["rate_limit_settings"] == "60 per minute"
|
|
|
|
def test_env_var_overrides_rate_limit_upload_user(self, _env_typed_setting):
|
|
"""LDR_SECURITY_RATE_LIMIT_UPLOAD_USER env var should override default."""
|
|
_env_typed_setting["security.rate_limit_upload_user"] = "200 per minute"
|
|
result = load_server_config()
|
|
assert result["rate_limit_upload_user"] == "200 per minute"
|
|
|
|
def test_env_var_overrides_rate_limit_upload_ip(self, _env_typed_setting):
|
|
"""LDR_SECURITY_RATE_LIMIT_UPLOAD_IP env var should override default."""
|
|
_env_typed_setting["security.rate_limit_upload_ip"] = "500 per minute"
|
|
result = load_server_config()
|
|
assert result["rate_limit_upload_ip"] == "500 per minute"
|
|
|
|
|
|
# ===================================================================
|
|
# LDR_APP_ALLOW_REGISTRATIONS fail-closed logic
|
|
# ===================================================================
|
|
class TestAllowRegistrationsFailClosed:
|
|
"""Tests for fail-closed behavior of LDR_APP_ALLOW_REGISTRATIONS."""
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
@pytest.mark.parametrize(
|
|
"value", ["true", "false", "1", "0", "yes", "no", "on", "off"]
|
|
)
|
|
def test_recognized_allow_registrations_values(self, value, monkeypatch):
|
|
"""Recognized boolean env-var values should NOT trigger fail-closed override."""
|
|
monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", value)
|
|
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
result = load_server_config()
|
|
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
assert not any("recognized" in call for call in warning_calls), (
|
|
f"Unexpected warning for recognized value '{value}': {warning_calls}"
|
|
)
|
|
# Verify the value was actually processed (not silently dropped)
|
|
assert result["allow_registrations"] is not None
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
@pytest.mark.parametrize(
|
|
"value", ["disabled", "nein", "flase", "MAYBE", "2", " "]
|
|
)
|
|
def test_unrecognized_allow_registrations_defaults_to_false(
|
|
self, value, monkeypatch
|
|
):
|
|
"""Unrecognized env-var value should force allow_registrations=False."""
|
|
monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", value)
|
|
|
|
result = load_server_config()
|
|
|
|
assert result["allow_registrations"] is False
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_unrecognized_allow_registrations_logs_warning(self, monkeypatch):
|
|
"""Should log a warning for unrecognized env-var value."""
|
|
monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", "nein")
|
|
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
load_server_config()
|
|
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
assert any(
|
|
"nein" in call and "recognized" in call
|
|
for call in warning_calls
|
|
), f"Expected warning about 'nein', got: {warning_calls}"
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_allow_registrations_env_not_set_uses_default(self, monkeypatch):
|
|
"""When env var is not set, allow_registrations should use default value."""
|
|
monkeypatch.delenv("LDR_APP_ALLOW_REGISTRATIONS", raising=False)
|
|
|
|
result = load_server_config()
|
|
|
|
# Default is True
|
|
assert result["allow_registrations"] is True
|
|
|
|
@pytest.mark.usefixtures("_default_typed_setting")
|
|
def test_empty_string_allow_registrations_fails_closed(self, monkeypatch):
|
|
"""Empty-string env var should trigger fail-closed (registrations=False)."""
|
|
monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", "")
|
|
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
result = load_server_config()
|
|
|
|
assert result["allow_registrations"] is False
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
assert any("recognized" in call for call in warning_calls), (
|
|
f"Expected fail-closed warning for empty string, got: {warning_calls}"
|
|
)
|
|
|
|
|
|
# ===================================================================
|
|
# Fixtures for legacy migration tests
|
|
# ===================================================================
|
|
@pytest.fixture(autouse=True)
|
|
def _mock_data_dir(tmp_path):
|
|
"""Point get_data_dir to a temp directory for all tests in this module."""
|
|
with patch(
|
|
"local_deep_research.web.server_config.get_data_dir",
|
|
return_value=str(tmp_path),
|
|
):
|
|
yield tmp_path
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_allow_registrations_env(monkeypatch):
|
|
"""Ensure LDR_APP_ALLOW_REGISTRATIONS is not set from the developer's shell.
|
|
|
|
The fail-closed logic reads this env var directly via os.getenv(),
|
|
bypassing mocks. If a developer has it set, tests could fail unexpectedly.
|
|
"""
|
|
monkeypatch.delenv("LDR_APP_ALLOW_REGISTRATIONS", raising=False)
|
|
|
|
|
|
@pytest.fixture()
|
|
def _passthrough_typed_setting():
|
|
"""Mock get_typed_setting_value: returns val if not None, else default.
|
|
|
|
This simulates the priority logic without env var checks, so we can
|
|
verify that legacy file values are passed through correctly.
|
|
"""
|
|
with patch(
|
|
"local_deep_research.web.server_config.get_typed_setting_value",
|
|
side_effect=lambda key, val, typ, default: (
|
|
val if val is not None else default
|
|
),
|
|
):
|
|
yield
|
|
|
|
|
|
# ===================================================================
|
|
# get_server_config_path
|
|
# ===================================================================
|
|
class TestGetServerConfigPath:
|
|
"""Tests for get_server_config_path function."""
|
|
|
|
def test_returns_path_object(self):
|
|
assert isinstance(get_server_config_path(), Path)
|
|
|
|
def test_filename_is_server_config_json(self):
|
|
assert get_server_config_path().name == "server_config.json"
|
|
|
|
def test_path_is_under_data_dir(self, _mock_data_dir):
|
|
assert get_server_config_path().parent == _mock_data_dir
|
|
|
|
|
|
# ===================================================================
|
|
# Legacy migration — _load_legacy_config via load_server_config
|
|
# ===================================================================
|
|
class TestLegacyMigration:
|
|
"""Tests for legacy server_config.json migration path."""
|
|
|
|
def _write_legacy(self, tmp_path, data):
|
|
"""Write a legacy server_config.json file."""
|
|
(tmp_path / "server_config.json").write_text(
|
|
json.dumps(data), encoding="utf-8"
|
|
)
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_legacy_allow_registrations_false_honored(self, _mock_data_dir):
|
|
"""Security-critical: allow_registrations=false from legacy file must be honored."""
|
|
self._write_legacy(_mock_data_dir, {"allow_registrations": False})
|
|
result = load_server_config()
|
|
assert result["allow_registrations"] is False
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_env_var_overrides_legacy_value(self, _mock_data_dir):
|
|
"""Env var should take priority over legacy file value.
|
|
|
|
We use _env_typed_setting-style mock to simulate env override.
|
|
"""
|
|
self._write_legacy(_mock_data_dir, {"port": 9999})
|
|
|
|
# Simulate env var override: get_typed_setting_value returns env value
|
|
def _side_effect(key, val, typ, default):
|
|
if key == "web.port":
|
|
return 8080 # env var wins
|
|
return val if val is not None else default
|
|
|
|
with patch(
|
|
"local_deep_research.web.server_config.get_typed_setting_value",
|
|
side_effect=_side_effect,
|
|
):
|
|
result = load_server_config()
|
|
|
|
assert result["port"] == 8080
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_no_legacy_file_returns_defaults(self):
|
|
"""Without a legacy file, defaults should be returned."""
|
|
result = load_server_config()
|
|
assert result["host"] == "0.0.0.0"
|
|
assert result["port"] == 5000
|
|
assert result["allow_registrations"] is True
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_corrupt_json_falls_back_to_defaults(self, _mock_data_dir):
|
|
"""Corrupt JSON should log a warning and return defaults."""
|
|
(_mock_data_dir / "server_config.json").write_text(
|
|
"not valid json{{{", encoding="utf-8"
|
|
)
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
result = load_server_config()
|
|
|
|
assert result["host"] == "0.0.0.0"
|
|
assert result["allow_registrations"] is True
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
assert any("Could not read" in call for call in warning_calls)
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_corrupt_encoding_falls_back_to_defaults(self, _mock_data_dir):
|
|
"""Invalid UTF-8 bytes should log a warning and return defaults."""
|
|
(_mock_data_dir / "server_config.json").write_bytes(
|
|
b"\x80\xfe\xff invalid utf-8"
|
|
)
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
result = load_server_config()
|
|
|
|
assert result["host"] == "0.0.0.0"
|
|
assert result["allow_registrations"] is True
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
assert any("Could not read" in call for call in warning_calls)
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_non_dict_json_falls_back_to_defaults(self, _mock_data_dir):
|
|
"""Non-dict JSON (e.g. array) should log a warning and return defaults."""
|
|
self._write_legacy(_mock_data_dir, [1, 2, 3])
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
result = load_server_config()
|
|
|
|
assert result["host"] == "0.0.0.0"
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
assert any("does not contain" in call for call in warning_calls)
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_partial_legacy_file(self, _mock_data_dir):
|
|
"""Partial legacy file: port from file, rest defaults."""
|
|
self._write_legacy(_mock_data_dir, {"port": 8080})
|
|
result = load_server_config()
|
|
assert result["port"] == 8080
|
|
assert result["host"] == "0.0.0.0"
|
|
assert result["allow_registrations"] is True
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_security_warning_logged_for_allow_registrations(
|
|
self, _mock_data_dir
|
|
):
|
|
"""Security warning with 'SECURITY' should be logged for allow_registrations."""
|
|
self._write_legacy(_mock_data_dir, {"allow_registrations": False})
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
load_server_config()
|
|
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
assert any("SECURITY" in call for call in warning_calls)
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_info_logged_for_non_security_settings(self, _mock_data_dir):
|
|
"""Non-security settings should log at info level, not warning."""
|
|
self._write_legacy(_mock_data_dir, {"host": "127.0.0.1"})
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
load_server_config()
|
|
|
|
info_calls = [str(c) for c in mock_logger.info.call_args_list]
|
|
assert any("host" in call for call in info_calls)
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_unrecognized_keys_logged_as_warning(self, _mock_data_dir):
|
|
"""Unrecognized keys in legacy file produce a warning listing them."""
|
|
self._write_legacy(
|
|
_mock_data_dir, {"unknown_key": "value", "another": 42}
|
|
)
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
result = load_server_config()
|
|
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
# Should warn about unrecognized keys
|
|
assert any("unrecognized" in call.lower() for call in warning_calls)
|
|
# No banner since no recognized keys
|
|
info_calls = [str(c) for c in mock_logger.info.call_args_list]
|
|
assert not any(
|
|
"server_config.json detected" in call for call in info_calls
|
|
)
|
|
# Defaults returned
|
|
assert result["host"] == "0.0.0.0"
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_empty_dict_no_warnings(self, _mock_data_dir):
|
|
"""Empty dict {} should produce no warnings and return defaults."""
|
|
self._write_legacy(_mock_data_dir, {})
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
result = load_server_config()
|
|
|
|
assert result["host"] == "0.0.0.0"
|
|
assert result["allow_registrations"] is True
|
|
info_calls = [str(c) for c in mock_logger.info.call_args_list]
|
|
assert not any(
|
|
"server_config.json detected" in call for call in info_calls
|
|
)
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_banner_logged_for_recognized_keys(self, _mock_data_dir):
|
|
"""Info banner should be logged when recognized keys are present."""
|
|
self._write_legacy(_mock_data_dir, {"port": 8080})
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
load_server_config()
|
|
info_calls = [str(c) for c in mock_logger.info.call_args_list]
|
|
assert any("server_config.json detected" in call for call in info_calls)
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_all_defaults_no_per_key_messages(self, _mock_data_dir):
|
|
"""Legacy file with all default values should not produce per-key messages."""
|
|
from local_deep_research.web.server_config import _DEFAULTS
|
|
|
|
self._write_legacy(_mock_data_dir, dict(_DEFAULTS))
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
load_server_config()
|
|
|
|
# Banner should still appear (file exists with recognized keys)
|
|
info_calls = [str(c) for c in mock_logger.info.call_args_list]
|
|
assert any("server_config.json detected" in call for call in info_calls)
|
|
|
|
# But no per-key info or warning messages about non-default values
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
assert not any("non-default" in call for call in info_calls)
|
|
assert not any("non-default" in call for call in warning_calls)
|
|
|
|
|
|
class TestLegacyAllowRegistrationsFailClosed:
|
|
"""Fail-closed for legacy JSON allow_registrations string values.
|
|
|
|
The guard fires when the legacy JSON has an unrecognized string for
|
|
allow_registrations and no env var overrides it.
|
|
"""
|
|
|
|
@staticmethod
|
|
def _write_legacy(tmp_path, data):
|
|
(tmp_path / "server_config.json").write_text(
|
|
json.dumps(data), encoding="utf-8"
|
|
)
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_unrecognized_string_defaults_to_false(self, _mock_data_dir):
|
|
"""Legacy JSON 'allow_registrations': 'disabled' should fail closed."""
|
|
self._write_legacy(_mock_data_dir, {"allow_registrations": "disabled"})
|
|
result = load_server_config()
|
|
assert result["allow_registrations"] is False
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
@pytest.mark.parametrize("value", ["true", "yes", "1", "on"])
|
|
def test_recognized_truthy_string_not_overridden(
|
|
self, value, _mock_data_dir
|
|
):
|
|
"""Truthy recognized strings should NOT be overridden by the guard."""
|
|
self._write_legacy(_mock_data_dir, {"allow_registrations": value})
|
|
result = load_server_config()
|
|
assert result["allow_registrations"] == value # passthrough, not False
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
@pytest.mark.parametrize("value", ["false", "no", "0", "off"])
|
|
def test_recognized_falsy_string_not_overridden(
|
|
self, value, _mock_data_dir
|
|
):
|
|
"""Falsy recognized strings — guard should not override."""
|
|
self._write_legacy(_mock_data_dir, {"allow_registrations": value})
|
|
result = load_server_config()
|
|
assert (
|
|
result["allow_registrations"] == value
|
|
) # passthrough, not bool False
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_native_bool_not_affected(self, _mock_data_dir):
|
|
"""Native JSON bool should not trigger the string guard."""
|
|
self._write_legacy(_mock_data_dir, {"allow_registrations": False})
|
|
result = load_server_config()
|
|
assert result["allow_registrations"] is False
|
|
|
|
@pytest.mark.usefixtures("_passthrough_typed_setting")
|
|
def test_env_var_guard_takes_priority_over_legacy_guard(
|
|
self, _mock_data_dir, monkeypatch
|
|
):
|
|
"""When both env var and legacy JSON have bad values, the env var guard fires."""
|
|
self._write_legacy(_mock_data_dir, {"allow_registrations": "disabled"})
|
|
monkeypatch.setenv("LDR_APP_ALLOW_REGISTRATIONS", "nein")
|
|
with patch(
|
|
"local_deep_research.web.server_config.logger"
|
|
) as mock_logger:
|
|
result = load_server_config()
|
|
assert result["allow_registrations"] is False
|
|
warning_calls = [str(c) for c in mock_logger.warning.call_args_list]
|
|
assert any(
|
|
"LDR_APP_ALLOW_REGISTRATIONS" in call for call in warning_calls
|
|
)
|
|
assert not any(
|
|
"Legacy server_config.json" in call for call in warning_calls
|
|
)
|
|
|
|
|
|
# ===================================================================
|
|
# has_legacy_customizations — direct tests using config_path parameter
|
|
# ===================================================================
|
|
class TestHasLegacyCustomizations:
|
|
"""Tests for has_legacy_customizations helper."""
|
|
|
|
def test_returns_false_when_no_file(self, tmp_path):
|
|
"""Non-existent path should return False."""
|
|
assert has_legacy_customizations(tmp_path / "nope.json") is False
|
|
|
|
def test_returns_true_when_non_default_value(self, tmp_path):
|
|
"""File with a non-default value should return True."""
|
|
cfg = tmp_path / "server_config.json"
|
|
cfg.write_text('{"port": 9999}', encoding="utf-8")
|
|
assert has_legacy_customizations(cfg) is True
|
|
|
|
def test_returns_false_when_all_defaults(self, tmp_path):
|
|
"""File with all default values should return False."""
|
|
from local_deep_research.web.server_config import _DEFAULTS
|
|
|
|
cfg = tmp_path / "server_config.json"
|
|
cfg.write_text(json.dumps(_DEFAULTS), encoding="utf-8")
|
|
assert has_legacy_customizations(cfg) is False
|
|
|
|
def test_returns_false_on_malformed_json(self, tmp_path):
|
|
"""Malformed JSON should return False, not raise."""
|
|
cfg = tmp_path / "server_config.json"
|
|
cfg.write_text("not valid json", encoding="utf-8")
|
|
assert has_legacy_customizations(cfg) is False
|
|
|
|
def test_returns_false_on_corrupt_encoding(self, tmp_path):
|
|
"""Invalid UTF-8 bytes should return False, not raise."""
|
|
cfg = tmp_path / "server_config.json"
|
|
cfg.write_bytes(b"\x80\xfe\xff invalid utf-8")
|
|
assert has_legacy_customizations(cfg) is False
|