feat: searxng.toml 支持 auth_basic/auth_bearer 认证配置
- 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 同步更新认证配置说明与安全提醒
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user