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,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Metona Team
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -112,9 +112,9 @@ pip install requests beautifulsoup4
|
|||||||
|
|
||||||
### 配置实例
|
### 配置实例
|
||||||
|
|
||||||
AI Agent 无需每次传入实例 URL,支持三种配置方式(优先级从高到低):
|
AI Agent 无需每次传入实例 URL 和认证信息,支持通过配置文件一次性设置:
|
||||||
|
|
||||||
1. **CLI 参数**:`-i https://your-instance`
|
1. **CLI 参数**:`-i https://your-instance`(最高优先级)
|
||||||
2. **环境变量**:`export SEARXNG_INSTANCE="https://your-instance"`
|
2. **环境变量**:`export SEARXNG_INSTANCE="https://your-instance"`
|
||||||
3. **配置文件**:`./searxng.toml` 或 `~/.config/searxng-cli/searxng.toml`
|
3. **配置文件**:`./searxng.toml` 或 `~/.config/searxng-cli/searxng.toml`
|
||||||
|
|
||||||
@@ -122,8 +122,15 @@ AI Agent 无需每次传入实例 URL,支持三种配置方式(优先级从
|
|||||||
# searxng.toml
|
# searxng.toml
|
||||||
[searxng]
|
[searxng]
|
||||||
instance = "https://your-searxng.example.com"
|
instance = "https://your-searxng.example.com"
|
||||||
|
# 私有实例认证(可选,二选一):
|
||||||
|
auth_basic = "user:password" # Basic 认证
|
||||||
|
# auth_bearer = "sk-token-123" # Bearer Token(与 auth_basic 同时设置时 bearer 优先)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**认证优先级**(从高到低):`--auth-*` CLI 参数 > `--auth-*-file` 文件 > `searxng.toml` 配置 > 环境变量
|
||||||
|
|
||||||
|
> **安全提醒**:配置文件中的明文密码有泄露风险。生产环境推荐使用 `--auth-basic-file ~/.searxng_auth` 或环境变量。
|
||||||
|
|
||||||
## 参数参考
|
## 参数参考
|
||||||
|
|
||||||
### search.py — 网络搜索
|
### search.py — 网络搜索
|
||||||
@@ -234,7 +241,8 @@ python scripts/fetch.py -u https://example.com \
|
|||||||
|
|
||||||
**网络**
|
**网络**
|
||||||
- 代理支持(`--proxy`)
|
- 代理支持(`--proxy`)
|
||||||
- 认证(Bearer/Basic,支持文件和环境变量,避免 shell 历史泄露)
|
- 认证(Bearer/Basic,支持 CLI/文件/配置文件/环境变量四级优先级)
|
||||||
|
- `searxng.toml` 配置 `auth_basic` / `auth_bearer` 字段,AI Agent 一次配置即可
|
||||||
- 凭证文件权限警告(POSIX)
|
- 凭证文件权限警告(POSIX)
|
||||||
|
|
||||||
**工程**
|
**工程**
|
||||||
@@ -243,7 +251,7 @@ python scripts/fetch.py -u https://example.com \
|
|||||||
- 结构化错误码(`E_NETWORK` / `E_AUTH` / `E_RATE_LIMIT` 等)
|
- 结构化错误码(`E_NETWORK` / `E_AUTH` / `E_RATE_LIMIT` 等)
|
||||||
- JSON Lines 流式输出(`--stream`)
|
- JSON Lines 流式输出(`--stream`)
|
||||||
- 进度事件(`--progress`,JSON Lines 到 stderr)
|
- 进度事件(`--progress`,JSON Lines 到 stderr)
|
||||||
- 309 个单元+集成测试
|
- 330 个单元+集成测试
|
||||||
|
|
||||||
## 跨 Agent 兼容性
|
## 跨 Agent 兼容性
|
||||||
|
|
||||||
@@ -271,7 +279,7 @@ pip install pytest
|
|||||||
pytest -q
|
pytest -q
|
||||||
```
|
```
|
||||||
|
|
||||||
309 个测试覆盖:缓存操作、认证解析、域名过滤、Markdown 转换、搜索逻辑、集成流程、日志配置、HTML 回退、自动抓取、健康检查、输出格式化、实例解析、并行搜索、CLI 端到端、错误码分类、流式输出、进度事件。
|
330 个测试覆盖:缓存操作、认证解析、域名过滤、Markdown 转换、搜索逻辑、集成流程、日志配置、HTML 回退、自动抓取、健康检查、输出格式化、实例解析、并行搜索、CLI 端到端、错误码分类、流式输出、进度事件、配置文件认证。
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ SearXNG is a privacy-respecting metasearch engine that aggregates results from 7
|
|||||||
|
|
||||||
**Network & auth**
|
**Network & auth**
|
||||||
- Proxy support (`--proxy`) for both search and fetch (sets `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`)
|
- Proxy support (`--proxy`) for both search and fetch (sets `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`)
|
||||||
- Auth via CLI flag, file, or env var (`--auth-bearer` / `--auth-basic` + `*-file` variants) to avoid leaking secrets in shell history
|
- Auth via CLI flag, file, config file, or env var (`--auth-bearer` / `--auth-basic` + `*-file` variants) to avoid leaking secrets in shell history
|
||||||
|
- Config-file auth — `auth_basic` and `auth_bearer` fields in `searxng.toml` let AI agents set credentials once (priority: CLI > file > config > env)
|
||||||
- Credentials-file permission warning — `--auth-*-file` warns on stderr if the file is group/other-readable (POSIX only)
|
- Credentials-file permission warning — `--auth-*-file` warns on stderr if the file is group/other-readable (POSIX only)
|
||||||
|
|
||||||
**Engineering**
|
**Engineering**
|
||||||
@@ -101,10 +102,14 @@ python scripts/search.py -q "python asyncio tutorial" # -i not needed
|
|||||||
# [searxng]
|
# [searxng]
|
||||||
# instance = "https://my-searxng.example.com"
|
# instance = "https://my-searxng.example.com"
|
||||||
# # or: instances = ["https://a.example.com", "https://b.example.com"]
|
# # or: instances = ["https://a.example.com", "https://b.example.com"]
|
||||||
|
# # Auth (optional, for private instances):
|
||||||
|
# auth_basic = "user:password" # Basic auth
|
||||||
|
# auth_bearer = "sk-token-123" # Bearer token (auth_bearer wins if both set)
|
||||||
# # Any flag below can also be pre-set here (engines, categories, language,
|
# # Any flag below can also be pre-set here (engines, categories, language,
|
||||||
# # safesearch, time_range, method, format, timeout, max_retries, proxy,
|
# # safesearch, time_range, method, format, timeout, max_retries, proxy,
|
||||||
# # cache_ttl, fetch, fetch_timeout, fetch_retries, max_size).
|
# # cache_ttl, fetch, fetch_timeout, fetch_retries, max_size).
|
||||||
# # Explicit CLI flags always override config values.
|
# # Explicit CLI flags always override config values.
|
||||||
|
# # Auth priority: --auth-* > --auth-*-file > searxng.toml > env var
|
||||||
# Plain list also works in ./instances.txt (one URL per line, # for comments)
|
# Plain list also works in ./instances.txt (one URL per line, # for comments)
|
||||||
|
|
||||||
# 7. Cache results for 30 minutes (identical queries skip the network)
|
# 7. Cache results for 30 minutes (identical queries skip the network)
|
||||||
@@ -442,8 +447,8 @@ usage: search.py [-h] [--query QUERY] [--instance URL]
|
|||||||
10. **Result caching:** `--cache-ttl 30` stores results for 30 min; identical queries within the TTL skip the network entirely. Cache lives at `$SEARXNG_CACHE_DIR` or `~/.cache/searxng-cli/cache.db` (SQLite, WAL mode). `--clear-cache` / `--cache-stats` manage it without searching
|
10. **Result caching:** `--cache-ttl 30` stores results for 30 min; identical queries within the TTL skip the network entirely. Cache lives at `$SEARXNG_CACHE_DIR` or `~/.cache/searxng-cli/cache.db` (SQLite, WAL mode). `--clear-cache` / `--cache-stats` manage it without searching
|
||||||
11. **Batch mode:** `--queries-file FILE` reads one query per line (blank/`#` lines skipped) and runs them in sequence; output is a JSON array (or one block per query in brief/urls). A failed query is recorded but does not abort the batch
|
11. **Batch mode:** `--queries-file FILE` reads one query per line (blank/`#` lines skipped) and runs them in sequence; output is a JSON array (or one block per query in brief/urls). A failed query is recorded but does not abort the batch
|
||||||
12. **Dedup + sort + domain filter:** After search (and cache), duplicate URLs are collapsed (default; `--no-dedup` disables), results are sorted (`--sort-by`; default: score descending), and then `--include-domain`/`--exclude-domain` filter by domain. Matching is case-insensitive and ignores a leading `www.`; when a domain is in both lists, exclude wins
|
12. **Dedup + sort + domain filter:** After search (and cache), duplicate URLs are collapsed (default; `--no-dedup` disables), results are sorted (`--sort-by`; default: score descending), and then `--include-domain`/`--exclude-domain` filter by domain. Matching is case-insensitive and ignores a leading `www.`; when a domain is in both lists, exclude wins
|
||||||
13. **Proxy & auth:** `--proxy URL` routes both search and fetch through a proxy; `--auth-bearer` / `--auth-basic` (plus `*-file` variants and `SEARXNG_BEARER_TOKEN` / `SEARXNG_BASIC_AUTH` env vars) supply credentials without leaking them via shell history
|
13. **Proxy & auth:** `--proxy URL` routes both search and fetch through a proxy; `--auth-bearer` / `--auth-basic` (plus `*-file` variants, `searxng.toml` `auth_basic`/`auth_bearer` fields, and `SEARXNG_BEARER_TOKEN` / `SEARXNG_BASIC_AUTH` env vars) supply credentials. Priority: CLI flag > file > config file > env var
|
||||||
14. **Config defaults:** `searxng.toml` may pre-set most flags (engines, categories, language, safesearch, time_range, method, format, sort_by, timeout, max_retries, proxy, cache_ttl, fetch, fetch_timeout, fetch_retries, max_size); explicit CLI flags always win
|
14. **Config defaults:** `searxng.toml` may pre-set most flags (engines, categories, language, safesearch, time_range, method, format, sort_by, timeout, max_retries, proxy, cache_ttl, fetch, fetch_timeout, fetch_retries, max_size, auth_basic, auth_bearer); explicit CLI flags always win
|
||||||
15. **Structured errors:** in `--format json` mode, failures print a JSON object `{"error": "...", "exit_code": N, "query": "..."}` to stdout so agents can parse them
|
15. **Structured errors:** in `--format json` mode, failures print a JSON object `{"error": "...", "exit_code": N, "query": "..."}` to stdout so agents can parse them
|
||||||
|
|
||||||
**Key options:**
|
**Key options:**
|
||||||
|
|||||||
+20
-4
@@ -114,7 +114,8 @@ def _warn_file_perms(path: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def resolve_auth_basic(cli_value: str = None, file_path: str = None,
|
def resolve_auth_basic(cli_value: str = None, file_path: str = None,
|
||||||
env_var: str = "SEARXNG_BASIC_AUTH") -> str:
|
env_var: str = "SEARXNG_BASIC_AUTH",
|
||||||
|
config_value: str = None) -> str:
|
||||||
"""Resolve basic-auth credentials without leaking them via shell history.
|
"""Resolve basic-auth credentials without leaking them via shell history.
|
||||||
|
|
||||||
Priority (highest wins):
|
Priority (highest wins):
|
||||||
@@ -122,7 +123,9 @@ def resolve_auth_basic(cli_value: str = None, file_path: str = None,
|
|||||||
but leaks into shell history; discouraged)
|
but leaks into shell history; discouraged)
|
||||||
2. ``file_path`` — ``--auth-basic-file FILE``; first non-empty line
|
2. ``file_path`` — ``--auth-basic-file FILE``; first non-empty line
|
||||||
is read as ``user:pass``. Recommended for shells.
|
is read as ``user:pass``. Recommended for shells.
|
||||||
3. ``env_var`` — ``SEARXNG_BASIC_AUTH`` environment variable.
|
3. ``config_value`` — ``auth_basic`` field from ``searxng.toml``.
|
||||||
|
Convenient for AI agents that read config once.
|
||||||
|
4. ``env_var`` — ``SEARXNG_BASIC_AUTH`` environment variable.
|
||||||
|
|
||||||
Returns ``"user:pass"`` or ``None`` if no source provides credentials.
|
Returns ``"user:pass"`` or ``None`` if no source provides credentials.
|
||||||
Raises ``RuntimeError`` if a file is specified but cannot be read.
|
Raises ``RuntimeError`` if a file is specified but cannot be read.
|
||||||
@@ -143,16 +146,26 @@ def resolve_auth_basic(cli_value: str = None, file_path: str = None,
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise RuntimeError(f"cannot read auth file '{file_path}': {e}") from e
|
raise RuntimeError(f"cannot read auth file '{file_path}': {e}") from e
|
||||||
|
|
||||||
|
if config_value:
|
||||||
|
return config_value
|
||||||
|
|
||||||
import os
|
import os
|
||||||
return os.environ.get(env_var)
|
return os.environ.get(env_var)
|
||||||
|
|
||||||
|
|
||||||
def resolve_auth_bearer(cli_value: str = None, file_path: str = None,
|
def resolve_auth_bearer(cli_value: str = None, file_path: str = None,
|
||||||
env_var: str = "SEARXNG_BEARER_TOKEN") -> str:
|
env_var: str = "SEARXNG_BEARER_TOKEN",
|
||||||
"""Resolve a Bearer token from CLI flag, file, or environment variable.
|
config_value: str = None) -> str:
|
||||||
|
"""Resolve a Bearer token from CLI flag, file, config, or environment.
|
||||||
|
|
||||||
Mirrors :func:`resolve_auth_basic` for token-style auth. Useful for
|
Mirrors :func:`resolve_auth_basic` for token-style auth. Useful for
|
||||||
long-lived API tokens that should not appear in shell history.
|
long-lived API tokens that should not appear in shell history.
|
||||||
|
|
||||||
|
Priority (highest wins):
|
||||||
|
1. ``cli_value`` — explicit ``--auth-bearer "token"``
|
||||||
|
2. ``file_path`` — ``--auth-bearer-file FILE``
|
||||||
|
3. ``config_value`` — ``auth_bearer`` field from ``searxng.toml``
|
||||||
|
4. ``env_var`` — ``SEARXNG_BEARER_TOKEN`` environment variable
|
||||||
"""
|
"""
|
||||||
if cli_value:
|
if cli_value:
|
||||||
return cli_value
|
return cli_value
|
||||||
@@ -170,6 +183,9 @@ def resolve_auth_bearer(cli_value: str = None, file_path: str = None,
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise RuntimeError(f"cannot read token file '{file_path}': {e}") from e
|
raise RuntimeError(f"cannot read token file '{file_path}': {e}") from e
|
||||||
|
|
||||||
|
if config_value:
|
||||||
|
return config_value
|
||||||
|
|
||||||
import os
|
import os
|
||||||
return os.environ.get(env_var)
|
return os.environ.get(env_var)
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -1429,17 +1429,32 @@ Use --config FILE to load a non-default config file (overrides the auto-discover
|
|||||||
|
|
||||||
# Resolve instance(s): -i > SEARXNG_INSTANCE env > config file
|
# Resolve instance(s): -i > SEARXNG_INSTANCE env > config file
|
||||||
instance_urls = resolve_instances(args.instance)
|
instance_urls = resolve_instances(args.instance)
|
||||||
|
# Fallback: if --config was used, instance may be in the config dict
|
||||||
|
# but not in the default search paths that resolve_instances checks.
|
||||||
|
if not instance_urls and config.get("instance"):
|
||||||
|
instance_urls = parse_instances(config["instance"])
|
||||||
|
elif not instance_urls and config.get("instances"):
|
||||||
|
raw = config["instances"]
|
||||||
|
if isinstance(raw, str):
|
||||||
|
instance_urls = parse_instances(raw)
|
||||||
|
elif isinstance(raw, list):
|
||||||
|
instance_urls = [u if u.startswith(("http://", "https://"))
|
||||||
|
else "https://" + u for u in raw if u and str(u).strip()]
|
||||||
if not instance_urls:
|
if not instance_urls:
|
||||||
_emit_error("no SearXNG instance resolved. Provide -i/--instance, set the "
|
_emit_error("no SearXNG instance resolved. Provide -i/--instance, set the "
|
||||||
"SEARXNG_INSTANCE environment variable, or create a searxng.toml / "
|
"SEARXNG_INSTANCE environment variable, or create a searxng.toml / "
|
||||||
"instances.txt config file.", args, error_code=E_CONFIG)
|
"instances.txt config file.", args, error_code=E_CONFIG)
|
||||||
|
|
||||||
# Build auth headers if provided (needed by both verify and search).
|
# Build auth headers if provided (needed by both verify and search).
|
||||||
# Credentials may come from CLI flag, file, or env var (in priority order)
|
# Credentials may come from CLI flag, file, config file, or env var
|
||||||
# to avoid leaking secrets via shell history or `ps`.
|
# (in priority order) to avoid leaking secrets via shell history or `ps`.
|
||||||
try:
|
try:
|
||||||
bearer_token = resolve_auth_bearer(args.auth_bearer, args.auth_bearer_file)
|
bearer_token = resolve_auth_bearer(
|
||||||
basic_auth = resolve_auth_basic(args.auth_basic, args.auth_basic_file)
|
args.auth_bearer, args.auth_bearer_file,
|
||||||
|
config_value=config.get("auth_bearer"))
|
||||||
|
basic_auth = resolve_auth_basic(
|
||||||
|
args.auth_basic, args.auth_basic_file,
|
||||||
|
config_value=config.get("auth_basic"))
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
_emit_error(str(e), args, error_code=E_AUTH)
|
_emit_error(str(e), args, error_code=E_AUTH)
|
||||||
auth_headers = build_auth_headers(
|
auth_headers = build_auth_headers(
|
||||||
|
|||||||
@@ -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