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)