test_search.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. """关键词搜索离线测试:纯 parser + search_keyword(去重/翻页/截断/SearchError)。"""
  2. from __future__ import annotations
  3. import json
  4. from pathlib import Path
  5. import pytest
  6. from creation_knowledge.config import PgConfig, Settings
  7. from creation_knowledge.integrations.crawler import RateLimiter
  8. from creation_knowledge.integrations.search import (
  9. SearchError, parse_search_response, search_keyword,
  10. )
  11. FIX = Path(__file__).parent / "fixtures"
  12. def _settings() -> Settings:
  13. return Settings(
  14. pg=PgConfig(host="h", port=5432, user="u", password="p", database="d"),
  15. crawler_base_url="http://crawler.test", crawler_key="", crawler_timeout=30,
  16. video_model="m", gemini_api_key="",
  17. openrouter_base_url="b", openrouter_api_key="k",
  18. llm_model="m", max_cards=12, frames_dir="f", douyin_ratio="540p", data_dir="")
  19. def _no_wait() -> RateLimiter:
  20. return RateLimiter(min_interval_seconds=0.0)
  21. class _Resp:
  22. def __init__(self, payload): self._p = payload
  23. def raise_for_status(self): return None
  24. def json(self): return self._p
  25. class _FakeClient:
  26. """按调用次序返回预置 payload;记录每次 post 的 body。"""
  27. def __init__(self, pages): self.pages = list(pages); self.calls = []; self.closed = False
  28. def post(self, url, json=None, headers=None, timeout=None):
  29. self.calls.append(json)
  30. return _Resp(self.pages.pop(0))
  31. def close(self): self.closed = True
  32. def test_parse_from_fixture():
  33. resp = json.loads((FIX / "xhs_search_分镜脚本.json").read_text("utf-8"))
  34. ids, has_more, cursor = parse_search_response(resp)
  35. assert ids == ["6a2bce890000000006034be4", "68282529000000002100e28c"]
  36. assert has_more is True and cursor == "2"
  37. def test_parse_business_error_raises():
  38. with pytest.raises(SearchError):
  39. parse_search_response({"code": 10000, "msg": "未知错误", "data": None})
  40. def test_search_dedup_and_limit():
  41. # page1 含重复 id,page2 提供更多;limit=3 → 截断且去重
  42. page1 = {"code": 0, "data": {"has_more": True, "next_cursor": "2",
  43. "data": [{"id": "a"}, {"id": "a"}, {"id": "b"}]}}
  44. page2 = {"code": 0, "data": {"has_more": True, "next_cursor": "3",
  45. "data": [{"id": "b"}, {"id": "c"}, {"id": "d"}]}}
  46. client = _FakeClient([page1, page2])
  47. out = search_keyword("分镜", settings=_settings(), http_client=client,
  48. rate_limiter=_no_wait(), limit=3)
  49. assert out == ["a", "b", "c"]
  50. def test_search_stops_on_no_more():
  51. page1 = {"code": 0, "data": {"has_more": False, "next_cursor": "",
  52. "data": [{"id": "a"}, {"id": "b"}]}}
  53. client = _FakeClient([page1])
  54. out = search_keyword("分镜", settings=_settings(), http_client=client,
  55. rate_limiter=_no_wait(), limit=10)
  56. assert out == ["a", "b"] # has_more=False → 不再翻页,不报错
  57. assert client.calls[0]["keyword"] == "分镜"
  58. def test_search_business_error_raises():
  59. client = _FakeClient([{"code": 10000, "msg": "未知错误", "data": None}])
  60. with pytest.raises(SearchError):
  61. search_keyword("x", settings=_settings(), http_client=client,
  62. rate_limiter=_no_wait())
  63. def test_search_unsupported_platform():
  64. with pytest.raises(SearchError):
  65. search_keyword("x", platform="bilibili", settings=_settings(),
  66. rate_limiter=_no_wait())