|
|
@@ -0,0 +1,233 @@
|
|
|
+import httpx
|
|
|
+import pytest
|
|
|
+
|
|
|
+from content_agent.integrations.douyin import CrawapiDouyinClient
|
|
|
+
|
|
|
+
|
|
|
+class FakeHttpClient:
|
|
|
+ def __init__(self, responses):
|
|
|
+ self.responses = list(responses)
|
|
|
+ self.requests = []
|
|
|
+
|
|
|
+ def post(self, url, json, headers, timeout):
|
|
|
+ self.requests.append({"url": url, "json": json, "headers": headers, "timeout": timeout})
|
|
|
+ response = self.responses.pop(0)
|
|
|
+ if isinstance(response, Exception):
|
|
|
+ raise response
|
|
|
+ return response
|
|
|
+
|
|
|
+
|
|
|
+def _response(status_code, data):
|
|
|
+ return httpx.Response(
|
|
|
+ status_code,
|
|
|
+ json=data,
|
|
|
+ request=httpx.Request("POST", "http://crawapi.test/endpoint"),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _client(responses):
|
|
|
+ return CrawapiDouyinClient(
|
|
|
+ base_url="http://crawapi.test",
|
|
|
+ keyword_path="/crawler/dou_yin/keyword",
|
|
|
+ content_portrait_path="/crawler/dou_yin/re_dian_bao/video_like_portrait",
|
|
|
+ default_account_id="771431222",
|
|
|
+ http_client=FakeHttpClient(responses),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def test_douyin_keyword_search_maps_video_and_portrait_fields():
|
|
|
+ client = _client(
|
|
|
+ [
|
|
|
+ _response(
|
|
|
+ 200,
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "data": [
|
|
|
+ {
|
|
|
+ "aweme_id": "7615247738577423622",
|
|
|
+ "desc": "早睡早起早上好",
|
|
|
+ "author": {
|
|
|
+ "nickname": "让你心情变浅的兔子",
|
|
|
+ "sec_uid": "MS4wLjABAAAAdYc",
|
|
|
+ },
|
|
|
+ "statistics": {
|
|
|
+ "digg_count": 1931,
|
|
|
+ "comment_count": 45,
|
|
|
+ "share_count": 1968,
|
|
|
+ "collect_count": 462,
|
|
|
+ },
|
|
|
+ "text_extra": [{"hashtag_name": "早上好"}],
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "has_more": True,
|
|
|
+ "next_cursor": "10",
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ _response(
|
|
|
+ 200,
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "data": {
|
|
|
+ "年龄": {
|
|
|
+ "50+": {"percentage": "18.00%", "preference": "135.0"},
|
|
|
+ "31-40": {"percentage": "20.00%", "preference": "80.0"},
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ]
|
|
|
+ )
|
|
|
+
|
|
|
+ results = client.search(
|
|
|
+ {
|
|
|
+ "query_id": "q_001",
|
|
|
+ "query": "早上好祝福视频",
|
|
|
+ "origin_source": "pattern_itemset",
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ assert len(results) == 1
|
|
|
+ result = results[0]
|
|
|
+ assert result["candidate_id"] == "q_001_c_001"
|
|
|
+ assert result["aweme_id"] == "7615247738577423622"
|
|
|
+ assert result["author_sec_uid"] == "MS4wLjABAAAAdYc"
|
|
|
+ assert result["cha_list"] == ["#早上好"]
|
|
|
+ assert result["portrait_available"] is True
|
|
|
+ assert result["age_50_plus_level"] == "strong"
|
|
|
+ assert result["pattern_recall"] == "candidate_related"
|
|
|
+ assert result["candidate_relation"] == "derived_from_pattern_demand"
|
|
|
+ assert result["platform_auth_mode"] == "no_bearer"
|
|
|
+ assert client.http_client.requests[0]["json"]["account_id"] == "771431222"
|
|
|
+
|
|
|
+
|
|
|
+def test_douyin_keyword_search_returns_empty_list():
|
|
|
+ client = _client([_response(200, {"data": {"data": [], "has_more": False}})])
|
|
|
+
|
|
|
+ results = client.search(
|
|
|
+ {"query_id": "q_001", "query": "无结果", "origin_source": "pattern_itemset"}
|
|
|
+ )
|
|
|
+
|
|
|
+ assert results == []
|
|
|
+
|
|
|
+
|
|
|
+def test_douyin_keyword_search_http_error_is_sanitized():
|
|
|
+ client = _client([_response(500, {"error": "server failed"})])
|
|
|
+
|
|
|
+ with pytest.raises(RuntimeError, match="keyword_search failed: HTTP 500"):
|
|
|
+ client.search(
|
|
|
+ {"query_id": "q_001", "query": "接口失败", "origin_source": "pattern_itemset"}
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def test_douyin_portrait_http_error_marks_candidate_missing_portrait():
|
|
|
+ client = _client(
|
|
|
+ [
|
|
|
+ _response(
|
|
|
+ 200,
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "data": [
|
|
|
+ {
|
|
|
+ "aweme_id": "7615247738577423622",
|
|
|
+ "desc": "早上好",
|
|
|
+ "author": {"nickname": "作者", "sec_uid": "MS4wLjABAAAA001"},
|
|
|
+ "statistics": {"digg_count": 1},
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ _response(500, {"error": "portrait failed"}),
|
|
|
+ ]
|
|
|
+ )
|
|
|
+
|
|
|
+ result = client.search(
|
|
|
+ {
|
|
|
+ "query_id": "q_001",
|
|
|
+ "query": "早上好",
|
|
|
+ "origin_source": "pattern_itemset",
|
|
|
+ }
|
|
|
+ )[0]
|
|
|
+
|
|
|
+ assert result["portrait_available"] is False
|
|
|
+ assert result["age_50_plus_level"] == "missing"
|
|
|
+
|
|
|
+
|
|
|
+def test_douyin_keyword_search_can_limit_results_per_query():
|
|
|
+ client = CrawapiDouyinClient(
|
|
|
+ base_url="http://crawapi.test",
|
|
|
+ keyword_path="/crawler/dou_yin/keyword",
|
|
|
+ content_portrait_path="/crawler/dou_yin/re_dian_bao/video_like_portrait",
|
|
|
+ max_results_per_query=1,
|
|
|
+ http_client=FakeHttpClient(
|
|
|
+ [
|
|
|
+ _response(
|
|
|
+ 200,
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "data": [
|
|
|
+ {"aweme_id": "1", "author": {}, "statistics": {}},
|
|
|
+ {"aweme_id": "2", "author": {}, "statistics": {}},
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ _response(200, {"data": {"data": {"年龄": {}}}}),
|
|
|
+ ]
|
|
|
+ ),
|
|
|
+ )
|
|
|
+
|
|
|
+ results = client.search(
|
|
|
+ {"query_id": "q_001", "query": "限量", "origin_source": "pattern_itemset"}
|
|
|
+ )
|
|
|
+
|
|
|
+ assert [result["aweme_id"] for result in results] == ["1"]
|
|
|
+
|
|
|
+
|
|
|
+def test_douyin_portrait_supports_dimensions_shape_and_excludes_41_to_50():
|
|
|
+ client = _client(
|
|
|
+ [
|
|
|
+ _response(
|
|
|
+ 200,
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "data": [
|
|
|
+ {
|
|
|
+ "aweme_id": "7635992906608060495",
|
|
|
+ "desc": "高考加油",
|
|
|
+ "author": {"nickname": "一个富贵", "sec_uid": "MS4wLjABAAAA001"},
|
|
|
+ "statistics": {"digg_count": 100},
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ _response(
|
|
|
+ 200,
|
|
|
+ {
|
|
|
+ "data": {
|
|
|
+ "data": {
|
|
|
+ "dimensions": {
|
|
|
+ "年龄": [
|
|
|
+ {"name": "41-50", "percentage": "30.00%", "preference": "150.0"},
|
|
|
+ {"name": "50-", "percentage": "4.83%", "preference": "13.80"},
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ),
|
|
|
+ ]
|
|
|
+ )
|
|
|
+
|
|
|
+ result = client.search(
|
|
|
+ {"query_id": "q_001", "query": "高考加油", "origin_source": "pattern_itemset"}
|
|
|
+ )[0]
|
|
|
+
|
|
|
+ assert result["portrait_available"] is True
|
|
|
+ assert result["age_50_plus_ratio"] == 0.0483
|
|
|
+ assert result["age_50_plus_level"] == "weak"
|
|
|
+ assert result["age_distribution"][0]["is_50_plus"] is False
|
|
|
+ assert result["age_distribution"][1]["is_50_plus"] is True
|