kol-insight/backend/tests/test_session_pool.py
zfc f123f68be3 feat(video-analysis): 完成视频分析模块迭代任务
Bug 修复:
- T-019: 修复品牌API响应解析,正确解析 data[0].brand_name
- T-020: 添加品牌API Bearer Token认证

视频分析功能:
- T-021: SessionID池服务,从内部API获取Cookie列表
- T-022: SessionID自动重试,失效时自动切换重试
- T-023: 巨量云图API封装,支持超时和错误处理
- T-024: 视频分析数据接口 GET /api/v1/videos/{item_id}/analysis
- T-025: 数据库A3指标更新
- T-026: 视频分析前端页面,展示6大类25+指标

测试覆盖率:
- brand_api.py: 100%
- session_pool.py: 100%
- yuntu_api.py: 100%
- video_analysis.py: 99%

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

315 lines
10 KiB
Python

"""
Tests for SessionID Pool Service (T-021, T-022)
"""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
import httpx
from app.services.session_pool import (
SessionPool,
session_pool,
get_session_with_retry,
)
class TestSessionPool:
"""Tests for SessionPool class."""
async def test_refresh_success(self):
"""Test successful session pool refresh."""
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"},
]
}
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 == 3
assert not pool.is_empty
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": [{"sessionid": "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_from_pool(self):
"""Test getting random session from pool."""
pool = SessionPool()
pool._sessions = ["session_1", "session_2", "session_3"]
session = pool.get_random()
assert session in pool._sessions
def test_get_random_from_empty_pool(self):
"""Test getting random session from empty pool."""
pool = SessionPool()
session = pool.get_random()
assert session is None
def test_remove_session(self):
"""Test removing a session from pool."""
pool = SessionPool()
pool._sessions = ["session_1", "session_2", "session_3"]
pool.remove("session_2")
assert pool.size == 2
assert "session_2" not in pool._sessions
def test_remove_nonexistent_session(self):
"""Test removing a session that doesn't exist."""
pool = SessionPool()
pool._sessions = ["session_1"]
# Should not raise
pool.remove("nonexistent")
assert pool.size == 1
def test_size_property(self):
"""Test size property."""
pool = SessionPool()
assert pool.size == 0
pool._sessions = ["a", "b"]
assert pool.size == 2
def test_is_empty_property(self):
"""Test is_empty property."""
pool = SessionPool()
assert pool.is_empty is True
pool._sessions = ["a"]
assert pool.is_empty is False
class TestGetSessionWithRetry:
"""Tests for get_session_with_retry function (T-022)."""
async def test_get_session_success(self):
"""Test successful session retrieval."""
with patch.object(session_pool, "_sessions", ["session_1", "session_2"]):
result = await get_session_with_retry()
assert result in ["session_1", "session_2"]
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
# After refresh, pool should have sessions
async def refresh_side_effect():
session_pool._sessions.append("new_session")
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."""
original_sessions = session_pool._sessions.copy()
try:
session_pool._sessions = []
with patch.object(session_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()
try:
session_pool._sessions = []
with patch.object(session_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."""
pool = SessionPool()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"data": [
{"sessionid": "valid_session"},
{"no_sessionid": "missing"},
None,
{"sessionid": ""}, # Empty string should be filtered
{"sessionid": "another_valid"},
]
}
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 "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."""
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