Files
RAGcore-Public/tests/unit/test_config.py
T

319 lines
12 KiB
Python

from pathlib import Path
from uuid import uuid4
import pytest
from pydantic import ValidationError
from ragcore.config import Settings
def test_log_level_is_normalized(tmp_path: Path) -> None:
settings = Settings(
log_level="warning",
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
)
assert settings.log_level == "WARNING"
def test_invalid_log_level_is_rejected() -> None:
with pytest.raises(ValidationError):
Settings(log_level="verbose")
def _production_settings(tmp_path: Path, **overrides: object) -> Settings:
values: dict[str, object] = {
"env": "production",
"secret_key": "s" * 40,
"api_token_pepper": "p" * 40,
"qdrant_api_key": "q" * 40,
"database_url": "postgresql+psycopg://app:s3cure-db-passw0rd-xyz@postgres:5432/ragcore",
"blob_root": tmp_path / "blobs",
"parsed_root": tmp_path / "parsed",
"temp_root": tmp_path / "tmp",
"model_state_root": tmp_path / "models",
"public_base_url": "https://ragcore.example.test",
"trusted_proxy_ips": "172.30.50.0/29",
}
values.update(overrides)
return Settings(**values)
def test_production_rejects_default_secret(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="secret_key"):
Settings(
env="production",
secret_key="development-only-change-me",
api_token_pepper="p" * 40,
qdrant_api_key="q" * 40,
database_url="postgresql+psycopg://app:password@postgres:5432/ragcore",
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
)
def test_production_rejects_short_secret(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="secret_key"):
_production_settings(tmp_path, secret_key="too-short")
def test_production_rejects_placeholder_qdrant_key(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="qdrant_api_key"):
_production_settings(tmp_path, qdrant_api_key="replace-with-at-least-32-random-characters")
def test_production_accepts_strong_secrets_and_absolute_storage(tmp_path: Path) -> None:
settings = _production_settings(tmp_path)
assert settings.env == "production"
assert settings.blob_root.is_absolute()
def test_trusted_proxy_ips_are_normalized_and_deduplicated() -> None:
settings = Settings(trusted_proxy_ips="172.30.50.2, 172.30.50.0/29,172.30.50.2")
assert settings.trusted_proxy_ips == ("172.30.50.2", "172.30.50.0/29")
@pytest.mark.parametrize("value", ["*", "0.0.0.0/0", "::/0", "proxy.local"])
def test_wildcard_or_non_ip_proxy_trust_is_rejected(value: str) -> None:
with pytest.raises(ValidationError, match="trusted_proxy_ips"):
Settings(trusted_proxy_ips=value)
def test_production_requires_an_explicit_trusted_proxy(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="trusted_proxy_ips"):
_production_settings(tmp_path, trusted_proxy_ips=())
def test_production_requires_an_https_public_url(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="public_base_url"):
_production_settings(tmp_path, public_base_url="http://127.0.0.1:1237")
@pytest.mark.parametrize(
("field_name", "value"),
[
("database_url", "sqlite:///ragcore.db"),
("qdrant_url", "postgresql://qdrant:6333"),
("ollama_url", "not-a-url"),
("public_base_url", "http://"),
],
)
def test_invalid_urls_are_rejected(field_name: str, value: str) -> None:
with pytest.raises(ValidationError):
Settings(**{field_name: value})
def test_storage_path_traversal_is_rejected() -> None:
with pytest.raises(ValidationError, match="storage paths"):
Settings(blob_root=Path("../outside"))
def test_production_rejects_user_share_storage(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="direct pool path"):
_production_settings(tmp_path, blob_root=Path("/mnt/pool/appdata/ragcore/blobs"))
@pytest.mark.parametrize("password", ["ragcore", "postgres", "password", "changeme", "RAGCORE"])
def test_production_rejects_default_or_weak_database_passwords(
tmp_path: Path, password: str
) -> None:
with pytest.raises(ValidationError, match="weak password"):
_production_settings(
tmp_path,
database_url=f"postgresql+psycopg://app:{password}@postgres:5432/ragcore",
)
def test_production_rejects_short_database_password(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="weak password"):
_production_settings(
tmp_path, database_url="postgresql+psycopg://app:short1@postgres:5432/ragcore"
)
def test_production_accepts_a_strong_database_password(tmp_path: Path) -> None:
settings = _production_settings(
tmp_path,
database_url="postgresql+psycopg://app:a-genuinely-long-random-password@postgres:5432/ragcore",
)
assert settings.env == "production"
def test_verify_runtime_directories_writable_passes_for_writable_dirs(tmp_path: Path) -> None:
settings = Settings(
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
model_state_root=tmp_path / "models",
)
settings.ensure_runtime_directories()
settings.verify_runtime_directories_writable()
def test_verify_runtime_directories_writable_raises_for_a_missing_directory(
tmp_path: Path,
) -> None:
settings = Settings(
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
model_state_root=tmp_path / "models",
)
with pytest.raises(OSError):
settings.verify_runtime_directories_writable()
def test_production_ignores_placeholder_oidc_secret_when_oidc_is_not_configured(
tmp_path: Path,
) -> None:
"""oidc_client_secret defaults to a placeholder; that's fine as long as
oidc_workspace_id is unset, since the /auth/* routes never activate."""
settings = _production_settings(tmp_path)
assert settings.oidc_workspace_id is None
assert settings.oidc_client_secret.get_secret_value() == "replace-me"
def test_production_rejects_placeholder_oidc_secret_once_oidc_is_configured(
tmp_path: Path,
) -> None:
with pytest.raises(ValidationError, match="oidc_client_secret"):
_production_settings(tmp_path, oidc_workspace_id=uuid4())
def test_production_accepts_a_strong_oidc_secret_once_configured(tmp_path: Path) -> None:
settings = _production_settings(
tmp_path, oidc_workspace_id=uuid4(), oidc_client_secret="o" * 40
)
assert settings.oidc_workspace_id is not None
def test_embedding_profiles_path_derives_from_model_state_root(tmp_path: Path) -> None:
settings = Settings(model_state_root=tmp_path / "models")
assert settings.embedding_profiles_path == tmp_path / "models" / "embedding_profiles.json"
def test_rc_embedding_bootstrap_requires_the_operator_command(tmp_path: Path) -> None:
rc_settings = _production_settings(tmp_path, env="rc")
assert rc_settings.automatic_embedding_bootstrap_enabled is False
assert Settings(env="development").automatic_embedding_bootstrap_enabled is True
assert _production_settings(tmp_path).automatic_embedding_bootstrap_enabled is True
def test_ensure_runtime_directories_creates_model_state_root(tmp_path: Path) -> None:
settings = Settings(
blob_root=tmp_path / "blobs",
parsed_root=tmp_path / "parsed",
temp_root=tmp_path / "tmp",
model_state_root=tmp_path / "models",
)
settings.ensure_runtime_directories()
assert settings.model_state_root.is_dir()
def test_model_state_root_traversal_is_rejected() -> None:
with pytest.raises(ValidationError, match="storage paths"):
Settings(model_state_root=Path("../outside"))
def test_production_rejects_user_share_model_state_root(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="direct pool path"):
_production_settings(tmp_path, model_state_root=Path("/mnt/pool/appdata/ragcore/models"))
def test_production_requires_absolute_model_state_root(tmp_path: Path) -> None:
with pytest.raises(ValidationError, match="must be absolute"):
_production_settings(tmp_path, model_state_root=Path("relative/models"))
def test_secret_repr_and_json_are_redacted() -> None:
secret = "actual-secret-value-that-must-not-leak"
settings = Settings(secret_key=secret, api_token_pepper="another-secret-value")
assert secret not in repr(settings)
assert secret not in settings.model_dump_json()
assert "**********" in settings.model_dump_json()
# -- the filesystem source allowlist ----------------------------------------
def test_source_roots_default_to_empty_so_no_folder_source_can_be_read() -> None:
"""Fail closed. There is no implicit default root: a deployment that never
named one reads no server-side paths at all."""
assert Settings().source_roots == ()
def test_source_roots_accept_the_comma_separated_environment_form() -> None:
settings = Settings(source_roots="/mnt/rag-sources, /srv/corpora")
assert settings.source_roots == (Path("/mnt/rag-sources"), Path("/srv/corpora"))
def test_source_roots_accept_a_list() -> None:
settings = Settings(source_roots=["/mnt/rag-sources"])
assert settings.source_roots == (Path("/mnt/rag-sources"),)
def test_empty_entries_are_ignored_rather_than_becoming_roots() -> None:
"""A trailing comma must not silently produce a root that means "here"."""
assert Settings(source_roots="/mnt/rag-sources,,").source_roots == (Path("/mnt/rag-sources"),)
assert Settings(source_roots="").source_roots == ()
def test_a_filesystem_root_is_rejected_at_startup() -> None:
"""Configuring "/" would make the allowlist meaningless, so it is refused
when the process boots rather than when a source is first read."""
with pytest.raises(ValidationError, match="must not be a filesystem root"):
Settings(source_roots="/")
def test_a_relative_source_root_is_rejected_at_startup() -> None:
with pytest.raises(ValidationError, match="absolute"):
Settings(source_roots="rag-sources")
def test_a_traversing_source_root_is_rejected_at_startup() -> None:
with pytest.raises(ValidationError, match="traverse"):
Settings(source_roots="/mnt/rag-sources/../..")
def test_duplicate_source_roots_collapse() -> None:
settings = Settings(source_roots="/mnt/rag-sources,/mnt/rag-sources")
assert settings.source_roots == (Path("/mnt/rag-sources"),)
def test_production_rejects_a_user_share_source_root(tmp_path: Path) -> None:
"""Same rule the storage paths follow: /mnt/user is Unraid's fuse share,
not a direct pool path."""
with pytest.raises(ValidationError, match="direct pool path"):
_production_settings(tmp_path, source_roots="/mnt/pool/rag-sources")
def test_production_accepts_a_direct_pool_source_root(tmp_path: Path) -> None:
settings = _production_settings(tmp_path, source_roots="/mnt/cache/rag-sources")
assert settings.source_roots == (Path("/mnt/cache/rag-sources"),)
def test_worker_concurrency_defaults_to_one_and_is_bounded() -> None:
"""One by default: a deployment that upgrades gets exactly the throughput
it had, until an operator asks for more."""
assert Settings(env="test").worker_concurrency == 1
assert Settings(env="test", worker_concurrency=8).worker_concurrency == 8
for rejected in (0, -1, 65):
with pytest.raises(ValidationError):
Settings(env="test", worker_concurrency=rejected)