Files
searxng-use-cli/tests/test_config_auth.py
thzxx dea899143d 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 同步更新认证配置说明与安全提醒
2026-08-01 18:01:32 +08:00

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"