- common.py: resolve_auth_basic/bearer 新增 config_value 参数,优先级 CLI > file > config > env - search.py: main() 从 load_config() 读取 auth_basic/auth_bearer;修复 --config 指定文件中 instance 字段不被解析的问题 - LICENSE: 补齐 MIT 协议文件 - tests: +21 测试覆盖配置文件认证优先级链与 main() 集成(309→330) - docs: SKILL.md/README.md 同步更新认证配置说明与安全提醒
258 lines
9.0 KiB
Python
258 lines
9.0 KiB
Python
"""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"
|