"""Tests for auth credentials resolved from searxng.toml config file. Covers: resolve_auth_basic/resolve_auth_bearer with config_value parameter, priority chain (CLI > file > config > env), and main() integration that reads auth_basic/auth_bearer from load_config(). """ import os from unittest.mock import patch from pathlib import Path import pytest from common import resolve_auth_basic, resolve_auth_bearer from search import load_config # ----- resolve_auth_basic: config_value parameter ----- def test_basic_config_value_used_when_no_cli_or_file(): """config_value is returned when CLI and file are not provided.""" assert resolve_auth_basic(config_value="user:pass") == "user:pass" def test_basic_cli_overrides_config(): """CLI value takes precedence over config_value.""" assert resolve_auth_basic(cli_value="cli:pass", config_value="cfg:pass") == "cli:pass" def test_basic_file_overrides_config(tmp_path): """File takes precedence over config_value.""" auth_file = tmp_path / "auth.txt" auth_file.write_text("file:pass\n") assert resolve_auth_basic(file_path=str(auth_file), config_value="cfg:pass") == "file:pass" def test_config_overrides_env(monkeypatch): """config_value takes precedence over environment variable.""" monkeypatch.setenv("SEARXNG_BASIC_AUTH", "env:pass") assert resolve_auth_basic(config_value="cfg:pass") == "cfg:pass" def test_env_used_when_config_is_none(monkeypatch): """Environment variable is the fallback when config_value is None.""" monkeypatch.setenv("SEARXNG_BASIC_AUTH", "env:pass") assert resolve_auth_basic(config_value=None) == "env:pass" def test_config_none_and_no_env_returns_none(monkeypatch): """All sources absent → returns None.""" monkeypatch.delenv("SEARXNG_BASIC_AUTH", raising=False) assert resolve_auth_basic(config_value=None) is None def test_config_empty_string_falls_through_to_env(monkeypatch): """Empty string config_value is treated as absent (falsy).""" monkeypatch.setenv("SEARXNG_BASIC_AUTH", "env:pass") assert resolve_auth_basic(config_value="") == "env:pass" # ----- resolve_auth_bearer: config_value parameter ----- def test_bearer_config_value_used_when_no_cli_or_file(): """config_value is returned when CLI and file are not provided.""" assert resolve_auth_bearer(config_value="cfg-token-123") == "cfg-token-123" def test_bearer_cli_overrides_config(): """CLI value takes precedence over config_value.""" assert resolve_auth_bearer(cli_value="cli-token", config_value="cfg-token") == "cli-token" def test_bearer_file_overrides_config(tmp_path): """File takes precedence over config_value.""" token_file = tmp_path / "token.txt" token_file.write_text("file-token\n") assert resolve_auth_bearer(file_path=str(token_file), config_value="cfg-token") == "file-token" def test_bearer_config_overrides_env(monkeypatch): """config_value takes precedence over environment variable.""" monkeypatch.setenv("SEARXNG_BEARER_TOKEN", "env-token") assert resolve_auth_bearer(config_value="cfg-token") == "cfg-token" def test_bearer_env_used_when_config_is_none(monkeypatch): """Environment variable is the fallback when config_value is None.""" monkeypatch.setenv("SEARXNG_BEARER_TOKEN", "env-token") assert resolve_auth_bearer(config_value=None) == "env-token" def test_bearer_config_none_and_no_env_returns_none(monkeypatch): """All sources absent → returns None.""" monkeypatch.delenv("SEARXNG_BEARER_TOKEN", raising=False) assert resolve_auth_bearer(config_value=None) is None # ----- Full priority chain verification ----- def test_basic_full_priority_chain(tmp_path, monkeypatch): """Verify full priority: CLI > file > config > env.""" monkeypatch.setenv("SEARXNG_BASIC_AUTH", "env:pass") auth_file = tmp_path / "auth.txt" auth_file.write_text("file:pass\n") # CLI wins over all assert resolve_auth_basic(cli_value="cli:pass", file_path=str(auth_file), config_value="cfg:pass") == "cli:pass" # File wins over config and env assert resolve_auth_basic(file_path=str(auth_file), config_value="cfg:pass") == "file:pass" # Config wins over env assert resolve_auth_basic(config_value="cfg:pass") == "cfg:pass" # Env is fallback assert resolve_auth_basic() == "env:pass" def test_bearer_full_priority_chain(tmp_path, monkeypatch): """Verify full priority: CLI > file > config > env.""" monkeypatch.setenv("SEARXNG_BEARER_TOKEN", "env-token") token_file = tmp_path / "token.txt" token_file.write_text("file-token\n") assert resolve_auth_bearer(cli_value="cli-token", file_path=str(token_file), config_value="cfg-token") == "cli-token" assert resolve_auth_bearer(file_path=str(token_file), config_value="cfg-token") == "file-token" assert resolve_auth_bearer(config_value="cfg-token") == "cfg-token" assert resolve_auth_bearer() == "env-token" # ----- load_config: auth fields in toml ----- def test_load_config_reads_auth_basic(tmp_path): """load_config() returns auth_basic from searxng.toml.""" cfg = tmp_path / "searxng.toml" cfg.write_text(""" [searxng] instance = "https://example.com" auth_basic = "user:pass" """) config = load_config(str(cfg)) assert config.get("auth_basic") == "user:pass" def test_load_config_reads_auth_bearer(tmp_path): """load_config() returns auth_bearer from searxng.toml.""" cfg = tmp_path / "searxng.toml" cfg.write_text(""" [searxng] instance = "https://example.com" auth_bearer = "sk-token-123" """) config = load_config(str(cfg)) assert config.get("auth_bearer") == "sk-token-123" def test_load_config_reads_both_auth_fields(tmp_path): """Both auth_basic and auth_bearer can be set in the same config.""" cfg = tmp_path / "searxng.toml" cfg.write_text(""" [searxng] instance = "https://example.com" auth_basic = "user:pass" auth_bearer = "sk-token" """) config = load_config(str(cfg)) assert config.get("auth_basic") == "user:pass" assert config.get("auth_bearer") == "sk-token" def test_load_config_no_auth_fields(tmp_path): """Config without auth fields returns None for both.""" cfg = tmp_path / "searxng.toml" cfg.write_text(""" [searxng] instance = "https://example.com" """) config = load_config(str(cfg)) assert config.get("auth_basic") is None assert config.get("auth_bearer") is None # ----- main() integration: auth from config file ----- def test_main_uses_auth_from_config(tmp_path, monkeypatch, capsys): """main() reads auth_basic from searxng.toml and uses it for search.""" import search as search_mod import sys as _sys cfg = tmp_path / "searxng.toml" cfg.write_text(f""" [searxng] instance = "https://config-auth.example.com" auth_basic = "cfguser:cfgpass" """) captured_auth = {} def _fake_search_multi(urls, params, **kwargs): captured_auth["headers"] = kwargs.get("auth_headers", {}) return {"results": [{"title": "t", "url": "https://x.com"}]} monkeypatch.setattr(_sys, "argv", [ "search.py", "-q", "test", "--config", str(cfg), "--format", "json", ]) monkeypatch.setattr(search_mod, "search_multi", _fake_search_multi) # Ensure env vars don't interfere monkeypatch.delenv("SEARXNG_BASIC_AUTH", raising=False) monkeypatch.delenv("SEARXNG_BEARER_TOKEN", raising=False) # main() calls sys.exit(2) on empty results, sys.exit(0) on stream mode; # on normal success it returns without exit. We just want it to not raise. search_mod.main() # Verify auth headers were built from config value assert "Authorization" in captured_auth["headers"] assert "Basic" in captured_auth["headers"]["Authorization"] def test_main_cli_auth_overrides_config(tmp_path, monkeypatch, capsys): """CLI --auth-basic overrides auth_basic in searxng.toml.""" import search as search_mod import sys as _sys cfg = tmp_path / "searxng.toml" cfg.write_text(""" [searxng] instance = "https://config-auth.example.com" auth_basic = "cfguser:cfgpass" """) captured_auth = {} def _fake_search_multi(urls, params, **kwargs): captured_auth["headers"] = kwargs.get("auth_headers", {}) return {"results": [{"title": "t", "url": "https://x.com"}]} monkeypatch.setattr(_sys, "argv", [ "search.py", "-q", "test", "--config", str(cfg), "--auth-basic", "cliuser:clipass", "--format", "json", ]) monkeypatch.setattr(search_mod, "search_multi", _fake_search_multi) monkeypatch.delenv("SEARXNG_BASIC_AUTH", raising=False) search_mod.main() # CLI value should be used, not config value import base64 auth_header = captured_auth["headers"]["Authorization"] decoded = base64.b64decode(auth_header.split(" ")[1]).decode() assert decoded == "cliuser:clipass"