kol-insight/backend/tests/test_session_pool.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

574 lines
18 KiB
Python

"""
Tests for SessionID Pool Service (T-021, T-022, T-027)
T-027 更新:
- 改为 CookieConfig 数据结构
- get_random_config() 随机选取配置
- remove_by_auth_token() 移除失效配置
"""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
import httpx
from app.services.session_pool import (
SessionPool,
CookieConfig,
session_pool,
get_session_with_retry,
get_random_config,
)
class TestSessionPool:
"""Tests for SessionPool class."""
async def test_refresh_success(self):
"""Test successful session pool refresh (T-027 format)."""
pool = SessionPool()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": [
{
"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",
},
]
}
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 == 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()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": []}
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 False
assert pool.size == 0
async def test_refresh_api_error(self):
"""Test refresh with API error."""
pool = SessionPool()
mock_response = MagicMock()
mock_response.status_code = 500
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 False
async def test_refresh_timeout(self):
"""Test refresh with timeout."""
pool = SessionPool()
mock_client = AsyncMock()
mock_client.get.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):
result = await pool.refresh()
assert result is False
async def test_refresh_request_error(self):
"""Test refresh with request error."""
pool = SessionPool()
mock_client = AsyncMock()
mock_client.get.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):
result = await pool.refresh()
assert result is False
async def test_refresh_unexpected_error(self):
"""Test refresh with unexpected error."""
pool = SessionPool()
mock_client = AsyncMock()
mock_client.get.side_effect = ValueError("Unexpected")
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 False
async def test_refresh_with_auth_header(self):
"""Test that refresh includes Authorization header."""
pool = SessionPool()
mock_response = MagicMock()
mock_response.status_code = 200
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
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.session_pool.settings") as mock_settings:
mock_settings.YUNTU_API_TOKEN = "test_token"
mock_settings.YUNTU_API_TIMEOUT = 10.0
mock_settings.BRAND_API_BASE_URL = "https://api.test.com"
await pool.refresh()
mock_client.get.assert_called_once()
call_args = mock_client.get.call_args
assert "headers" in call_args.kwargs
assert call_args.kwargs["headers"]["Authorization"] == "Bearer test_token"
def test_get_random_config_from_pool(self):
"""Test getting random config from pool (T-027)."""
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",
),
]
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 == "session_1"
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_by_auth_token(self):
"""Test removing config by auth_token (T-027)."""
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_by_auth_token("sessionid=session_1")
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._configs = [
CookieConfig(
brand_id="533661",
aadvid="1648829117232140",
auth_token="sessionid=session_1",
industry_id=20,
brand_name="Brand1",
),
]
# Should not raise
pool.remove_by_auth_token("nonexistent")
assert pool.size == 1
def test_size_property(self):
"""Test size property."""
pool = SessionPool()
assert pool.size == 0
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):
"""Test is_empty property."""
pool = SessionPool()
assert pool.is_empty is True
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 compat)."""
async def test_get_session_success(self):
"""Test successful session 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_session_with_retry()
assert result == "session_1"
async def test_get_session_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_session_with_retry()
assert mock_refresh.called
assert result == "new_session"
async def test_get_session_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_session_with_retry(max_retries=3)
assert result is None
assert mock_refresh.call_count == 3
async def test_get_session_max_retries(self):
"""Test max retries limit."""
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_session_with_retry(max_retries=5)
assert result is None
assert mock_refresh.call_count == 5
class TestSessionPoolIntegration:
"""Integration tests for session pool."""
async def test_refresh_filters_invalid_items(self):
"""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": [
{
"brand_id": "533661",
"aadvid": "1648829117232140",
"auth_token": "sessionid=valid_session",
"industry_id": 20,
"brand_name": "Valid1",
},
{"no_auth_token": "missing"},
None,
{
"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",
},
]
}
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 == 2
async def test_refresh_handles_non_dict_data(self):
"""Test refresh with non-dict response."""
pool = SessionPool()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = ["not", "a", "dict"]
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 False