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>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
"""
|
||||
Tests for SessionID Pool Service (T-021, T-022)
|
||||
Tests for SessionID Pool Service (T-021, T-022, T-027)
|
||||
|
||||
T-027 更新:
|
||||
- 改为 CookieConfig 数据结构
|
||||
- get_random_config() 随机选取配置
|
||||
- remove_by_auth_token() 移除失效配置
|
||||
"""
|
||||
|
||||
import pytest
|
||||
@@ -8,8 +13,10 @@ import httpx
|
||||
|
||||
from app.services.session_pool import (
|
||||
SessionPool,
|
||||
CookieConfig,
|
||||
session_pool,
|
||||
get_session_with_retry,
|
||||
get_random_config,
|
||||
)
|
||||
|
||||
|
||||
@@ -17,16 +24,27 @@ class TestSessionPool:
|
||||
"""Tests for SessionPool class."""
|
||||
|
||||
async def test_refresh_success(self):
|
||||
"""Test successful session pool refresh."""
|
||||
"""Test successful session pool refresh (T-027 format)."""
|
||||
pool = SessionPool()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"data": [
|
||||
{"sessionid": "session_001", "user": "test1"},
|
||||
{"sessionid": "session_002", "user": "test2"},
|
||||
{"sessionid": "session_003", "user": "test3"},
|
||||
{
|
||||
"brand_id": "533661",
|
||||
"aadvid": "1648829117232140",
|
||||
"auth_token": "sessionid=session_001",
|
||||
"industry_id": 20,
|
||||
"brand_name": "Brand1",
|
||||
},
|
||||
{
|
||||
"brand_id": "10186612",
|
||||
"aadvid": "9876543210",
|
||||
"auth_token": "sessionid=session_002",
|
||||
"industry_id": 30,
|
||||
"brand_name": "Brand2",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -39,9 +57,38 @@ class TestSessionPool:
|
||||
result = await pool.refresh()
|
||||
|
||||
assert result is True
|
||||
assert pool.size == 3
|
||||
assert pool.size == 2
|
||||
assert not pool.is_empty
|
||||
|
||||
async def test_refresh_with_sessionid_cookie_field(self):
|
||||
"""Test refresh using sessionid_cookie field (fallback)."""
|
||||
pool = SessionPool()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"data": [
|
||||
{
|
||||
"brand_id": "533661",
|
||||
"aadvid": "1648829117232140",
|
||||
"sessionid_cookie": "sessionid=session_001",
|
||||
"industry_id": 20,
|
||||
"brand_name": "Brand1",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get.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 pool.refresh()
|
||||
|
||||
assert result is True
|
||||
assert pool.size == 1
|
||||
|
||||
async def test_refresh_empty_data(self):
|
||||
"""Test refresh with empty data array."""
|
||||
pool = SessionPool()
|
||||
@@ -126,7 +173,17 @@ class TestSessionPool:
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"data": [{"sessionid": "test"}]}
|
||||
mock_response.json.return_value = {
|
||||
"data": [
|
||||
{
|
||||
"brand_id": "123",
|
||||
"aadvid": "456",
|
||||
"auth_token": "sessionid=test",
|
||||
"industry_id": 20,
|
||||
"brand_name": "Test",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get.return_value = mock_response
|
||||
@@ -146,40 +203,131 @@ class TestSessionPool:
|
||||
assert "headers" in call_args.kwargs
|
||||
assert call_args.kwargs["headers"]["Authorization"] == "Bearer test_token"
|
||||
|
||||
def test_get_random_from_pool(self):
|
||||
"""Test getting random session from pool."""
|
||||
def test_get_random_config_from_pool(self):
|
||||
"""Test getting random config from pool (T-027)."""
|
||||
pool = SessionPool()
|
||||
pool._sessions = ["session_1", "session_2", "session_3"]
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="533661",
|
||||
aadvid="1648829117232140",
|
||||
auth_token="sessionid=session_1",
|
||||
industry_id=20,
|
||||
brand_name="Brand1",
|
||||
),
|
||||
CookieConfig(
|
||||
brand_id="10186612",
|
||||
aadvid="9876543210",
|
||||
auth_token="sessionid=session_2",
|
||||
industry_id=30,
|
||||
brand_name="Brand2",
|
||||
),
|
||||
]
|
||||
|
||||
config = pool.get_random_config()
|
||||
|
||||
assert config is not None
|
||||
assert "aadvid" in config
|
||||
assert "auth_token" in config
|
||||
assert config["auth_token"] in ["sessionid=session_1", "sessionid=session_2"]
|
||||
|
||||
def test_get_random_config_from_empty_pool(self):
|
||||
"""Test getting random config from empty pool."""
|
||||
pool = SessionPool()
|
||||
|
||||
config = pool.get_random_config()
|
||||
|
||||
assert config is None
|
||||
|
||||
def test_get_random_from_pool_compat(self):
|
||||
"""Test get_random compatibility method."""
|
||||
pool = SessionPool()
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="533661",
|
||||
aadvid="1648829117232140",
|
||||
auth_token="sessionid=session_1",
|
||||
industry_id=20,
|
||||
brand_name="Brand1",
|
||||
),
|
||||
]
|
||||
|
||||
session = pool.get_random()
|
||||
|
||||
assert session in pool._sessions
|
||||
assert session == "session_1"
|
||||
|
||||
def test_get_random_from_empty_pool(self):
|
||||
"""Test getting random session from empty pool."""
|
||||
def test_get_random_from_empty_pool_compat(self):
|
||||
"""Test get_random from empty pool."""
|
||||
pool = SessionPool()
|
||||
|
||||
session = pool.get_random()
|
||||
|
||||
assert session is None
|
||||
|
||||
def test_remove_session(self):
|
||||
"""Test removing a session from pool."""
|
||||
def test_remove_by_auth_token(self):
|
||||
"""Test removing config by auth_token (T-027)."""
|
||||
pool = SessionPool()
|
||||
pool._sessions = ["session_1", "session_2", "session_3"]
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="533661",
|
||||
aadvid="1648829117232140",
|
||||
auth_token="sessionid=session_1",
|
||||
industry_id=20,
|
||||
brand_name="Brand1",
|
||||
),
|
||||
CookieConfig(
|
||||
brand_id="10186612",
|
||||
aadvid="9876543210",
|
||||
auth_token="sessionid=session_2",
|
||||
industry_id=30,
|
||||
brand_name="Brand2",
|
||||
),
|
||||
]
|
||||
|
||||
pool.remove("session_2")
|
||||
pool.remove_by_auth_token("sessionid=session_1")
|
||||
|
||||
assert pool.size == 2
|
||||
assert "session_2" not in pool._sessions
|
||||
assert pool.size == 1
|
||||
config = pool.get_random_config()
|
||||
assert config["auth_token"] == "sessionid=session_2"
|
||||
|
||||
def test_remove_session_compat(self):
|
||||
"""Test remove compatibility method."""
|
||||
pool = SessionPool()
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="533661",
|
||||
aadvid="1648829117232140",
|
||||
auth_token="sessionid=session_1",
|
||||
industry_id=20,
|
||||
brand_name="Brand1",
|
||||
),
|
||||
CookieConfig(
|
||||
brand_id="10186612",
|
||||
aadvid="9876543210",
|
||||
auth_token="sessionid=session_2",
|
||||
industry_id=30,
|
||||
brand_name="Brand2",
|
||||
),
|
||||
]
|
||||
|
||||
pool.remove("session_1")
|
||||
|
||||
assert pool.size == 1
|
||||
|
||||
def test_remove_nonexistent_session(self):
|
||||
"""Test removing a session that doesn't exist."""
|
||||
pool = SessionPool()
|
||||
pool._sessions = ["session_1"]
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="533661",
|
||||
aadvid="1648829117232140",
|
||||
auth_token="sessionid=session_1",
|
||||
industry_id=20,
|
||||
brand_name="Brand1",
|
||||
),
|
||||
]
|
||||
|
||||
# Should not raise
|
||||
pool.remove("nonexistent")
|
||||
pool.remove_by_auth_token("nonexistent")
|
||||
|
||||
assert pool.size == 1
|
||||
|
||||
@@ -188,7 +336,22 @@ class TestSessionPool:
|
||||
pool = SessionPool()
|
||||
assert pool.size == 0
|
||||
|
||||
pool._sessions = ["a", "b"]
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="123",
|
||||
aadvid="456",
|
||||
auth_token="sessionid=a",
|
||||
industry_id=20,
|
||||
brand_name="A",
|
||||
),
|
||||
CookieConfig(
|
||||
brand_id="789",
|
||||
aadvid="012",
|
||||
auth_token="sessionid=b",
|
||||
industry_id=30,
|
||||
brand_name="B",
|
||||
),
|
||||
]
|
||||
assert pool.size == 2
|
||||
|
||||
def test_is_empty_property(self):
|
||||
@@ -196,29 +359,117 @@ class TestSessionPool:
|
||||
pool = SessionPool()
|
||||
assert pool.is_empty is True
|
||||
|
||||
pool._sessions = ["a"]
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="123",
|
||||
aadvid="456",
|
||||
auth_token="sessionid=a",
|
||||
industry_id=20,
|
||||
brand_name="A",
|
||||
),
|
||||
]
|
||||
assert pool.is_empty is False
|
||||
|
||||
|
||||
class TestGetRandomConfig:
|
||||
"""Tests for get_random_config function (T-027)."""
|
||||
|
||||
async def test_get_config_success(self):
|
||||
"""Test successful config retrieval."""
|
||||
pool = SessionPool()
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="533661",
|
||||
aadvid="1648829117232140",
|
||||
auth_token="sessionid=session_1",
|
||||
industry_id=20,
|
||||
brand_name="Brand1",
|
||||
),
|
||||
]
|
||||
|
||||
with patch("app.services.session_pool.session_pool", pool):
|
||||
result = await get_random_config()
|
||||
|
||||
assert result is not None
|
||||
assert result["aadvid"] == "1648829117232140"
|
||||
assert result["auth_token"] == "sessionid=session_1"
|
||||
|
||||
async def test_get_config_refresh_on_empty(self):
|
||||
"""Test that pool is refreshed when empty."""
|
||||
pool = SessionPool()
|
||||
|
||||
with patch("app.services.session_pool.session_pool", pool):
|
||||
with patch.object(pool, "refresh") as mock_refresh:
|
||||
async def refresh_side_effect():
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="123",
|
||||
aadvid="456",
|
||||
auth_token="sessionid=new_session",
|
||||
industry_id=20,
|
||||
brand_name="New",
|
||||
),
|
||||
]
|
||||
return True
|
||||
|
||||
mock_refresh.side_effect = refresh_side_effect
|
||||
|
||||
result = await get_random_config()
|
||||
|
||||
assert mock_refresh.called
|
||||
assert result["auth_token"] == "sessionid=new_session"
|
||||
|
||||
async def test_get_config_retry_on_refresh_failure(self):
|
||||
"""Test retry behavior when refresh fails."""
|
||||
pool = SessionPool()
|
||||
|
||||
with patch("app.services.session_pool.session_pool", pool):
|
||||
with patch.object(pool, "refresh") as mock_refresh:
|
||||
mock_refresh.return_value = False
|
||||
|
||||
result = await get_random_config(max_retries=3)
|
||||
|
||||
assert result is None
|
||||
assert mock_refresh.call_count == 3
|
||||
|
||||
|
||||
class TestGetSessionWithRetry:
|
||||
"""Tests for get_session_with_retry function (T-022)."""
|
||||
"""Tests for get_session_with_retry function (T-022 compat)."""
|
||||
|
||||
async def test_get_session_success(self):
|
||||
"""Test successful session retrieval."""
|
||||
with patch.object(session_pool, "_sessions", ["session_1", "session_2"]):
|
||||
pool = SessionPool()
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="533661",
|
||||
aadvid="1648829117232140",
|
||||
auth_token="sessionid=session_1",
|
||||
industry_id=20,
|
||||
brand_name="Brand1",
|
||||
),
|
||||
]
|
||||
|
||||
with patch("app.services.session_pool.session_pool", pool):
|
||||
result = await get_session_with_retry()
|
||||
|
||||
assert result in ["session_1", "session_2"]
|
||||
assert result == "session_1"
|
||||
|
||||
async def test_get_session_refresh_on_empty(self):
|
||||
"""Test that pool is refreshed when empty."""
|
||||
with patch.object(session_pool, "_sessions", []):
|
||||
with patch.object(session_pool, "refresh") as mock_refresh:
|
||||
mock_refresh.return_value = True
|
||||
pool = SessionPool()
|
||||
|
||||
# After refresh, pool should have sessions
|
||||
with patch("app.services.session_pool.session_pool", pool):
|
||||
with patch.object(pool, "refresh") as mock_refresh:
|
||||
async def refresh_side_effect():
|
||||
session_pool._sessions.append("new_session")
|
||||
pool._configs = [
|
||||
CookieConfig(
|
||||
brand_id="123",
|
||||
aadvid="456",
|
||||
auth_token="sessionid=new_session",
|
||||
industry_id=20,
|
||||
brand_name="New",
|
||||
),
|
||||
]
|
||||
return True
|
||||
|
||||
mock_refresh.side_effect = refresh_side_effect
|
||||
@@ -230,55 +481,65 @@ class TestGetSessionWithRetry:
|
||||
|
||||
async def test_get_session_retry_on_refresh_failure(self):
|
||||
"""Test retry behavior when refresh fails."""
|
||||
original_sessions = session_pool._sessions.copy()
|
||||
pool = SessionPool()
|
||||
|
||||
try:
|
||||
session_pool._sessions = []
|
||||
|
||||
with patch.object(session_pool, "refresh") as mock_refresh:
|
||||
with patch("app.services.session_pool.session_pool", pool):
|
||||
with patch.object(pool, "refresh") as mock_refresh:
|
||||
mock_refresh.return_value = False
|
||||
|
||||
result = await get_session_with_retry(max_retries=3)
|
||||
|
||||
assert result is None
|
||||
assert mock_refresh.call_count == 3
|
||||
finally:
|
||||
session_pool._sessions = original_sessions
|
||||
|
||||
async def test_get_session_max_retries(self):
|
||||
"""Test max retries limit."""
|
||||
original_sessions = session_pool._sessions.copy()
|
||||
pool = SessionPool()
|
||||
|
||||
try:
|
||||
session_pool._sessions = []
|
||||
|
||||
with patch.object(session_pool, "refresh") as mock_refresh:
|
||||
with patch("app.services.session_pool.session_pool", pool):
|
||||
with patch.object(pool, "refresh") as mock_refresh:
|
||||
mock_refresh.return_value = False
|
||||
|
||||
result = await get_session_with_retry(max_retries=5)
|
||||
|
||||
assert result is None
|
||||
assert mock_refresh.call_count == 5
|
||||
finally:
|
||||
session_pool._sessions = original_sessions
|
||||
|
||||
|
||||
class TestSessionPoolIntegration:
|
||||
"""Integration tests for session pool."""
|
||||
|
||||
async def test_refresh_filters_invalid_items(self):
|
||||
"""Test that refresh filters out invalid items."""
|
||||
"""Test that refresh filters out invalid items (T-027 format)."""
|
||||
pool = SessionPool()
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"data": [
|
||||
{"sessionid": "valid_session"},
|
||||
{"no_sessionid": "missing"},
|
||||
{
|
||||
"brand_id": "533661",
|
||||
"aadvid": "1648829117232140",
|
||||
"auth_token": "sessionid=valid_session",
|
||||
"industry_id": 20,
|
||||
"brand_name": "Valid1",
|
||||
},
|
||||
{"no_auth_token": "missing"},
|
||||
None,
|
||||
{"sessionid": ""}, # Empty string should be filtered
|
||||
{"sessionid": "another_valid"},
|
||||
{
|
||||
"brand_id": "10186612",
|
||||
"aadvid": "", # Empty aadvid should be filtered
|
||||
"auth_token": "sessionid=xxx",
|
||||
"industry_id": 30,
|
||||
"brand_name": "Invalid",
|
||||
},
|
||||
{
|
||||
"brand_id": "789012",
|
||||
"aadvid": "9876543210",
|
||||
"auth_token": "sessionid=another_valid",
|
||||
"industry_id": 40,
|
||||
"brand_name": "Valid2",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -292,8 +553,6 @@ class TestSessionPoolIntegration:
|
||||
|
||||
assert result is True
|
||||
assert pool.size == 2
|
||||
assert "valid_session" in pool._sessions
|
||||
assert "another_valid" in pool._sessions
|
||||
|
||||
async def test_refresh_handles_non_dict_data(self):
|
||||
"""Test refresh with non-dict response."""
|
||||
|
||||
@@ -195,6 +195,13 @@ class TestGetVideoAnalysisData:
|
||||
|
||||
result = await get_video_analysis_data(mock_session, "video_123")
|
||||
|
||||
# T-027: 验证使用 industry_id 而不是 brand_id 调用 API
|
||||
mock_api.assert_called_once_with(
|
||||
item_id="video_123",
|
||||
publish_time=datetime(2025, 1, 15),
|
||||
industry_id="20",
|
||||
)
|
||||
|
||||
# 验证基础信息
|
||||
assert result["base_info"]["item_id"] == "video_123"
|
||||
assert result["base_info"]["title"] == "测试视频"
|
||||
@@ -249,6 +256,10 @@ class TestGetVideoAnalysisData:
|
||||
mock_video.after_view_search_uv = 1000
|
||||
mock_video.return_search_cnt = 50
|
||||
mock_video.estimated_video_cost = 10000
|
||||
mock_video.total_new_a3_cnt = 500
|
||||
mock_video.heated_new_a3_cnt = 100
|
||||
mock_video.natural_new_a3_cnt = 400
|
||||
mock_video.total_cost = 10000
|
||||
|
||||
# Mock session
|
||||
mock_session = AsyncMock()
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
"""
|
||||
Tests for Yuntu API Service (T-023)
|
||||
Tests for Yuntu API Service (T-023, T-027)
|
||||
|
||||
T-027 更新:
|
||||
- call_yuntu_api 参数改为 auth_token(完整 cookie 值)
|
||||
- 日期格式改为 YYYYMMDD
|
||||
- industry_id 改为字符串
|
||||
"""
|
||||
|
||||
import pytest
|
||||
@@ -24,11 +29,11 @@ class TestCallYuntuAPI:
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"status": 0,
|
||||
"msg": "ok",
|
||||
"data": {
|
||||
"total_show_cnt": 100000,
|
||||
"a3_increase_cnt": 500,
|
||||
"a3_increase_cnt": "500",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -42,17 +47,18 @@ class TestCallYuntuAPI:
|
||||
item_id="test_item_123",
|
||||
publish_time=datetime(2025, 1, 1),
|
||||
industry_id="20",
|
||||
session_id="test_session",
|
||||
aadvid="1648829117232140",
|
||||
auth_token="sessionid=test_session",
|
||||
)
|
||||
|
||||
assert result["code"] == 0
|
||||
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."""
|
||||
"""Test that API is called with correct parameters (T-027 format)."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"code": 0, "data": {}}
|
||||
mock_response.json.return_value = {"status": 0, "data": {}}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.post.return_value = mock_response
|
||||
@@ -64,26 +70,27 @@ class TestCallYuntuAPI:
|
||||
item_id="video_001",
|
||||
publish_time=datetime(2025, 1, 15),
|
||||
industry_id="30",
|
||||
session_id="session_abc",
|
||||
aadvid="1648829117232140",
|
||||
auth_token="sessionid=session_abc",
|
||||
)
|
||||
|
||||
mock_client.post.assert_called_once()
|
||||
call_args = mock_client.post.call_args
|
||||
|
||||
# 验证URL
|
||||
# 验证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"] == "2025-01-15"
|
||||
assert json_data["end_date"] == "2025-02-14" # +30天
|
||||
assert json_data["industry_id_list"] == ["30"]
|
||||
assert json_data["start_date"] == "20250115" # YYYYMMDD
|
||||
assert json_data["end_date"] == "20250214" # +30天
|
||||
assert json_data["industry_id_list"] == ["30"] # 字符串数组
|
||||
|
||||
# 验证headers包含sessionid
|
||||
# 验证headers - T-027: 直接使用 auth_token
|
||||
headers = call_args.kwargs["headers"]
|
||||
assert "Cookie" in headers
|
||||
assert "sessionid=session_abc" in headers["Cookie"]
|
||||
assert headers["Cookie"] == "sessionid=session_abc"
|
||||
|
||||
async def test_call_session_invalid_401(self):
|
||||
"""Test handling of 401 response (session invalid)."""
|
||||
@@ -101,7 +108,8 @@ class TestCallYuntuAPI:
|
||||
item_id="test",
|
||||
publish_time=datetime.now(),
|
||||
industry_id="20",
|
||||
session_id="invalid_session",
|
||||
aadvid="123",
|
||||
auth_token="sessionid=invalid_session",
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
@@ -122,7 +130,8 @@ class TestCallYuntuAPI:
|
||||
item_id="test",
|
||||
publish_time=datetime.now(),
|
||||
industry_id="20",
|
||||
session_id="invalid_session",
|
||||
aadvid="123",
|
||||
auth_token="sessionid=invalid_session",
|
||||
)
|
||||
|
||||
async def test_call_api_error_500(self):
|
||||
@@ -142,18 +151,19 @@ class TestCallYuntuAPI:
|
||||
item_id="test",
|
||||
publish_time=datetime.now(),
|
||||
industry_id="20",
|
||||
session_id="session",
|
||||
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 (code != 0)."""
|
||||
"""Test handling of business error (status != 0)."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"code": 1001,
|
||||
"message": "Invalid parameter",
|
||||
"status": 1001,
|
||||
"msg": "Invalid parameter",
|
||||
}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
@@ -167,7 +177,8 @@ class TestCallYuntuAPI:
|
||||
item_id="test",
|
||||
publish_time=datetime.now(),
|
||||
industry_id="20",
|
||||
session_id="session",
|
||||
aadvid="123",
|
||||
auth_token="sessionid=session",
|
||||
)
|
||||
|
||||
assert "Invalid parameter" in exc_info.value.message
|
||||
@@ -185,7 +196,8 @@ class TestCallYuntuAPI:
|
||||
item_id="test",
|
||||
publish_time=datetime.now(),
|
||||
industry_id="20",
|
||||
session_id="session",
|
||||
aadvid="123",
|
||||
auth_token="sessionid=session",
|
||||
)
|
||||
|
||||
assert "timeout" in exc_info.value.message.lower()
|
||||
@@ -203,62 +215,24 @@ class TestCallYuntuAPI:
|
||||
item_id="test",
|
||||
publish_time=datetime.now(),
|
||||
industry_id="20",
|
||||
session_id="session",
|
||||
aadvid="123",
|
||||
auth_token="sessionid=session",
|
||||
)
|
||||
|
||||
async def test_call_without_session_id(self):
|
||||
"""Test API call without providing session_id (gets from pool)."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"code": 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):
|
||||
with patch(
|
||||
"app.services.yuntu_api.get_session_with_retry"
|
||||
) as mock_get_session:
|
||||
mock_get_session.return_value = "pool_session"
|
||||
|
||||
result = await call_yuntu_api(
|
||||
item_id="test",
|
||||
publish_time=datetime.now(),
|
||||
industry_id="20",
|
||||
)
|
||||
|
||||
assert result["code"] == 0
|
||||
mock_get_session.assert_called_once()
|
||||
|
||||
async def test_call_no_session_available(self):
|
||||
"""Test API call when no session is available."""
|
||||
with patch(
|
||||
"app.services.yuntu_api.get_session_with_retry"
|
||||
) as mock_get_session:
|
||||
mock_get_session.return_value = None
|
||||
|
||||
with pytest.raises(YuntuAPIError) as exc_info:
|
||||
await call_yuntu_api(
|
||||
item_id="test",
|
||||
publish_time=datetime.now(),
|
||||
industry_id="20",
|
||||
)
|
||||
|
||||
assert "session" in exc_info.value.message.lower()
|
||||
|
||||
|
||||
class TestGetVideoAnalysis:
|
||||
"""Tests for get_video_analysis function with retry logic (T-022)."""
|
||||
"""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_session_with_retry") as mock_session:
|
||||
mock_session.return_value = "valid_session"
|
||||
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 = {"code": 0, "data": {"a3_increase_cnt": 100}}
|
||||
mock_call.return_value = {"status": 0, "data": {"a3_increase_cnt": "100"}}
|
||||
|
||||
result = await get_video_analysis(
|
||||
item_id="test",
|
||||
@@ -266,20 +240,24 @@ class TestGetVideoAnalysis:
|
||||
industry_id="20",
|
||||
)
|
||||
|
||||
assert result["data"]["a3_increase_cnt"] == 100
|
||||
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_session_with_retry") as mock_session:
|
||||
mock_session.side_effect = ["session_1", "session_2", "session_3"]
|
||||
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"),
|
||||
{"code": 0, "data": {}},
|
||||
{"status": 0, "data": {}},
|
||||
]
|
||||
|
||||
with patch("app.services.yuntu_api.session_pool") as mock_pool:
|
||||
@@ -290,15 +268,15 @@ class TestGetVideoAnalysis:
|
||||
max_retries=3,
|
||||
)
|
||||
|
||||
assert result["code"] == 0
|
||||
assert result["status"] == 0
|
||||
assert mock_call.call_count == 3
|
||||
# 验证失效的session被移除
|
||||
assert mock_pool.remove.call_count == 2
|
||||
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_session_with_retry") as mock_session:
|
||||
mock_session.return_value = "session"
|
||||
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")
|
||||
@@ -316,8 +294,8 @@ class TestGetVideoAnalysis:
|
||||
|
||||
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_session_with_retry") as mock_session:
|
||||
mock_session.return_value = "session"
|
||||
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)
|
||||
@@ -332,10 +310,10 @@ class TestGetVideoAnalysis:
|
||||
assert mock_call.call_count == 1
|
||||
assert exc_info.value.status_code == 500
|
||||
|
||||
async def test_no_session_available(self):
|
||||
"""Test error when no session is available."""
|
||||
with patch("app.services.yuntu_api.get_session_with_retry") as mock_session:
|
||||
mock_session.return_value = None
|
||||
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(
|
||||
@@ -349,7 +327,7 @@ class TestParseAnalysisResponse:
|
||||
"""Tests for parse_analysis_response function."""
|
||||
|
||||
def test_parse_complete_response(self):
|
||||
"""Test parsing complete response data."""
|
||||
"""Test parsing complete response data (T-027: handles string values)."""
|
||||
response = {
|
||||
"data": {
|
||||
"total_show_cnt": 100000,
|
||||
@@ -359,17 +337,17 @@ class TestParseAnalysisResponse:
|
||||
"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,
|
||||
"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.5,
|
||||
"cost": 10000,
|
||||
"natural_cost": 0,
|
||||
"ad_cost": 10000.5,
|
||||
"ad_cost": 10000,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,9 +355,11 @@ class TestParseAnalysisResponse:
|
||||
|
||||
assert result["total_show_cnt"] == 100000
|
||||
assert result["natural_show_cnt"] == 80000
|
||||
assert result["a3_increase_cnt"] == 500
|
||||
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.5
|
||||
assert result["cost"] == 10000
|
||||
|
||||
def test_parse_empty_response(self):
|
||||
"""Test parsing empty response."""
|
||||
@@ -404,7 +384,7 @@ class TestParseAnalysisResponse:
|
||||
response = {
|
||||
"data": {
|
||||
"total_show_cnt": 50000,
|
||||
"a3_increase_cnt": 100,
|
||||
"a3_increase_cnt": "100",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,3 +394,21 @@ class TestParseAnalysisResponse:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user