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:
zfc
2026-01-28 22:01:55 +08:00
co-authored by Claude Opus 4.5
parent f123f68be3
commit 7cd29c5980
25 changed files with 2482 additions and 1324 deletions
+311 -52
View File
@@ -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."""
+11
View File
@@ -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()
+93 -95
View File
@@ -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