Files
re-xxm/tests/test_library.py
T

344 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""入库(候选确认 / 手动)、忽略、拉黑动作测试(假只读客户端,不联网):
uv run python tests/test_library.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")
from fixtures import view_for # noqa: E402
from reaction_board.bilibili.client import ( # noqa: E402
BilibiliAPIError,
BilibiliError,
)
from reaction_board.bilibili.models import VideoDetail # noqa: E402
from reaction_board.config import ConfigStore # noqa: E402
from reaction_board.database import Database # noqa: E402
from reaction_board.repository import Repository # noqa: E402
from reaction_board.services.library import LibraryService # noqa: E402
from reaction_board.timeutil import shanghai_date # noqa: E402
NOW = 1_793_444_393
DATE = shanghai_date(NOW)
BV = "BV1xvxi6LEZD"
def detail(
aid,
*,
bvid=BV,
title="咻咻满 reaction",
up_mid=100,
up_name="UP100",
pubdate=NOW - 3600,
view=819,
duration=9504,
pages=2,
):
payload = view_for(aid, view=view)
data = payload["data"]
data["bvid"] = bvid
data["title"] = title
data["pubdate"] = pubdate
data["ctime"] = pubdate
data["duration"] = duration
data["owner"] = {"mid": up_mid, "name": up_name, "face": "http://i1.hdslb.com/bfs/face/u.jpg"}
data["pages"] = [{"cid": 1, "page": i + 1} for i in range(pages)]
return VideoDetail.from_api(data)
class FakeReadClient:
def __init__(self, by_bvid=None, by_aid=None, short_map=None, fail_bvids=(),
api_errors=None):
self.by_bvid = by_bvid or {}
self.by_aid = by_aid or {}
self.short_map = short_map or {}
self.fail_bvids = set(fail_bvids)
self.api_errors = dict(api_errors or {})
def view_detail(self, *, aid=None, bvid=None):
if bvid is not None:
if bvid in self.api_errors:
raise BilibiliAPIError(self.api_errors[bvid], "请求错误")
if bvid in self.fail_bvids:
raise BilibiliError(f"view 失败: {bvid}")
if bvid in self.by_bvid:
return self.by_bvid[bvid]
raise BilibiliError(f"未知 bvid: {bvid}")
if aid in self.by_aid:
return self.by_aid[aid]
raise BilibiliError(f"未知 aid: {aid}")
def resolve_short_link(self, url):
if url not in self.short_map:
raise BilibiliError(f"未知短链: {url}")
return self.short_map[url]
class FakeProvider:
def __init__(self, client):
self.client = client
def get(self):
return self.client
class FakeUpProfile:
def __init__(self):
self.calls = []
def capture(self, mids, *, names=None, live_since=None, backfilled=False):
self.calls.append((list(mids), dict(names or {})))
class Result:
requested = len(mids)
updated_videos = len(mids)
failed = 0
return Result()
def write_config(tmp):
data = {
"bilibili": {
"read": {"mid": 1, "csrf": "r", "cookie": "read-cookie"},
"write": {"mid": 2, "csrf": "w", "cookie": "write-cookie"},
},
"storage": {
"database_path": str(Path(tmp) / "app.db"),
"backup_dir": str(Path(tmp) / "backup"),
},
"discover": {"keywords": ["x"], "pages": 1, "window_days": 14},
}
path = Path(tmp) / "config.json"
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
return path
def setup(tmp, client, up_profile=None):
store = ConfigStore(write_config(tmp), env={})
db = Database(Path(tmp) / "app.db", backup_dir=Path(tmp) / "backup")
db.initialize()
repo = Repository(db, clock=lambda: NOW)
service = LibraryService(
store, repo, FakeProvider(client),
up_profile_service=up_profile, clock=lambda: NOW,
)
return db, repo, service
def add_candidate(repo, aid, *, up_mid=100, pubdate=NOW - 3600):
return repo.upsert_candidate(
aid=aid, bvid=BV, title="咻咻满 reaction", up_mid=up_mid,
up_name=f"UP{up_mid}", pubdate=pubdate, cover=None, score=3,
channel="search:test#p1", now=NOW,
)
def test_ingest_candidate_added_and_snapshot():
with tempfile.TemporaryDirectory() as tmp:
ups = FakeUpProfile()
client = FakeReadClient(by_aid={9: detail(9)})
db, repo, service = setup(tmp, client, ups)
try:
add_candidate(repo, 9)
result = service.ingest_candidate(9)
assert result.ok is True and result.state == "added"
assert result.aid == 9
assert repo.get_candidate(9) is None
video = repo.get_video(9)
assert video["status"] == "active"
assert video["source"] == "candidate:telegram"
assert video["folder_id"] is None
assert video["up_mid"] == 100
assert video["up_face"] == "http://i1.hdslb.com/bfs/face/u.jpg"
snap = repo.get_snapshot(DATE, 9)
assert snap is not None and snap["ok"] == 1 and snap["view"] == 819
assert ups.calls == [([100], {100: "UP100"})]
finally:
db.close()
def test_ingest_candidate_exists_and_missing():
with tempfile.TemporaryDirectory() as tmp:
client = FakeReadClient(by_aid={9: detail(9)})
db, repo, service = setup(tmp, client)
try:
add_candidate(repo, 9)
assert service.ingest_candidate(9).state == "added"
# 候选已被删除,但视频已在库
again = service.ingest_candidate(9)
assert again.ok is True and again.state == "exists"
missing = service.ingest_candidate(999)
assert missing.ok is False and missing.state == "missing"
finally:
db.close()
def test_ingest_candidate_recovers_dropped():
with tempfile.TemporaryDirectory() as tmp:
client = FakeReadClient(by_aid={9: detail(9)})
db, repo, service = setup(tmp, client)
try:
add_candidate(repo, 9)
assert service.ingest_candidate(9).state == "added"
repo.set_video_status(9, "dropped", now=NOW)
add_candidate(repo, 9)
result = service.ingest_candidate(9)
assert result.ok is True and result.state == "recovered"
assert repo.get_video(9)["status"] == "active"
finally:
db.close()
def test_ingest_candidate_falls_back_when_detail_fails():
with tempfile.TemporaryDirectory() as tmp:
client = FakeReadClient(fail_bvids={BV})
db, repo, service = setup(tmp, client)
try:
add_candidate(repo, 9)
result = service.ingest_candidate(9)
assert result.ok is True and result.state == "added"
assert repo.get_video(9)["title"] == "咻咻满 reaction"
assert repo.get_snapshot(DATE, 9) is None
finally:
db.close()
def test_manual_ingest_forms_and_multiple():
with tempfile.TemporaryDirectory() as tmp:
ups = FakeUpProfile()
client = FakeReadClient(
by_bvid={BV: detail(9)},
by_aid={12345: detail(77, bvid="BV77", up_mid=200, up_name="UP200")},
short_map={"https://b23.tv/abc": f"https://www.bilibili.com/video/{BV}"},
)
db, repo, service = setup(tmp, client, ups)
try:
result = service.ingest_text(
f"看这个 https://www.bilibili.com/video/{BV} 还有 av12345"
)
assert result.refs == 2 and result.added == 2
assert [r.aid for r in result.results] == [9, 77]
assert repo.get_video(9)["source"] == "manual:telegram"
assert repo.get_video(77)["status"] == "active"
short = service.ingest_text("https://b23.tv/abc")
assert short.added == 0
assert short.results[0].state == "exists"
none = service.ingest_text("没有视频")
assert none.refs == 0 and none.results == []
finally:
db.close()
def test_manual_ingest_error_isolation():
with tempfile.TemporaryDirectory() as tmp:
client = FakeReadClient(
by_bvid={BV: detail(9)},
fail_bvids={"BVfail000000"},
)
db, repo, service = setup(tmp, client)
try:
result = service.ingest_text(f"{BV} BVfail000000")
assert result.added == 1
states = [r.state for r in result.results]
assert states == ["added", "error"]
finally:
db.close()
def test_snapshot_not_overwritten():
with tempfile.TemporaryDirectory() as tmp:
client = FakeReadClient(by_bvid={BV: detail(9, view=819)})
db, repo, service = setup(tmp, client)
try:
repo.upsert_snapshot(
DATE, 9, metrics={"view": 5000}, ok=True, err=None, captured_at=NOW,
)
service.ingest_text(BV)
snap = repo.get_snapshot(DATE, 9)
assert snap["view"] == 5000 # 已有成功快照不覆盖
finally:
db.close()
def test_manual_ingest_truncation():
with tempfile.TemporaryDirectory() as tmp:
bvs = {
"BV1x8x166E00": detail(1, bvid="BV1x8x166E00"),
"BV1x8x166E01": detail(2, bvid="BV1x8x166E01"),
"BV1x8x166E02": detail(3, bvid="BV1x8x166E02"),
}
client = FakeReadClient(by_bvid=bvs)
store = ConfigStore(write_config(tmp), env={})
db = Database(Path(tmp) / "app.db", backup_dir=Path(tmp) / "backup")
db.initialize()
try:
repo = Repository(db, clock=lambda: NOW)
service = LibraryService(
store, repo, FakeProvider(client), clock=lambda: NOW, max_refs=2
)
result = service.ingest_text(" ".join(bvs))
assert result.refs == 2
assert result.truncated == 1
assert result.added == 2
assert len(result.results) == 2
finally:
db.close()
def test_manual_ingest_invalid_bvid_message():
with tempfile.TemporaryDirectory() as tmp:
client = FakeReadClient(api_errors={"BV1x8x166E00": -400})
db, repo, service = setup(tmp, client)
try:
result = service.ingest_text("BV1x8x166E00")
assert result.refs == 1
assert result.results[0].state == "error"
assert "无效或视频不存在" in result.results[0].message
finally:
db.close()
def test_ignore_and_blacklist():
with tempfile.TemporaryDirectory() as tmp:
client = FakeReadClient()
db, repo, service = setup(tmp, client)
try:
add_candidate(repo, 11, up_mid=300)
add_candidate(repo, 12, up_mid=300)
assert service.ignore(11).state == "rejected"
assert repo.get_candidate(11)["decision"] == "rejected"
result = service.blacklist(12)
assert result.ok is True
assert repo.is_blacklisted(300)
assert repo.get_candidate(12)["decision"] == "rejected"
assert service.ignore(999).state == "missing"
finally:
db.close()
def main():
test_ingest_candidate_added_and_snapshot()
test_ingest_candidate_exists_and_missing()
test_ingest_candidate_recovers_dropped()
test_ingest_candidate_falls_back_when_detail_fails()
test_manual_ingest_forms_and_multiple()
test_manual_ingest_error_isolation()
test_snapshot_not_overwritten()
test_manual_ingest_truncation()
test_manual_ingest_invalid_bvid_message()
test_ignore_and_blacklist()
print("OK: 入库、手动入库、忽略、拉黑测试全部通过")
if __name__ == "__main__":
main()