feat(auth): separate read and write bilibili accounts
This commit is contained in:
1 parent
5c2ab7b187
commit
178ab64b18
17 files changed
+671
-707
No files matched your search
+61
-143
@@ -1,170 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""auth 相关纯函数自测(不联网): python tests/test_auth.py"""
|
||||
"""双账号 auth 纯函数与写回隔离测试(不联网): uv run python tests/test_auth.py"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
import favsort as f # noqa: E402
|
||||
|
||||
OLD = (
|
||||
"buvid3=OLD-B3; buvid4=OLD-B4; _uuid=OLD-UUID; buvid_fp=OLD-FP; "
|
||||
"b_nut=1700000000; SESSDATA=old%2C1700000000%2Cabc; bili_jct=oldcsrf; "
|
||||
"DedeUserID=28802351; DedeUserID__ckMd5=oldmd5; sid=oldsid; b_lsid=OLDL; "
|
||||
"bili_ticket=ticket; home_feed_column=5"
|
||||
from reaction_board.bilibili.auth import ( # noqa: E402
|
||||
cookie_value,
|
||||
cookies_from_set_cookie,
|
||||
merge_cookie,
|
||||
parse_cookie,
|
||||
sessdata_expiry,
|
||||
)
|
||||
from reaction_board.bilibili.auth_flow import AuthService, write_qr_png # noqa: E402
|
||||
|
||||
READ_OLD = "buvid3=READ-B3; {{SECRET_i5bghhpt}}readcsrf; DedeUserID=11; SESSDATA=old%2C1700000000%2Ca"
|
||||
WRITE_OLD = "buvid3=WRITE-B3; bili_jct=writecsrf; DedeUserID=22; SESSDATA=old%2C1700000000%2Cb"
|
||||
NEW = {"SESSDATA": "new%2C1800000000%2Cxyz", "bili_jct": "newcsrf", "DedeUserID": "99"}
|
||||
|
||||
|
||||
def test_parse_and_merge():
|
||||
pairs = f.parse_cookie(OLD)
|
||||
assert pairs[0] == ("buvid3", "OLD-B3")
|
||||
assert f.cookie_value(OLD, "SESSDATA") == "old%2C1700000000%2Cabc"
|
||||
|
||||
new = {
|
||||
"SESSDATA": "new%2C1800000000%2Cxyz",
|
||||
"bili_jct": "newcsrf",
|
||||
"DedeUserID": "28802351",
|
||||
"DedeUserID__ckMd5": "newmd5",
|
||||
"sid": "newsid",
|
||||
def raw_config():
|
||||
return {
|
||||
"bilibili": {
|
||||
"read": {"mid": 11, "csrf": "readcsrf", "cookie": READ_OLD},
|
||||
"write": {"mid": 22, "csrf": "writecsrf", "cookie": WRITE_OLD},
|
||||
},
|
||||
"folders": [],
|
||||
}
|
||||
merged = f.merge_cookie(OLD, new)
|
||||
# 身份字段被替换
|
||||
assert f.cookie_value(merged, "SESSDATA") == "new%2C1800000000%2Cxyz"
|
||||
assert f.cookie_value(merged, "bili_jct") == "newcsrf"
|
||||
assert f.cookie_value(merged, "sid") == "newsid"
|
||||
# 设备指纹原样保留
|
||||
assert f.cookie_value(merged, "buvid3") == "OLD-B3"
|
||||
assert f.cookie_value(merged, "buvid4") == "OLD-B4"
|
||||
assert f.cookie_value(merged, "_uuid") == "OLD-UUID"
|
||||
assert f.cookie_value(merged, "buvid_fp") == "OLD-FP"
|
||||
assert f.cookie_value(merged, "b_nut") == "1700000000"
|
||||
assert f.cookie_value(merged, "home_feed_column") == "5"
|
||||
# 旧会话字段在没有新值时应被丢弃
|
||||
assert f.cookie_value(merged, "b_lsid") == ""
|
||||
|
||||
|
||||
def test_set_cookie_and_url_fallback():
|
||||
headers = [
|
||||
"SESSDATA=abc%2C1800000000%2Cdef; Path=/; Domain=.bilibili.com; HttpOnly",
|
||||
"bili_jct=csrf2; Domain=.bilibili.com",
|
||||
"buvid3=SHOULD-NOT-OVERWRITE; Domain=.bilibili.com",
|
||||
]
|
||||
got = f.cookies_from_set_cookie(headers)
|
||||
assert got["SESSDATA"] == "abc%2C1800000000%2Cdef"
|
||||
assert got["bili_jct"] == "csrf2"
|
||||
|
||||
merged = f.merge_cookie(OLD, got)
|
||||
assert f.cookie_value(merged, "buvid3") == "OLD-B3" # 设备字段不被 Set-Cookie 覆盖
|
||||
def test_cookie_helpers():
|
||||
assert parse_cookie(READ_OLD)[0] == ("buvid3", "READ-B3")
|
||||
merged = merge_cookie(WRITE_OLD, NEW)
|
||||
assert cookie_value(merged, "SESSDATA") == "new%2C1800000000%2Cxyz"
|
||||
assert cookie_value(merged, "bili_jct") == "newcsrf"
|
||||
assert cookie_value(merged, "buvid3") == "WRITE-B3"
|
||||
assert sessdata_expiry(merged) == 1800000000
|
||||
headers = ["SESSDATA=x; Path=/", "bili_jct=y; Domain=.bilibili.com"]
|
||||
assert cookies_from_set_cookie(headers) == {"SESSDATA": "x", "bili_jct": "y"}
|
||||
|
||||
|
||||
def test_expiry():
|
||||
assert f.sessdata_expiry(OLD) == 1700000000
|
||||
assert f.sessdata_expiry("SESSDATA=bad") is None
|
||||
assert f.sessdata_expiry("") is None
|
||||
def test_finish_updates_only_selected_account_and_rolls_back():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
original = raw_config()
|
||||
path.write_text(json.dumps(original, ensure_ascii=False), encoding="utf-8")
|
||||
service = AuthService(path, account="write")
|
||||
|
||||
service._get_json = lambda credentials, url, auth: { # type: ignore[method-assign]
|
||||
"code": 0, "data": {"isLogin": True, "mid": 99, "uname": "writer"}
|
||||
}
|
||||
assert service.finish(service._load_raw(), NEW) == 0
|
||||
saved = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert saved["bilibili"]["read"] == original["bilibili"]["read"]
|
||||
assert saved["bilibili"]["write"]["mid"] == 99
|
||||
assert saved["bilibili"]["write"]["csrf"] == "newcsrf"
|
||||
assert cookie_value(saved["bilibili"]["write"]["cookie"], "buvid3") == "WRITE-B3"
|
||||
assert path.with_name("config.json.bak").exists()
|
||||
|
||||
def test_ensure_device_cookies():
|
||||
called = {"n": 0}
|
||||
|
||||
def fake_get_json(cfg, url, auth=False):
|
||||
called["n"] += 1
|
||||
return {"code": 0, "data": {"b_3": "NEW-B3", "b_4": "NEW-B4"}}
|
||||
|
||||
orig = f.get_json
|
||||
f.get_json = fake_get_json
|
||||
try:
|
||||
cfg = {"cookie": "SESSDATA=x; bili_jct=y"}
|
||||
added = f.ensure_device_cookies(cfg)
|
||||
assert "buvid3" in added and "buvid4" in added and "_uuid" in added
|
||||
assert f.cookie_value(cfg["cookie"], "buvid3") == "NEW-B3"
|
||||
assert f.cookie_value(cfg["cookie"], "buvid4") == "NEW-B4"
|
||||
assert f.cookie_value(cfg["cookie"], "_uuid").endswith("infoc")
|
||||
|
||||
# 已有设备字段时不应调用 spi,也不应改动
|
||||
called["n"] = 0
|
||||
cfg2 = {"cookie": OLD}
|
||||
added2 = f.ensure_device_cookies(cfg2)
|
||||
assert added2 == [] and called["n"] == 0
|
||||
assert cfg2["cookie"] == OLD
|
||||
finally:
|
||||
f.get_json = orig
|
||||
|
||||
|
||||
def test_finish_auth_write_and_rollback():
|
||||
import json
|
||||
import tempfile
|
||||
|
||||
new = {"SESSDATA": "new%2C1800000000%2Cxyz", "bili_jct": "newcsrf",
|
||||
"DedeUserID": "28802351", "DedeUserID__ckMd5": "newmd5"}
|
||||
cfg = {"mid": 28802351, "csrf": "oldcsrf", "cookie": OLD,
|
||||
"overrides": {}, "sleep": {}}
|
||||
old_raw = json.dumps(cfg, ensure_ascii=False, indent=2)
|
||||
|
||||
orig = (f.CONFIG_PATH, f.CONFIG_BACKUP, f.fetch_nav, f.ensure_device_cookies)
|
||||
|
||||
def fake_nav_ok(c):
|
||||
return {"code": 0, "data": {"isLogin": True, "uname": "tester", "mid": 28802351}}
|
||||
|
||||
def fake_nav_bad(c):
|
||||
return {"code": -101, "message": "账号未登录", "data": {"isLogin": False}}
|
||||
|
||||
def fake_device(c):
|
||||
return []
|
||||
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path = os.path.join(d, "config.json")
|
||||
f.CONFIG_PATH, f.CONFIG_BACKUP = path, path + ".bak"
|
||||
f.ensure_device_cookies = fake_device
|
||||
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(old_raw)
|
||||
|
||||
# 成功:写入新身份字段,设备字段不变,并生成备份
|
||||
f.fetch_nav = fake_nav_ok
|
||||
assert f.finish_auth(cfg, dict(new)) == 0
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
saved = json.load(fh)
|
||||
assert "newcsrf" == saved["csrf"]
|
||||
assert f.cookie_value(saved["cookie"], "buvid3") == "OLD-B3"
|
||||
assert f.cookie_value(saved["cookie"], "SESSDATA") == "new%2C1800000000%2Cxyz"
|
||||
assert os.path.exists(path + ".bak")
|
||||
|
||||
# 失败:用上一份备份回滚
|
||||
before = open(path, encoding="utf-8").read()
|
||||
cfg2 = dict(saved)
|
||||
f.fetch_nav = fake_nav_bad
|
||||
assert f.finish_auth(cfg2, {"SESSDATA": "x%2C1%2Cy", "bili_jct": "z"}) == 4
|
||||
after = open(path, encoding="utf-8").read()
|
||||
assert after == before
|
||||
finally:
|
||||
f.CONFIG_PATH, f.CONFIG_BACKUP, f.fetch_nav, f.ensure_device_cookies = orig
|
||||
before = path.read_text(encoding="utf-8")
|
||||
service._get_json = lambda credentials, url, auth: { # type: ignore[method-assign]
|
||||
"code": -101, "data": {"isLogin": False}
|
||||
}
|
||||
assert service.finish(service._load_raw(), NEW) == 4
|
||||
assert path.read_text(encoding="utf-8") == before
|
||||
|
||||
|
||||
def test_qr_png():
|
||||
import tempfile
|
||||
|
||||
path = os.path.join(tempfile.gettempdir(), "favsort-test-qr.png")
|
||||
try:
|
||||
f.write_qr_png("https://example.com/login?key=abc&from=test", path)
|
||||
with open(path, "rb") as fh:
|
||||
data = fh.read()
|
||||
assert data.startswith(b"\x89PNG\r\n\x1a\n")
|
||||
assert len(data) > 500
|
||||
finally:
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "qr.png"
|
||||
write_qr_png("https://example.com/login?key=abc", path)
|
||||
assert path.read_bytes().startswith(b"\x89PNG\r\n\x1a\n")
|
||||
|
||||
|
||||
def main():
|
||||
test_parse_and_merge()
|
||||
test_set_cookie_and_url_fallback()
|
||||
test_expiry()
|
||||
test_ensure_device_cookies()
|
||||
test_finish_auth_write_and_rollback()
|
||||
test_cookie_helpers()
|
||||
test_finish_updates_only_selected_account_and_rolls_back()
|
||||
test_qr_png()
|
||||
print("OK: auth 纯函数测试全部通过")
|
||||
print("OK: 双账号 auth Cookie 合并、写回隔离与回滚测试全部通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+44
-4
@@ -22,9 +22,10 @@ import reaction_board.cli as cli # noqa: E402
|
||||
def write_config(tmp, *, cookie="SESSDATA=x; buvid3=y", csrf="csrf"):
|
||||
path = Path(tmp) / "config.json"
|
||||
path.write_text(json.dumps({
|
||||
"mid": 1,
|
||||
"csrf": csrf,
|
||||
"cookie": cookie,
|
||||
"bilibili": {
|
||||
"read": {"mid": 1, "csrf": csrf, "cookie": cookie},
|
||||
"write": {"mid": 2, "csrf": "write-csrf", "cookie": "write-cookie"},
|
||||
},
|
||||
"storage": {
|
||||
"database_path": str(Path(tmp) / "app.db"),
|
||||
"backup_dir": str(Path(tmp) / "backup"),
|
||||
@@ -112,6 +113,37 @@ def test_auth_check_without_cookie():
|
||||
assert "未配置 cookie" in payload["message"]
|
||||
|
||||
|
||||
def test_auth_check_all_and_auth_dispatch():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = write_config(tmp, cookie="", csrf="")
|
||||
code, output = run_cli(["auth-check", "--account", "all", "--config", str(path)])
|
||||
assert code == 1
|
||||
payload = json.loads(output)
|
||||
assert payload["ok"] is False
|
||||
assert [item["account"] for item in payload["accounts"]] == ["read", "write"]
|
||||
|
||||
original = cli.AuthService
|
||||
calls = []
|
||||
|
||||
class FakeAuthService:
|
||||
def __init__(self, path, *, account, logger):
|
||||
calls.append((str(path), account))
|
||||
|
||||
def run(self, *, timeout, no_browser):
|
||||
assert timeout == 9 and no_browser is True
|
||||
return 7
|
||||
|
||||
cli.AuthService = FakeAuthService
|
||||
try:
|
||||
code, _ = run_cli([
|
||||
"auth", "--account", "write", "--timeout", "9", "--no-browser", "--config", str(path),
|
||||
])
|
||||
finally:
|
||||
cli.AuthService = original
|
||||
assert code == 7
|
||||
assert calls == [(str(path), "write")]
|
||||
|
||||
|
||||
def test_run_once_offline():
|
||||
class FakeClient:
|
||||
def check_login(self):
|
||||
@@ -125,7 +157,13 @@ def test_run_once_offline():
|
||||
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
original = cli._client
|
||||
cli._client = lambda *args, **kwargs: FakeClient()
|
||||
selected = []
|
||||
|
||||
def fake_client(credentials, *args, **kwargs):
|
||||
selected.append(credentials)
|
||||
return FakeClient()
|
||||
|
||||
cli._client = fake_client
|
||||
try:
|
||||
code, output = run_cli(["run", "--once", "--config", str(path)])
|
||||
finally:
|
||||
@@ -133,6 +171,7 @@ def test_run_once_offline():
|
||||
assert code == 0
|
||||
assert '"name": "folder_sync"' in output
|
||||
assert '"name": "snapshot"' in output
|
||||
assert selected and all(credentials.mid == 1 for credentials in selected)
|
||||
|
||||
|
||||
def main_test():
|
||||
@@ -141,6 +180,7 @@ def main_test():
|
||||
test_sync_requires_auth_without_network()
|
||||
test_snapshot_offline_empty_db()
|
||||
test_auth_check_without_cookie()
|
||||
test_auth_check_all_and_auth_dispatch()
|
||||
test_run_once_offline()
|
||||
print("OK: CLI init/sync/snapshot/auth-check/run/status/错误路径测试全部通过")
|
||||
|
||||
|
||||
+65
-140
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""配置加载 / 校验 / 热加载测试(不联网): uv run python tests/test_config.py"""
|
||||
"""双账号配置加载 / 校验 / 热加载测试(不联网): uv run python tests/test_config.py"""
|
||||
|
||||
import json
|
||||
import os
|
||||
@@ -13,24 +13,22 @@ sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
from reaction_board.config import ConfigError, ConfigStore, load_config # noqa: E402
|
||||
|
||||
COOKIE = "SESSDATA=secret%2C1800000000%2Cabc; bili_jct=csrf-secret; buvid3=dev"
|
||||
|
||||
|
||||
def write(path: Path, data) -> None:
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def bump_mtime(path: Path) -> None:
|
||||
"""保证 mtime 严格大于上一次,避免 Windows 时间精度导致热加载测试偶发失败。"""
|
||||
target = max(path.stat().st_mtime_ns, time.time_ns()) + 1_000_000
|
||||
os.utime(path, ns=(target, target))
|
||||
|
||||
|
||||
def base_legacy() -> dict:
|
||||
def base_config() -> dict:
|
||||
return {
|
||||
"mid": 28802351,
|
||||
"csrf": "csrf-secret",
|
||||
"cookie": COOKIE,
|
||||
"bilibili": {
|
||||
"read": {"mid": 11, "csrf": "read-csrf", "cookie": "read-cookie"},
|
||||
"write": {"mid": 22, "csrf": "write-csrf", "cookie": "write-cookie"},
|
||||
},
|
||||
"overrides": {"1": {"mid": 2, "name": "x"}},
|
||||
"sleep": {"min": 1},
|
||||
"folders": [
|
||||
@@ -42,168 +40,95 @@ def base_legacy() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def test_legacy_flat_mapping():
|
||||
def test_dual_accounts_and_env_override():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
write(path, base_legacy())
|
||||
cfg = load_config(path, env={})
|
||||
assert cfg.bilibili.mid == 28802351
|
||||
assert cfg.bilibili.csrf == "csrf-secret"
|
||||
assert cfg.bilibili.cookie == COOKIE
|
||||
assert cfg.storage.database_path == Path(tmp) / "data" / "app.db"
|
||||
assert cfg.storage.backup_dir == Path(tmp) / "data" / "backup"
|
||||
assert cfg.schedule.snapshot_at == "00:01"
|
||||
assert [f.media_id for f in cfg.folders] == [4451053851, 3700023351]
|
||||
assert cfg.track_fans == (37754047,)
|
||||
assert cfg.up_blacklist_seed == (1, 2) # 去重
|
||||
assert cfg.raw["overrides"] # 旧字段仍保留给 favsort
|
||||
assert set(cfg.heat_weights) == {
|
||||
"view", "like", "coin", "favorite", "share", "reply", "danmaku"
|
||||
}
|
||||
|
||||
|
||||
def test_nested_and_env_override():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
write(path, {
|
||||
"bilibili": {"mid": 1, "csrf": "old", "cookie": "old-cookie"},
|
||||
"storage": {"database_path": "var/app.db", "backup_dir": "var/bak"},
|
||||
"folders": [],
|
||||
})
|
||||
write(path, base_config())
|
||||
env = {
|
||||
"BILI_MID": "999",
|
||||
"BILI_CSRF": "env-csrf",
|
||||
"BILI_COOKIE": "env-cookie",
|
||||
"TG_BOT_TOKEN": "123456:abcdefghijklmnopqrstuvwxyzABCDE",
|
||||
"TG_CHAT_ID": "42",
|
||||
"DASHBOARD_AUTH_TOKEN": "web-token",
|
||||
"BILI_READ_MID": "101", "BILI_READ_CSRF": "env-read", "BILI_READ_COOKIE": "env-read-cookie",
|
||||
"BILI_WRITE_MID": "202", "BILI_WRITE_CSRF": "env-write", "BILI_WRITE_COOKIE": "env-write-cookie",
|
||||
"DATABASE_PATH": "custom.db",
|
||||
}
|
||||
cfg = load_config(path, env=env)
|
||||
assert cfg.bilibili.mid == 999
|
||||
assert cfg.bilibili.csrf == "env-csrf"
|
||||
assert cfg.bilibili.cookie == "env-cookie"
|
||||
assert cfg.telegram.chat_id == 42
|
||||
assert cfg.telegram.bot_token.startswith("123456:")
|
||||
assert cfg.web.auth_token == "web-token"
|
||||
assert (cfg.bilibili.read.mid, cfg.bilibili.read.csrf, cfg.bilibili.read.cookie) == (101, "env-read", "env-read-cookie")
|
||||
assert (cfg.bilibili.write.mid, cfg.bilibili.write.csrf, cfg.bilibili.write.cookie) == (202, "env-write", "env-write-cookie")
|
||||
assert cfg.storage.database_path == Path(tmp) / "custom.db"
|
||||
# 备份目录仍按配置文件目录解析
|
||||
assert cfg.storage.backup_dir == Path(tmp) / "var" / "bak"
|
||||
assert cfg.up_blacklist_seed == (1, 2)
|
||||
|
||||
|
||||
def test_absolute_paths_kept():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
db = str(Path(tmp) / "abs" / "app.db")
|
||||
path = Path(tmp) / "config.json"
|
||||
write(path, {
|
||||
"bilibili": {"mid": 1, "csrf": "c", "cookie": "k"},
|
||||
"storage": {"database_path": db, "backup_dir": str(Path(tmp) / "b")},
|
||||
"folders": [],
|
||||
})
|
||||
cfg = load_config(path, env={})
|
||||
assert cfg.storage.database_path == Path(db)
|
||||
|
||||
|
||||
def test_invalid_configs():
|
||||
def test_flat_and_missing_account_rejected():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
cases = [
|
||||
({"folders": [{"year": 2025, "media_id": 1},
|
||||
{"year": 2026, "media_id": 1}]}, "media_id 重复"),
|
||||
({"folders": [{"year": 2025, "media_id": 1},
|
||||
{"year": 2025, "media_id": 2}]}, "year 重复"),
|
||||
({"folders": [{"year": 2025, "media_id": 0}]}, "不能小于 1"),
|
||||
({"schedule": {"snapshot_at": "24:00"}}, "snapshot_at"),
|
||||
({"schedule": {"snapshot_at": "1:01"}}, "snapshot_at"),
|
||||
({"notify": {"max_items_per_message": 0}}, "max_items_per_message"),
|
||||
({"heat_weights": {"view": -1}}, "heat_weights.view"),
|
||||
({"classify": {"require_title": ""}}, "require_title"),
|
||||
]
|
||||
base = {
|
||||
"bilibili": {"mid": 1, "csrf": "c", "cookie": "k"},
|
||||
"folders": [],
|
||||
}
|
||||
for patch, needle in cases:
|
||||
data = json.loads(json.dumps(base))
|
||||
data.update(patch)
|
||||
write(path, data)
|
||||
try:
|
||||
load_config(path, env={})
|
||||
except ConfigError as exc:
|
||||
assert needle in str(exc), (needle, str(exc))
|
||||
else:
|
||||
raise AssertionError(f"应当失败: {patch}")
|
||||
write(path, {"mid": 1, "csrf": "x", "cookie": "y", "folders": []})
|
||||
try:
|
||||
load_config(path, env={})
|
||||
except ConfigError as exc:
|
||||
assert "bilibili.read" in str(exc)
|
||||
else:
|
||||
raise AssertionError("flat 格式必须拒绝")
|
||||
write(path, {"bilibili": {"read": {}}, "folders": []})
|
||||
try:
|
||||
load_config(path, env={})
|
||||
except ConfigError as exc:
|
||||
assert "bilibili.write" in str(exc)
|
||||
else:
|
||||
raise AssertionError("缺少 write 必须拒绝")
|
||||
|
||||
|
||||
def test_require_auth():
|
||||
def test_validation_require_read_auth_and_hot_reload():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
write(path, {"bilibili": {"mid": 1, "csrf": "", "cookie": ""}, "folders": []})
|
||||
data = base_config()
|
||||
data["bilibili"]["read"].update(cookie="", csrf="")
|
||||
write(path, data)
|
||||
load_config(path, env={}, require_auth=False)
|
||||
try:
|
||||
load_config(path, env={}, require_auth=True)
|
||||
except ConfigError as exc:
|
||||
assert "cookie" in str(exc)
|
||||
assert "bilibili.read.cookie" in str(exc)
|
||||
else:
|
||||
raise AssertionError("require_auth 应拒绝空凭据")
|
||||
raise AssertionError("read 空凭据必须拒绝")
|
||||
|
||||
|
||||
def test_env_int_invalid():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
write(path, base_legacy())
|
||||
try:
|
||||
load_config(path, env={"BILI_MID": "abc"})
|
||||
except ConfigError as exc:
|
||||
assert "BILI_MID" in str(exc)
|
||||
else:
|
||||
raise AssertionError("非法整数环境变量应失败")
|
||||
|
||||
|
||||
def test_hot_reload_keeps_last_good_config():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
write(path, base_legacy())
|
||||
write(path, base_config())
|
||||
store = ConfigStore(path, env={})
|
||||
assert store.get().folders[0].title == "2025"
|
||||
|
||||
data = base_legacy()
|
||||
data["folders"][0]["title"] = "2025-新"
|
||||
data = base_config()
|
||||
data["folders"][0]["title"] = "2025-new"
|
||||
write(path, data)
|
||||
bump_mtime(path)
|
||||
assert store.get().folders[0].title == "2025-新"
|
||||
assert store.last_error is None
|
||||
|
||||
path.write_text("{not json", encoding="utf-8")
|
||||
assert store.get().folders[0].title == "2025-new"
|
||||
path.write_text("{bad", encoding="utf-8")
|
||||
bump_mtime(path)
|
||||
assert store.get().folders[0].title == "2025-新"
|
||||
assert store.last_error is not None
|
||||
assert store.get().folders[0].title == "2025-new"
|
||||
assert store.last_error
|
||||
|
||||
|
||||
def test_invalid_values():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
data = base_config()
|
||||
data["bilibili"]["write"]["mid"] = "bad"
|
||||
write(path, data)
|
||||
bump_mtime(path)
|
||||
assert store.get().folders[0].title == "2025-新"
|
||||
assert store.last_error is None
|
||||
|
||||
|
||||
def test_missing_file():
|
||||
try:
|
||||
load_config(Path(tempfile.gettempdir()) / "no-such-bili-fav-config.json", env={})
|
||||
except ConfigError as exc:
|
||||
assert "缺少配置文件" in str(exc)
|
||||
else:
|
||||
raise AssertionError("缺失配置应失败")
|
||||
try:
|
||||
load_config(path, env={})
|
||||
except ConfigError as exc:
|
||||
assert "bilibili.write.mid" in str(exc)
|
||||
else:
|
||||
raise AssertionError("非法 mid 必须拒绝")
|
||||
write(path, base_config())
|
||||
try:
|
||||
load_config(path, env={"BILI_READ_MID": "bad"})
|
||||
except ConfigError as exc:
|
||||
assert "BILI_READ_MID" in str(exc)
|
||||
else:
|
||||
raise AssertionError("非法环境变量必须拒绝")
|
||||
|
||||
|
||||
def main():
|
||||
test_legacy_flat_mapping()
|
||||
test_nested_and_env_override()
|
||||
test_absolute_paths_kept()
|
||||
test_invalid_configs()
|
||||
test_require_auth()
|
||||
test_env_int_invalid()
|
||||
test_hot_reload_keeps_last_good_config()
|
||||
test_missing_file()
|
||||
print("OK: 配置加载、校验与热加载测试全部通过")
|
||||
test_dual_accounts_and_env_override()
|
||||
test_flat_and_missing_account_rejected()
|
||||
test_validation_require_read_auth_and_hot_reload()
|
||||
test_invalid_values()
|
||||
print("OK: 双账号配置加载、拒绝 flat 格式、ENV 覆盖与热加载测试全部通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""favsort 写账号配置隔离测试: uv run python tests/test_favsort_config.py"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
import favsort # noqa: E402
|
||||
|
||||
|
||||
def test_loads_write_account_only():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
path.write_text(json.dumps({
|
||||
"bilibili": {
|
||||
"read": {"mid": 11, "csrf": "read-csrf", "cookie": "read-cookie"},
|
||||
"write": {"mid": 22, "csrf": "write-csrf", "cookie": "write-cookie"},
|
||||
},
|
||||
"overrides": {"1": {"mid": 2, "name": "UP"}},
|
||||
}), encoding="utf-8")
|
||||
old = favsort.CONFIG_PATH
|
||||
favsort.CONFIG_PATH = str(path)
|
||||
try:
|
||||
cfg = favsort.load_config()
|
||||
finally:
|
||||
favsort.CONFIG_PATH = old
|
||||
assert cfg["mid"] == 22
|
||||
assert cfg["csrf"] == "write-csrf"
|
||||
assert cfg["cookie"] == "write-cookie"
|
||||
assert cfg["overrides"] == {"1": {"mid": 2, "name": "UP"}}
|
||||
|
||||
|
||||
def test_rejects_flat_config():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "config.json"
|
||||
path.write_text(json.dumps({"mid": 1, "csrf": "x", "cookie": "y"}), encoding="utf-8")
|
||||
old = favsort.CONFIG_PATH
|
||||
favsort.CONFIG_PATH = str(path)
|
||||
try:
|
||||
try:
|
||||
favsort.load_config()
|
||||
except SystemExit as exc:
|
||||
assert "bilibili.write" in str(exc)
|
||||
else:
|
||||
raise AssertionError("flat config 必须拒绝")
|
||||
finally:
|
||||
favsort.CONFIG_PATH = old
|
||||
|
||||
|
||||
def main():
|
||||
test_loads_write_account_only()
|
||||
test_rejects_flat_config()
|
||||
print("OK: favsort 仅使用 bilibili.write 配置测试全部通过")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -74,9 +74,10 @@ class FakeFavTransport:
|
||||
|
||||
def write_config(tmp, folders):
|
||||
data = {
|
||||
"mid": 28802351,
|
||||
"csrf": "csrf-secret",
|
||||
"cookie": "SESSDATA=secret; bili_jct=csrf-secret; buvid3=device",
|
||||
"bilibili": {
|
||||
"read": {"mid": 28802351, "csrf": "csrf-secret", "cookie": "SESSDATA=secret; bili_jct=csrf-secret; buvid3=device"},
|
||||
"write": {"mid": 2, "csrf": "write-csrf", "cookie": "write-cookie"},
|
||||
},
|
||||
"storage": {
|
||||
"database_path": str(Path(tmp) / "app.db"),
|
||||
"backup_dir": str(Path(tmp) / "backup"),
|
||||
|
||||
@@ -75,9 +75,10 @@ def make_service(tmp, transport, *, track_fans=(37754047,), logger=None,
|
||||
retries=3, cookie="SESSDATA=secret; buvid3=device"):
|
||||
path = Path(tmp) / "config.json"
|
||||
path.write_text(json.dumps({
|
||||
"mid": 28802351,
|
||||
"csrf": "csrf",
|
||||
"cookie": cookie,
|
||||
"bilibili": {
|
||||
"read": {"mid": 28802351, "csrf": "csrf", "cookie": cookie},
|
||||
"write": {"mid": 2, "csrf": "write-csrf", "cookie": "write-cookie"},
|
||||
},
|
||||
"storage": {
|
||||
"database_path": str(Path(tmp) / "app.db"),
|
||||
"backup_dir": str(Path(tmp) / "backup"),
|
||||
|
||||
Reference in new issue
Block a user