64bbe53e71
文档一直写着「地址来自 scripts/data/config.json」,但 config.py 读 URL 只看环境变量与内置默认, 根本不读这个文件;本地那份还是改造前的旧文件,三个 URL 键是示例占位假值,还带着早已删除的 consumable_storage / attachment_dir。修法是让文档变成真的: - 优先级:环境变量 > config.json > src/config.py 内置默认;值统一 strip;坏 JSON 或缺文件不抛,退回默认并告警 - 含 your- 的占位值按未配置处理并 WARNING:优先级一改这些假地址就会立刻生效并把登录打挂,这条守卫 是本改动自身的安全带(老文件实测:三条 WARNING 后回落到内置默认,登录不受影响) - 凭据只从文件读(宿主清洗子进程的敏感环境变量,靠 env 传密码不可靠);新增 safe_config() 作为唯一 可进日志的视图,load_config() 含明文密码 - 清死代码:SAFE_CONFIG_KEYS、attachment_dir(Web 时代遗留,全项目零引用) - config.example.json 补五个可选 URL 键(留空=用默认);.env.example 的示例赋值全部注释掉—— env 优先级最高,整份复制出去会真拿 your-sso-url 去登录 - 本地 scripts/data/config.json 去掉 7 个陈旧键,只留凭据;改前备份为 config.json.bak-20261020 - 测试 tests/test_config.py 3 → 9 例:文件覆盖默认 / env 覆盖文件 / 占位忽略 / strip / 凭据不读 env / safe_config 无密码 / 坏文件 / 缺文件 / 返回副本 / 示例文件不得再含 your- 假值;全部指向 tmp 不碰真凭据 - 文档:操作指南写清三级优先级与占位语义,scripts/data/README 补字段表,AGENTS 增加配置优先级约束, 方案文档 §14 记录并划掉 §13.4 对应未完成项 验证:ruff / format --check / mypy src / deptry 全绿,pytest 141 passed,mcp_smoke 25 工具; load_config() 实测无 WARNING 且 URL 与凭据取值正确。--no-verify 提交并手跑等价检查。
120 lines
4.7 KiB
Python
120 lines
4.7 KiB
Python
"""配置加载测试:优先级链、占位值守卫、凭据只从文件读。
|
|
|
|
所有用例都指向 tmp 里的假配置文件,绝不碰 scripts/data/config.json(那是真凭据)。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from src import config as cfg
|
|
|
|
URL_KEYS = ("sso_login_url", "portal_url", "reimburse_url", "reimburse_page", "travel_page")
|
|
ENV_NAMES = ("SSO_LOGIN_URL", "PORTAL_URL", "REIMBURSE_URL", "REIMBURSE_PAGE", "TRAVEL_PAGE", "UPLOAD_PIN")
|
|
|
|
|
|
@pytest.fixture()
|
|
def cfg_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""把配置文件指到 tmp,清掉干扰断言的环境变量与缓存。"""
|
|
path = tmp_path / "config.json"
|
|
monkeypatch.setattr(cfg, "_CONFIG_PATH", path)
|
|
for name in ENV_NAMES:
|
|
monkeypatch.delenv(name, raising=False)
|
|
cfg.clear_config_cache()
|
|
yield path
|
|
cfg.clear_config_cache()
|
|
|
|
|
|
def write_cfg(path: Path, data: dict[str, str]) -> None:
|
|
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
|
cfg.clear_config_cache()
|
|
|
|
|
|
def test_file_wins_over_builtin_default(cfg_env: Path) -> None:
|
|
write_cfg(cfg_env, {"reimburse_url": "http://10.0.0.9:8081", "username": "u1"})
|
|
config = cfg.load_config()
|
|
assert config["reimburse_url"] == "http://10.0.0.9:8081"
|
|
assert config["username"] == "u1"
|
|
# 文件里没写的字段仍走内置默认,不会因为缺键就变空串
|
|
assert config["sso_login_url"] == cfg.URL_FIELDS["sso_login_url"][1]
|
|
|
|
|
|
def test_env_wins_over_file(cfg_env: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
write_cfg(cfg_env, {"portal_url": "https://file.example/oshall"})
|
|
monkeypatch.setenv("PORTAL_URL", "https://env.example/oshall")
|
|
cfg.clear_config_cache()
|
|
assert cfg.load_config()["portal_url"] == "https://env.example/oshall"
|
|
|
|
|
|
def test_placeholder_value_is_ignored(cfg_env: Path) -> None:
|
|
"""示例里抄来的 your-xxx 必须当成没配,否则老文件一升级就把登录打挂。"""
|
|
write_cfg(cfg_env, {"sso_login_url": "https://your-sso-url/login", "reimburse_url": ""})
|
|
config = cfg.load_config()
|
|
assert config["sso_login_url"] == cfg.URL_FIELDS["sso_login_url"][1]
|
|
assert config["reimburse_url"] == cfg.URL_FIELDS["reimburse_url"][1]
|
|
|
|
|
|
def test_values_are_trimmed(cfg_env: Path) -> None:
|
|
write_cfg(cfg_env, {"reimburse_page": " /expen/x?v=9 ", "username": " abc "})
|
|
config = cfg.load_config()
|
|
assert config["reimburse_page"] == "/expen/x?v=9"
|
|
assert config["username"] == "abc"
|
|
|
|
|
|
def test_credentials_not_read_from_env(cfg_env: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""凭据只认文件:宿主会清洗子进程环境变量,靠环境变量传密码不可靠。"""
|
|
write_cfg(cfg_env, {"username": "from-file", "password": "f"})
|
|
monkeypatch.setenv("username", "from-env")
|
|
monkeypatch.setenv("PASSWORD", "from-env")
|
|
cfg.clear_config_cache()
|
|
config = cfg.load_config()
|
|
assert config["username"] == "from-file"
|
|
assert config["password"] == "f"
|
|
|
|
|
|
def test_safe_config_hides_password(cfg_env: Path) -> None:
|
|
write_cfg(cfg_env, {"password": "secret-value"})
|
|
safe = cfg.safe_config()
|
|
assert "password" not in safe
|
|
assert "secret-value" not in json.dumps(safe, ensure_ascii=False)
|
|
|
|
|
|
def test_broken_config_file_falls_back(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
path = tmp_path / "config.json"
|
|
path.write_text("{坏文件", encoding="utf-8")
|
|
monkeypatch.setattr(cfg, "_CONFIG_PATH", path)
|
|
for name in ENV_NAMES:
|
|
monkeypatch.delenv(name, raising=False)
|
|
cfg.clear_config_cache()
|
|
config = cfg.load_config()
|
|
assert config["reimburse_url"] == cfg.URL_FIELDS["reimburse_url"][1]
|
|
assert config["username"] == ""
|
|
|
|
|
|
def test_missing_file_does_not_crash(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(cfg, "_CONFIG_PATH", tmp_path / "nope" / "config.json")
|
|
for name in ENV_NAMES:
|
|
monkeypatch.delenv(name, raising=False)
|
|
cfg.clear_config_cache()
|
|
config = cfg.load_config()
|
|
assert all(config[k] for k in URL_KEYS)
|
|
|
|
|
|
def test_load_config_returns_copy(cfg_env: Path) -> None:
|
|
write_cfg(cfg_env, {"username": "a"})
|
|
first = cfg.load_config()
|
|
first["username"] = "改了也不影响缓存"
|
|
assert cfg.load_config()["username"] == "a"
|
|
|
|
|
|
def test_example_has_no_placeholder_urls() -> None:
|
|
"""示例文件不能再埋 your-xxx:会被当成没配,却看着像配好了。"""
|
|
example = Path(__file__).resolve().parents[1] / "config.example.json"
|
|
data = json.loads(example.read_text(encoding="utf-8"))
|
|
for key in URL_KEYS:
|
|
value = str(data.get(key, ""))
|
|
assert "your-" not in value, f"config.example.json 的 {key} 又是占位假值: {value}"
|