Files
re-xxm/tests/test_repository.py
T
kairee e2cdf997af feat(p0): add favorite folder sync and sqlite data foundation
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.
2026-10-31 18:08:33 +08:00

244 lines
9.1 KiB
Python

# -*- coding: utf-8 -*-
"""收藏夹/视频状态机测试(不联网): uv run python tests/test_repository.py"""
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.bilibili.models import FavResource # noqa: E402
from reaction_board.config import FolderConfig # noqa: E402
from reaction_board.database import Database # noqa: E402
from reaction_board.repository import Repository # noqa: E402
F1 = FolderConfig(year=2025, media_id=4451053851, title="2025")
F2 = FolderConfig(year=2026, media_id=3700023351, title="2026")
def resource(aid, *, attr=0, up_mid=100, title="视频", bvid=None):
return FavResource(
aid=aid,
bvid=bvid or f"BV{aid}",
title=title,
up_mid=up_mid,
up_name="UP",
pubdate=1_700_000_000,
duration=600,
cover="http://example/cover.jpg",
attr=attr,
fav_time=None,
)
def setup(tmp):
db = Database(Path(tmp) / "app.db", backup_dir=Path(tmp) / "backup")
db.initialize()
repo = Repository(db, clock=lambda: 1_800_000_000)
repo.ensure_folder(F1)
repo.ensure_folder(F2)
return db, repo
def test_upsert_and_folder_resource():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
outcome = repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
assert outcome.status == "active"
assert outcome.previous_folder_id is None
video = repo.get_video(1)
assert video["bvid"] == "BV1"
assert video["folder_id"] == F1.media_id
assert video["source"] == f"fav:{F1.media_id}"
assert video["first_seen"] == 1_800_000_000
assert repo.folder_active_aids(F1.media_id) == {1}
# 幂等:重复导入不新增行、不改 first_seen
repo.upsert_from_fav(resource(1, title="新标题"), F1.media_id, was_added=False)
assert len(repo.list_videos()) == 1
video = repo.get_video(1)
assert video["title"] == "新标题"
assert video["first_seen"] == 1_800_000_000
finally:
db.close()
def test_invalid_attr_and_missing():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
out = repo.upsert_from_fav(resource(2, attr=9), F1.media_id, was_added=True)
assert out.status == "removed"
assert repo.get_video(2)["attr"] == 9
# 原本有效的视频后来失效:保留原历史标题
repo.upsert_from_fav(
resource(4, title="原始标题"), F1.media_id, was_added=True
)
repo.upsert_from_fav(
resource(4, title="已失效视频", attr=9), F1.media_id, was_added=False
)
video4 = repo.get_video(4)
assert video4["status"] == "removed"
assert video4["title"] == "原始标题"
status = repo.mark_missing_resource(
3, folder_id=F1.media_id, bvid="BV3"
)
assert status == "removed"
video = repo.get_video(3)
assert video["status"] == "removed"
assert video["bvid"] == "BV3"
assert video["title"] == "(资源已失效)"
assert video["attr"] == 9
assert 3 in repo.folder_active_aids(F1.media_id)
finally:
db.close()
def test_remove_from_folder_transitions():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
for aid in (1, 2):
repo.upsert_from_fav(resource(aid), F1.media_id, was_added=True)
# 2 同时出现在两个夹:从 F1 移除后仍 active
repo.upsert_from_fav(resource(2), F2.media_id, was_added=True)
status = repo.mark_resource_removed(F1.media_id, 2)
assert status == "active"
assert repo.get_video(2)["status"] == "active"
# 1 只在 F1:移除 -> unfaved,但数据保留
status = repo.mark_resource_removed(F1.media_id, 1)
assert status == "unfaved"
assert repo.get_video(1)["status"] == "unfaved"
assert repo.get_video(1)["bvid"] == "BV1"
assert 1 not in repo.folder_active_aids(F1.media_id)
# 再次出现在收藏夹 -> active
out = repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
assert out.status == "active"
finally:
db.close()
def test_dropped_only_revives_on_reappearing_in_folder():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
repo.set_video_status(1, "dropped")
# 全量刷新(未重新加入)保持 dropped
out = repo.upsert_from_fav(resource(1), F1.media_id, was_added=False)
assert out.status == "dropped"
# 从收藏夹移除也不改变 dropped
repo.mark_resource_removed(F1.media_id, 1)
assert repo.get_video(1)["status"] == "dropped"
# 重新出现在收藏夹 -> 自动恢复 active
out = repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
assert out.status == "active"
finally:
db.close()
def test_removed_stays_removed_when_folder_removed():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1, attr=9), F1.media_id, was_added=True)
assert repo.get_video(1)["status"] == "removed"
status = repo.mark_resource_removed(F1.media_id, 1)
assert status == "removed"
finally:
db.close()
def test_multi_folder_moves_primary_and_warns():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
out = repo.upsert_from_fav(resource(1), F2.media_id, was_added=True)
assert out.moved_folder is True
assert out.previous_folder_id == F1.media_id
assert repo.get_video(1)["folder_id"] == F2.media_id
# 两个夹的资源关系都保留
assert repo.folder_active_aids(F1.media_id) == {1}
assert repo.folder_active_aids(F2.media_id) == {1}
finally:
db.close()
def test_blacklist_seed_idempotent():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
assert repo.seed_blacklist([1, 2, 3], now=100) == 3
assert repo.seed_blacklist([2, 3, 4], now=200) == 1
rows = repo.blacklist()
assert [r["mid"] for r in rows] == [1, 2, 3, 4]
assert all(r["reason"] == "seed" for r in rows)
assert repo.is_blacklisted(2) and not repo.is_blacklisted(99)
repo.add_blacklist(9, name="切片号", reason="manual")
assert repo.is_blacklisted(9)
repo.add_blacklist(9, reason="updated")
row = next(r for r in repo.blacklist() if r["mid"] == 9)
assert row["name"] == "切片号"
assert row["reason"] == "updated"
finally:
db.close()
def test_folder_sync_metadata_and_counts():
with tempfile.TemporaryDirectory() as tmp:
db, repo = setup(tmp)
try:
repo.upsert_from_fav(resource(1), F1.media_id, was_added=True)
repo.finish_folder_sync(
F1, media_count=1, full_refresh=True, now=1_800_000_000
)
row = repo.get_folder(F1.media_id)
assert row.media_count == 1
assert row.last_ids_at == 1_800_000_000
assert row.last_full_at == 1_800_000_000
repo.finish_folder_sync(
F1, media_count=2, full_refresh=False, now=1_800_000_100
)
row = repo.get_folder(F1.media_id)
assert row.media_count == 2
assert row.last_ids_at == 1_800_000_100
assert row.last_full_at == 1_800_000_000
# 配置中移除的年度夹停用,重新加入时恢复
repo.disable_folders_except([F1.media_id])
assert repo.get_folder(F2.media_id).enabled is False
repo.ensure_folder(F2)
assert repo.get_folder(F2.media_id).enabled is True
counts = repo.counts()
assert counts["video_total"] == 1
assert counts["video_by_status"] == {"active": 1}
assert counts["folders"] == 2
finally:
db.close()
def main():
test_upsert_and_folder_resource()
test_invalid_attr_and_missing()
test_remove_from_folder_transitions()
test_dropped_only_revives_on_reappearing_in_folder()
test_removed_stays_removed_when_folder_removed()
test_multi_folder_moves_primary_and_warns()
test_blacklist_seed_idempotent()
test_folder_sync_metadata_and_counts()
print("OK: 收藏夹/视频状态机与黑名单测试全部通过")
if __name__ == "__main__":
main()