kol-insight/backend/tests/test_yuntu_api.py
zfc 7cd29c5980 feat(frontend): 重构视频分析页面,支持多种搜索方式
主要更新:
- 前端改用 Ant Design 组件(Table、Modal、Select 等)
- 支持三种搜索方式:星图ID、达人unique_id、达人昵称模糊匹配
- 列表页实时调用云图 API 获取 A3 数据和成本指标
- 详情弹窗显示完整 6 大类指标,支持文字复制
- 品牌 API URL 格式修复为查询参数形式
- 优化云图 API 参数格式和会话池管理

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 22:01:55 +08:00

415 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Tests for Yuntu API Service (T-023, T-027)
T-027 更新:
- call_yuntu_api 参数改为 auth_token完整 cookie 值)
- 日期格式改为 YYYYMMDD
- industry_id 改为字符串
"""
import pytest
from datetime import datetime
from unittest.mock import AsyncMock, patch, MagicMock
import httpx
from app.services.yuntu_api import (
call_yuntu_api,
get_video_analysis,
parse_analysis_response,
YuntuAPIError,
SessionInvalidError,
)
class TestCallYuntuAPI:
"""Tests for call_yuntu_api function."""
async def test_call_success(self):
"""Test successful API call."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"status": 0,
"msg": "ok",
"data": {
"total_show_cnt": 100000,
"a3_increase_cnt": "500",
},
}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
with patch("httpx.AsyncClient", return_value=mock_client):
result = await call_yuntu_api(
item_id="test_item_123",
publish_time=datetime(2025, 1, 1),
industry_id="20",
aadvid="1648829117232140",
auth_token="sessionid=test_session",
)
assert result["status"] == 0
assert result["data"]["total_show_cnt"] == 100000
async def test_call_with_correct_parameters(self):
"""Test that API is called with correct parameters (T-027 format)."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"status": 0, "data": {}}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
with patch("httpx.AsyncClient", return_value=mock_client):
await call_yuntu_api(
item_id="video_001",
publish_time=datetime(2025, 1, 15),
industry_id="30",
aadvid="1648829117232140",
auth_token="sessionid=session_abc",
)
mock_client.post.assert_called_once()
call_args = mock_client.post.call_args
# 验证URL包含aadvid
assert "GetContentMaterialAnalysisInfo" in call_args.args[0]
assert "aadvid=1648829117232140" in call_args.args[0]
# 验证请求体 - T-027: 日期格式 YYYYMMDD
json_data = call_args.kwargs["json"]
assert json_data["object_id"] == "video_001"
assert json_data["start_date"] == "20250115" # YYYYMMDD
assert json_data["end_date"] == "20250214" # +30天
assert json_data["industry_id_list"] == ["30"] # 字符串数组
# 验证headers - T-027: 直接使用 auth_token
headers = call_args.kwargs["headers"]
assert headers["Cookie"] == "sessionid=session_abc"
async def test_call_session_invalid_401(self):
"""Test handling of 401 response (session invalid)."""
mock_response = MagicMock()
mock_response.status_code = 401
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
with patch("httpx.AsyncClient", return_value=mock_client):
with pytest.raises(SessionInvalidError) as exc_info:
await call_yuntu_api(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
aadvid="123",
auth_token="sessionid=invalid_session",
)
assert exc_info.value.status_code == 401
async def test_call_session_invalid_403(self):
"""Test handling of 403 response (session invalid)."""
mock_response = MagicMock()
mock_response.status_code = 403
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
with patch("httpx.AsyncClient", return_value=mock_client):
with pytest.raises(SessionInvalidError):
await call_yuntu_api(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
aadvid="123",
auth_token="sessionid=invalid_session",
)
async def test_call_api_error_500(self):
"""Test handling of 500 response."""
mock_response = MagicMock()
mock_response.status_code = 500
mock_response.text = "Internal Server Error"
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
with patch("httpx.AsyncClient", return_value=mock_client):
with pytest.raises(YuntuAPIError) as exc_info:
await call_yuntu_api(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
aadvid="123",
auth_token="sessionid=session",
)
assert exc_info.value.status_code == 500
async def test_call_business_error(self):
"""Test handling of business error (status != 0)."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"status": 1001,
"msg": "Invalid parameter",
}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
with patch("httpx.AsyncClient", return_value=mock_client):
with pytest.raises(YuntuAPIError) as exc_info:
await call_yuntu_api(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
aadvid="123",
auth_token="sessionid=session",
)
assert "Invalid parameter" in exc_info.value.message
async def test_call_timeout(self):
"""Test handling of timeout."""
mock_client = AsyncMock()
mock_client.post.side_effect = httpx.TimeoutException("Timeout")
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
with patch("httpx.AsyncClient", return_value=mock_client):
with pytest.raises(YuntuAPIError) as exc_info:
await call_yuntu_api(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
aadvid="123",
auth_token="sessionid=session",
)
assert "timeout" in exc_info.value.message.lower()
async def test_call_request_error(self):
"""Test handling of request error."""
mock_client = AsyncMock()
mock_client.post.side_effect = httpx.RequestError("Connection failed")
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
with patch("httpx.AsyncClient", return_value=mock_client):
with pytest.raises(YuntuAPIError):
await call_yuntu_api(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
aadvid="123",
auth_token="sessionid=session",
)
class TestGetVideoAnalysis:
"""Tests for get_video_analysis function with retry logic (T-022, T-027)."""
async def test_success_first_try(self):
"""Test successful call on first attempt."""
with patch("app.services.yuntu_api.get_random_config") as mock_config:
mock_config.return_value = {
"aadvid": "123",
"auth_token": "sessionid=valid_session",
}
with patch("app.services.yuntu_api.call_yuntu_api") as mock_call:
mock_call.return_value = {"status": 0, "data": {"a3_increase_cnt": "100"}}
result = await get_video_analysis(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
)
assert result["data"]["a3_increase_cnt"] == "100"
assert mock_call.call_count == 1
async def test_retry_on_session_invalid(self):
"""Test retry when session is invalid."""
with patch("app.services.yuntu_api.get_random_config") as mock_config:
mock_config.side_effect = [
{"aadvid": "123", "auth_token": "sessionid=session_1"},
{"aadvid": "456", "auth_token": "sessionid=session_2"},
{"aadvid": "789", "auth_token": "sessionid=session_3"},
]
with patch("app.services.yuntu_api.call_yuntu_api") as mock_call:
# 前两次失败,第三次成功
mock_call.side_effect = [
SessionInvalidError("Invalid"),
SessionInvalidError("Invalid"),
{"status": 0, "data": {}},
]
with patch("app.services.yuntu_api.session_pool") as mock_pool:
result = await get_video_analysis(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
max_retries=3,
)
assert result["status"] == 0
assert mock_call.call_count == 3
# 验证失效的session被移除
assert mock_pool.remove_by_auth_token.call_count == 2
async def test_max_retries_exceeded(self):
"""Test that error is raised after max retries."""
with patch("app.services.yuntu_api.get_random_config") as mock_config:
mock_config.return_value = {"aadvid": "123", "auth_token": "sessionid=session"}
with patch("app.services.yuntu_api.call_yuntu_api") as mock_call:
mock_call.side_effect = SessionInvalidError("Invalid")
with patch("app.services.yuntu_api.session_pool"):
with pytest.raises(SessionInvalidError):
await get_video_analysis(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
max_retries=3,
)
assert mock_call.call_count == 3
async def test_no_retry_on_api_error(self):
"""Test that non-session errors don't trigger retry."""
with patch("app.services.yuntu_api.get_random_config") as mock_config:
mock_config.return_value = {"aadvid": "123", "auth_token": "sessionid=session"}
with patch("app.services.yuntu_api.call_yuntu_api") as mock_call:
mock_call.side_effect = YuntuAPIError("Server error", status_code=500)
with pytest.raises(YuntuAPIError) as exc_info:
await get_video_analysis(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
)
assert mock_call.call_count == 1
assert exc_info.value.status_code == 500
async def test_no_config_available(self):
"""Test error when no config is available."""
with patch("app.services.yuntu_api.get_random_config") as mock_config:
mock_config.return_value = None
with pytest.raises(YuntuAPIError):
await get_video_analysis(
item_id="test",
publish_time=datetime.now(),
industry_id="20",
)
class TestParseAnalysisResponse:
"""Tests for parse_analysis_response function."""
def test_parse_complete_response(self):
"""Test parsing complete response data (T-027: handles string values)."""
response = {
"data": {
"total_show_cnt": 100000,
"natural_show_cnt": 80000,
"ad_show_cnt": 20000,
"total_play_cnt": 50000,
"natural_play_cnt": 40000,
"ad_play_cnt": 10000,
"effective_play_cnt": 30000,
"a3_increase_cnt": "500", # 字符串
"ad_a3_increase_cnt": "100",
"natural_a3_increase_cnt": "400",
"after_view_search_uv": 1000,
"after_view_search_pv": 1500,
"brand_search_uv": 200,
"product_search_uv": 300,
"return_search_cnt": 50,
"cost": 10000,
"natural_cost": 0,
"ad_cost": 10000,
}
}
result = parse_analysis_response(response)
assert result["total_show_cnt"] == 100000
assert result["natural_show_cnt"] == 80000
assert result["a3_increase_cnt"] == 500 # 转为整数
assert result["ad_a3_increase_cnt"] == 100
assert result["natural_a3_increase_cnt"] == 400
assert result["after_view_search_uv"] == 1000
assert result["cost"] == 10000
def test_parse_empty_response(self):
"""Test parsing empty response."""
response = {"data": {}}
result = parse_analysis_response(response)
assert result["total_show_cnt"] == 0
assert result["a3_increase_cnt"] == 0
assert result["cost"] == 0
def test_parse_missing_data_key(self):
"""Test parsing response without data key."""
response = {}
result = parse_analysis_response(response)
assert result["total_show_cnt"] == 0
def test_parse_partial_response(self):
"""Test parsing partial response."""
response = {
"data": {
"total_show_cnt": 50000,
"a3_increase_cnt": "100",
}
}
result = parse_analysis_response(response)
assert result["total_show_cnt"] == 50000
assert result["a3_increase_cnt"] == 100
assert result["natural_show_cnt"] == 0 # Default value
assert result["cost"] == 0 # Default value
def test_parse_string_numbers(self):
"""Test parsing string numbers to int (T-027)."""
response = {
"data": {
"a3_increase_cnt": "1689071",
"ad_a3_increase_cnt": "36902",
"natural_a3_increase_cnt": "1652169",
"cost": 785000,
}
}
result = parse_analysis_response(response)
assert result["a3_increase_cnt"] == 1689071
assert result["ad_a3_increase_cnt"] == 36902
assert result["natural_a3_increase_cnt"] == 1652169
assert result["cost"] == 785000