fix(config): 让 config.json 的 URL 字段真正生效,并守住占位假值
文档一直写着「地址来自 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 提交并手跑等价检查。
This commit is contained in:
@@ -496,6 +496,29 @@ FakeBot 缺方法误判成产品故障。补 `_wait_any` / `_table_text` / `row_
|
||||
|
||||
1. `details` / `payment` / `attachments` 三步没联机验过(本轮只到 basic);travel 五步整条未联机
|
||||
2. P4 提交护栏未做;删除操作刻意不进工具层(破坏性动作交人工)
|
||||
3. `config.json` 里 `sso_login_url` / `portal_url` / `reimburse_url` 三个键被 `src/config.py` 忽略
|
||||
3. ~~`config.json` 里的 URL 三键被 `src/config.py` 忽略,示例里还留着占位假值~~ → 已修,见 §14
|
||||
(只读环境变量 + 内置默认),示例文件里留着占位假值会误导人:要么让它们生效,要么删掉键
|
||||
4. 经济科目固定取第 3 行、项目固定取第 1 行,仍无配置入口
|
||||
|
||||
## 14. 配置读取修复(2026-10-20)
|
||||
|
||||
P3 联机时顺带发现:文档写着「地址来自 `scripts/data/config.json`」,但 `src/config.py` 读 URL
|
||||
**只看环境变量和内置默认**,压根不读这个文件。本地那份还是改造前的旧文件,里面留着的
|
||||
`sso_login_url` 等三个键是示例占位假值(`https://your-sso-url/login`),另外还带着早已删除的
|
||||
`consumable_storage` 与 `attachment_dir`。也就是说:用户改文件不生效,改对了反而可能生效得晚。
|
||||
|
||||
修法是让文档变成真的,而不是把文档改回去:
|
||||
|
||||
- 优先级 **环境变量 > config.json > 内置默认**;值统一 `strip()`;坏 JSON / 文件缺失都不炸,退回默认并告警
|
||||
- 含 `your-` 的占位值一律当成没配,打 WARNING 说明去哪改。这条不是多余:优先级一改,
|
||||
老文件里的假地址就会**立刻生效**并把登录打挂,守卫是这次改动自身的安全带
|
||||
- 凭据只从文件读(宿主会清洗子进程环境变量,靠 env 传密码不可靠);新增 `safe_config()`
|
||||
作为唯一可进日志的视图,`load_config()` 里有明文密码
|
||||
- 顺手清掉死代码:`SAFE_CONFIG_KEYS`、`attachment_dir`(Web 时代遗留,全项目无引用)
|
||||
- `config.example.json` 补上五个可选 URL 键(留空 = 用内置默认);`.env.example` 里的示例赋值
|
||||
**全部注释掉**——环境变量优先级最高,整份复制出去就会把 `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 的假配置,不碰真凭据。`pytest 141 passed`。
|
||||
|
||||
+9
-6
@@ -1,14 +1,17 @@
|
||||
# 系统 URL 覆盖(可选)——不设则使用 src/config.py 里的内置默认值
|
||||
|
||||
注意:项目已移除 python-dotenv,不会再自动读取 .env 文件。
|
||||
取值优先级:环境变量 > scripts/data/config.json > src/config.py 内置默认。
|
||||
注意:项目已移除 python-dotenv,不会再自动读取 .env 文件(本文件只是变量名清单)。
|
||||
这些是真实的进程环境变量名:自己 export,或写进宿主的 MCP server env 配置。
|
||||
财务账号密码不放这里:DSH 会清洗 stdio 子进程的敏感环境变量,凭据只放 scripts/data/config.json。
|
||||
|
||||
SSO_LOGIN_URL=https://your-sso-url/login
|
||||
PORTAL_URL=https://your-portal-url/oshall
|
||||
REIMBURSE_URL=http://your-reimburse-host:8081
|
||||
REIMBURSE_PAGE=/expen/common/common?v=4.0
|
||||
TRAVEL_PAGE=/expen/travel/travel?v=4.0
|
||||
# 全部注释掉:整份复制出去也不会把坏地址塞进环境(环境变量优先级最高,写错就直接生效)。
|
||||
# 想用哪一行就去掉注释。左侧是变量名,右侧是内置默认值。
|
||||
# SSO_LOGIN_URL=https://tyrz.fynu.edu.cn/sso/login
|
||||
# PORTAL_URL=https://tyrz.fynu.edu.cn/oshall
|
||||
# REIMBURSE_URL=http://210.45.32.214:8081
|
||||
# REIMBURSE_PAGE=/expen/common/common?v=4.0
|
||||
# TRAVEL_PAGE=/expen/travel/travel?v=4.0
|
||||
|
||||
# 上传收件器访问码(也可用 --pin 参数或 config.json 的 upload_pin)
|
||||
UPLOAD_PIN=
|
||||
|
||||
@@ -111,6 +111,9 @@ last_reviewed: 2026-10-19
|
||||
等,别拍脑袋 `wait_for_timeout`。直达报销 URL 不建立会话,会被踢回「长时间未操作,请重新登录!」
|
||||
* **多进程共存**:宿主会保留旧代 stdio 子进程,subagent/teammate 也会各起一条 transport—— observed 4 个 PID 并存。
|
||||
因此任何"独占资源"(浏览器、Office)都要有磁盘锁,别假设只有一个服务器实例
|
||||
* **配置优先级**:环境变量 > `scripts/data/config.json` > `src/config.py` 内置默认;含 `your-` 的占位值
|
||||
按未配置处理并告警。改完配置要重连常驻 MCP 子进程才生效(进程内有缓存)。`safe_config()` 才是
|
||||
可以进日志/回显的那份,`load_config()` 里有明文密码
|
||||
* **敏感信息**:`scripts/data/config.json` 含登录凭据、`cases/` 含身份证号与卡号,两者都已在 `.gitignore`,
|
||||
绝不要提交,也不要回显密码
|
||||
* **发票类型区分**:差旅发票(高铁票/酒店住宿)走差旅报销流程,不生成申购单;普通发票生成低值易耗品申购单
|
||||
|
||||
+6
-1
@@ -4,5 +4,10 @@
|
||||
"default_name": "默认报销人姓名",
|
||||
"default_card_no": "默认公务卡号",
|
||||
"default_person_id": "默认人员编号",
|
||||
"upload_pin": "手机上传页访问码,可留空"
|
||||
"upload_pin": "手机上传页访问码,可留空",
|
||||
"sso_login_url": "",
|
||||
"portal_url": "",
|
||||
"reimburse_url": "",
|
||||
"reimburse_page": "",
|
||||
"travel_page": ""
|
||||
}
|
||||
|
||||
+6
-3
@@ -24,8 +24,10 @@ last_reviewed: 2026-10-19
|
||||
| 日常报销录入 | `/expen/common/common?v=4.0` | 普通报销目标页 |
|
||||
| 差旅报销 | `/expen/travel/travel?v=4.0` | 差旅报销目标页 |
|
||||
|
||||
地址来自 `scripts/data/config.json`,也可用环境变量覆盖(`SSO_LOGIN_URL` 等,见 `.env.example`;
|
||||
项目**不会**自动读取 `.env` 文件,需要自己导出或写进宿主的 env 配置)。
|
||||
取值优先级:**环境变量 > `scripts/data/config.json` > `src/config.py` 内置默认**。
|
||||
环境变量名是 `SSO_LOGIN_URL` / `PORTAL_URL` / `REIMBURSE_URL` / `REIMBURSE_PAGE` / `TRAVEL_PAGE`
|
||||
(见 `.env.example`;项目**不会**自动读 `.env` 文件,要自己 export 或写进宿主的 MCP env)。
|
||||
配置文件里这几项**可以不写**;写了占位假值(含 `your-`)会被当成没配并打 WARNING,不会拿去登录。
|
||||
|
||||
---
|
||||
|
||||
@@ -243,7 +245,8 @@ SSO 需要扫码或验证码时人工完成即可,流程会继续。每次运
|
||||
| `default_card_no` | 支付方式的「公务卡号」 |
|
||||
| `upload_pin` | 上传收件器访问码(等价 `--pin` 或环境变量 `UPLOAD_PIN`) |
|
||||
|
||||
URL 类字段(`sso_login_url`、`portal_url`、`reimburse_url`、`reimburse_page`、`travel_page`)有内置默认值,需要才配。
|
||||
URL 类字段(`sso_login_url`、`portal_url`、`reimburse_url`、`reimburse_page`、`travel_page`)有内置默认值,
|
||||
需要才配;配了就生效(早先版本这里写了也不读,已修)。想临时改地址用环境变量更方便,不用动文件。
|
||||
凭据不放环境变量:DSH 会清洗 stdio 子进程里含 KEY/PASSWORD/SECRET/TOKEN 的变量。
|
||||
|
||||
校验规则在 `config/validation_rules.json`;改规则要同步跑测试(骨架与规则有一致性用例)。
|
||||
|
||||
+88
-39
@@ -1,66 +1,115 @@
|
||||
"""
|
||||
配置加载
|
||||
"""配置加载
|
||||
|
||||
从 scripts/data/config.json 读取用户配置,环境变量可覆盖系统 URL 与用户字段。
|
||||
优先级:环境变量 > scripts/data/config.json > 内置默认值。
|
||||
|
||||
两条历史坑,别再犯:
|
||||
|
||||
1. 早期版本 URL 只认环境变量与内置默认,**根本不读 config.json**,而文档写着「地址来自
|
||||
config.json」——用户改文件不生效,还留着示例里的占位假值看着像配好了。现在按优先级链真读。
|
||||
2. 占位值(`https://your-sso-url/login` 这类从示例抄来的)必须当成「没配」,否则老文件一升级
|
||||
就变成生效的坏地址,登录直接挂。这里显式识别并告警,比让它悄悄生效好。
|
||||
|
||||
凭据(username/password/…)只从文件读,不走环境变量:DSH 会清洗 stdio 子进程里名字含
|
||||
KEY/PASSWORD/SECRET/TOKEN 的变量,指望环境变量传密码是不可靠的。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from . import get_logger
|
||||
|
||||
log = get_logger("config")
|
||||
|
||||
# 项目根 = src/config.py 的上上级(曾多写一层 parent,导致解析到项目之外、凭据恒为空)
|
||||
_CONFIG_PATH = Path(__file__).resolve().parent.parent / "scripts" / "data" / "config.json"
|
||||
|
||||
# 会话级配置允许覆盖的用户相关字段白名单(含密码)
|
||||
# URL 类字段:环境变量名 -> 内置默认值。键顺序即 config.json 里推荐的书写顺序
|
||||
URL_FIELDS: dict[str, tuple[str, str]] = {
|
||||
"sso_login_url": ("SSO_LOGIN_URL", "https://tyrz.fynu.edu.cn/sso/login"),
|
||||
"portal_url": ("PORTAL_URL", "https://tyrz.fynu.edu.cn/oshall"),
|
||||
"reimburse_url": ("REIMBURSE_URL", "http://210.45.32.214:8081"),
|
||||
"reimburse_page": ("REIMBURSE_PAGE", "/expen/common/common?v=4.0"),
|
||||
"travel_page": ("TRAVEL_PAGE", "/expen/travel/travel?v=4.0"),
|
||||
}
|
||||
|
||||
# 前端安全的配置字段白名单(不含密码)
|
||||
SAFE_CONFIG_KEYS = frozenset(
|
||||
{
|
||||
"username",
|
||||
"default_name",
|
||||
"default_card_no",
|
||||
"default_person_id",
|
||||
}
|
||||
)
|
||||
# 用户相关字段只从文件读
|
||||
USER_FIELDS = ("username", "password", "default_name", "default_card_no", "default_person_id")
|
||||
|
||||
# 模块级配置缓存
|
||||
_config_cache: dict[str, str | Path] | None = None
|
||||
# 环境变量名 -> 配置键(除 URL 外还有这个)
|
||||
_ENV_ONLY = {"UPLOAD_PIN": "upload_pin"}
|
||||
|
||||
|
||||
def _read_config() -> dict[str, str | Path]:
|
||||
"""读取并合并配置,缺失字段使用默认值"""
|
||||
raw = {}
|
||||
def _is_placeholder(value: str) -> bool:
|
||||
"""空值或示例里的占位值都算「没配」。"""
|
||||
if not value:
|
||||
return True
|
||||
low = value.lower()
|
||||
return "your-" in low or low.startswith("http://your") or "填这里" in value or value.startswith("你的")
|
||||
|
||||
|
||||
def _resolve(key: str, raw_value: object) -> str:
|
||||
"""单个 URL 字段按 环境变量 > 文件 > 默认 取值,占位值跳过并告警。"""
|
||||
env_name, default = URL_FIELDS[key]
|
||||
env_value = os.environ.get(env_name, "").strip()
|
||||
if env_value:
|
||||
return env_value
|
||||
file_value = str(raw_value).strip() if raw_value is not None else ""
|
||||
if _is_placeholder(file_value):
|
||||
if file_value:
|
||||
log.warning(
|
||||
"config.json 的 %s 是占位值 %r,已忽略并用内置默认;要改就在这一行填真实地址,或设环境变量 %s",
|
||||
key,
|
||||
file_value,
|
||||
env_name,
|
||||
)
|
||||
return default
|
||||
return file_value
|
||||
|
||||
|
||||
_config_cache: dict[str, str] | None = None
|
||||
|
||||
|
||||
def _read_config() -> dict[str, str]:
|
||||
raw: dict[str, object] = {}
|
||||
if _CONFIG_PATH.exists():
|
||||
with open(_CONFIG_PATH, encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
try:
|
||||
loaded = json.loads(_CONFIG_PATH.read_text(encoding="utf-8"))
|
||||
except Exception as exc:
|
||||
log.error("读取 %s 失败,全部退回默认值与环境变量: %s", _CONFIG_PATH, exc)
|
||||
loaded = {}
|
||||
if isinstance(loaded, dict):
|
||||
raw = loaded
|
||||
else:
|
||||
log.error("%s 顶层不是对象,忽略整个文件", _CONFIG_PATH)
|
||||
elif not _CONFIG_PATH.exists():
|
||||
log.warning("配置文件不存在: %s(凭据将为空,填报与匹配会失败)", _CONFIG_PATH)
|
||||
|
||||
project_root = _CONFIG_PATH.parent
|
||||
|
||||
return {
|
||||
"sso_login_url": os.environ.get("SSO_LOGIN_URL", "https://tyrz.fynu.edu.cn/sso/login"),
|
||||
"portal_url": os.environ.get("PORTAL_URL", "https://tyrz.fynu.edu.cn/oshall"),
|
||||
"reimburse_url": os.environ.get("REIMBURSE_URL", "http://210.45.32.214:8081"),
|
||||
"reimburse_page": os.environ.get("REIMBURSE_PAGE", "/expen/common/common?v=4.0"),
|
||||
"travel_page": os.environ.get("TRAVEL_PAGE", "/expen/travel/travel?v=4.0"),
|
||||
"username": raw.get("username", ""),
|
||||
"password": raw.get("password", ""),
|
||||
"default_name": raw.get("default_name", ""),
|
||||
"default_card_no": raw.get("default_card_no", ""),
|
||||
"default_person_id": raw.get("default_person_id", ""),
|
||||
"upload_pin": os.environ.get("UPLOAD_PIN") or raw.get("upload_pin", ""),
|
||||
"attachment_dir": project_root / "attachments",
|
||||
}
|
||||
config: dict[str, str] = {key: _resolve(key, raw.get(key)) for key in URL_FIELDS}
|
||||
for key in USER_FIELDS:
|
||||
value = raw.get(key, "")
|
||||
config[key] = str(value).strip() if value is not None else ""
|
||||
for env_name, key in _ENV_ONLY.items():
|
||||
from_env = os.environ.get(env_name, "").strip()
|
||||
from_file = str(raw.get(key, "") or "").strip()
|
||||
config[key] = from_env or from_file
|
||||
return config
|
||||
|
||||
|
||||
def load_config() -> dict[str, str | Path]:
|
||||
"""加载并合并配置,使用模块级缓存避免重复读取文件"""
|
||||
def load_config() -> dict[str, str]:
|
||||
"""加载配置(模块级缓存);返回副本,改返回值不会影响缓存。"""
|
||||
global _config_cache
|
||||
if _config_cache is None:
|
||||
_config_cache = _read_config()
|
||||
return dict(_config_cache)
|
||||
|
||||
|
||||
def safe_config() -> dict[str, str]:
|
||||
"""去掉密码的配置,日志与对外展示只能用它。"""
|
||||
return {k: v for k, v in load_config().items() if k != "password"}
|
||||
|
||||
|
||||
def clear_config_cache() -> None:
|
||||
"""清除配置缓存(测试或配置变更时调用)"""
|
||||
"""清缓存(测试或改了配置文件后调用;常驻的 MCP 子进程要靠重连才生效)。"""
|
||||
global _config_cache
|
||||
_config_cache = None
|
||||
|
||||
+113
-13
@@ -1,19 +1,119 @@
|
||||
"""配置模块单元测试"""
|
||||
"""配置加载测试:优先级链、占位值守卫、凭据只从文件读。
|
||||
|
||||
from src.config import load_config
|
||||
所有用例都指向 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")
|
||||
|
||||
|
||||
class TestLoadConfig:
|
||||
"""配置加载"""
|
||||
@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 test_returns_dict(self) -> None:
|
||||
config = load_config()
|
||||
assert isinstance(config, dict)
|
||||
|
||||
def test_has_username(self) -> None:
|
||||
config = load_config()
|
||||
assert "username" in config
|
||||
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_has_password(self) -> None:
|
||||
config = load_config()
|
||||
assert "password" in config
|
||||
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user