Implement P0 of the reaction dashboard: config loading with hot reload, SQLite migrations with backups, single-writer queue, read-only Bilibili fav client, folder import/diff sync, invalid-resource handling, blacklist seed, scheduler, and JSON redacted logging. Includes offline unit and transport-level contract tests; all existing and new tests pass.
111 lines
3.4 KiB
Python
111 lines
3.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""CLI 入口测试(离线): uv run python tests/test_cli.py"""
|
|
|
|
import contextlib
|
|
import io
|
|
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")
|
|
|
|
from reaction_board.cli import main # noqa: E402
|
|
from reaction_board.database import Database # noqa: E402
|
|
from reaction_board.repository import Repository # 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,
|
|
"storage": {
|
|
"database_path": str(Path(tmp) / "app.db"),
|
|
"backup_dir": str(Path(tmp) / "backup"),
|
|
},
|
|
"folders": [
|
|
{"year": 2025, "media_id": 11, "title": "2025"},
|
|
{"year": 2026, "media_id": 22, "title": "2026"},
|
|
],
|
|
"up_blacklist_seed": [101, 202],
|
|
}, ensure_ascii=False), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def run_cli(argv):
|
|
out = io.StringIO()
|
|
with contextlib.redirect_stdout(out):
|
|
code = main(argv)
|
|
return code, out.getvalue()
|
|
|
|
|
|
def test_init_and_status():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
path = write_config(tmp)
|
|
code, output = run_cli(["init", "--config", str(path)])
|
|
assert code == 0
|
|
assert '"schema_version": 1' in output
|
|
|
|
db = Database(Path(tmp) / "app.db")
|
|
db.initialize()
|
|
try:
|
|
repo = Repository(db)
|
|
assert len(repo.list_folders()) == 2
|
|
assert [r["mid"] for r in repo.blacklist()] == [101, 202]
|
|
assert repo.counts()["video_total"] == 0
|
|
finally:
|
|
db.close()
|
|
|
|
code, output = run_cli(["status", "--config", str(path)])
|
|
assert code == 0
|
|
assert '"configured_folders"' in output
|
|
assert '"media_count": null' in output
|
|
assert '"video_total": 0' in output
|
|
|
|
|
|
def test_missing_config():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
code, output = run_cli(["init", "--config", str(Path(tmp) / "nope.json")])
|
|
assert code == 2
|
|
payload = json.loads(output.strip().splitlines()[-1])
|
|
assert payload["level"] == "error"
|
|
assert "缺少配置文件" in payload["error"]
|
|
|
|
|
|
def test_sync_requires_auth_without_network():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
path = write_config(tmp, cookie="", csrf="")
|
|
code, output = run_cli(["sync", "--config", str(path)])
|
|
assert code == 2
|
|
payload = json.loads(output.strip().splitlines()[-1])
|
|
assert "cookie" in payload["error"]
|
|
assert not (Path(tmp) / "app.db").exists()
|
|
|
|
|
|
def test_run_once_without_folders():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
path = write_config(tmp)
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
data["folders"] = []
|
|
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
|
code, output = run_cli(["run", "--once", "--config", str(path)])
|
|
assert code == 0
|
|
assert '"name": "folder_sync"' in output
|
|
assert '"ok": true' in output
|
|
|
|
|
|
def main_test():
|
|
test_init_and_status()
|
|
test_missing_config()
|
|
test_sync_requires_auth_without_network()
|
|
test_run_once_without_folders()
|
|
print("OK: CLI init/sync/run/status/错误路径测试全部通过")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main_test()
|