"""配置加载测试:优先级链、占位值守卫、凭据只从文件读。 所有用例都指向 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}"