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

288 lines
12 KiB
Python
Raw Permalink 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.

import pytest
import asyncio
from unittest.mock import AsyncMock, patch, MagicMock
import httpx
from app.services.brand_api import get_brand_names, fetch_brand_name
class TestBrandAPI:
"""Tests for Brand API integration."""
async def test_get_brand_names_success(self):
"""Test successful brand name fetching."""
with patch("app.services.brand_api.fetch_brand_name") as mock_fetch:
mock_fetch.side_effect = [
("brand_001", "品牌A"),
("brand_002", "品牌B"),
]
result = await get_brand_names(["brand_001", "brand_002"])
assert result["brand_001"] == "品牌A"
assert result["brand_002"] == "品牌B"
async def test_get_brand_names_empty_list(self):
"""Test with empty brand ID list."""
result = await get_brand_names([])
assert result == {}
async def test_get_brand_names_with_none_values(self):
"""Test filtering out None values."""
with patch("app.services.brand_api.fetch_brand_name") as mock_fetch:
mock_fetch.return_value = ("brand_001", "品牌A")
result = await get_brand_names(["brand_001", None, ""])
assert "brand_001" in result
assert len(result) == 1
async def test_get_brand_names_deduplication(self):
"""Test that duplicate brand IDs are deduplicated."""
with patch("app.services.brand_api.fetch_brand_name") as mock_fetch:
mock_fetch.return_value = ("brand_001", "品牌A")
result = await get_brand_names(["brand_001", "brand_001", "brand_001"])
# Should only call once due to deduplication
assert mock_fetch.call_count == 1
async def test_get_brand_names_partial_failure(self):
"""Test that partial failures don't break the whole batch."""
with patch("app.services.brand_api.fetch_brand_name") as mock_fetch:
mock_fetch.side_effect = [
("brand_001", "品牌A"),
("brand_002", "brand_002"), # Fallback to ID
("brand_003", "品牌C"),
]
result = await get_brand_names(["brand_001", "brand_002", "brand_003"])
assert result["brand_001"] == "品牌A"
assert result["brand_002"] == "brand_002" # Fallback
assert result["brand_003"] == "品牌C"
async def test_fetch_brand_name_success(self):
"""Test successful single brand fetch via get_brand_names."""
# 使用更高层的 mock测试整个流程
with patch("app.services.brand_api.fetch_brand_name") as mock_fetch:
mock_fetch.return_value = ("test_id", "测试品牌")
result = await get_brand_names(["test_id"])
assert result["test_id"] == "测试品牌"
async def test_fetch_brand_name_failure(self):
"""Test brand fetch failure returns ID as fallback."""
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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("test_id", semaphore)
assert brand_id == "test_id"
assert brand_name == "test_id" # Fallback to ID
async def test_fetch_brand_name_404(self):
"""Test brand fetch with 404 returns ID as fallback."""
mock_response = AsyncMock()
mock_response.status_code = 404
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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("nonexistent", semaphore)
assert brand_id == "nonexistent"
assert brand_name == "nonexistent"
async def test_concurrency_limit(self):
"""Test that concurrency is limited."""
with patch("app.services.brand_api.fetch_brand_name") as mock_fetch:
# 创建 15 个品牌 ID
brand_ids = [f"brand_{i:03d}" for i in range(15)]
mock_fetch.side_effect = [(id, f"名称_{id}") for id in brand_ids]
result = await get_brand_names(brand_ids)
assert len(result) == 15
# 验证所有调用都完成了
assert mock_fetch.call_count == 15
async def test_fetch_brand_name_200_with_array_data(self):
"""Test successful brand fetch with array data structure (T-019 fix)."""
# 正确的API响应格式: data是数组从data[0].brand_name获取品牌名称
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"total": 1,
"last_updated": "2025-12-30T11:28:40.738185",
"has_more": 0,
"data": [
{
"industry_id": 20,
"industry_name": "母婴",
"brand_id": 533661,
"brand_name": "Giving/启初"
}
]
}
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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("533661", semaphore)
assert brand_id == "533661"
assert brand_name == "Giving/启初"
async def test_fetch_brand_name_200_with_empty_data_array(self):
"""Test brand fetch with 200 but empty data array (T-019 edge case)."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"total": 0,
"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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("unknown_brand", semaphore)
assert brand_id == "unknown_brand"
assert brand_name == "unknown_brand" # Fallback
async def test_fetch_brand_name_200_no_brand_name_field(self):
"""Test brand fetch with 200 but no brand_name in data item."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"total": 1,
"data": [{"brand_id": 123}] # No brand_name field
}
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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("brand_no_name", semaphore)
assert brand_id == "brand_no_name"
assert brand_name == "brand_no_name" # Fallback
async def test_fetch_brand_name_with_auth_header(self):
"""Test that Authorization header is sent (T-020)."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"total": 1,
"data": [{"brand_id": 123, "brand_name": "测试品牌"}]
}
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.brand_api.settings") as mock_settings:
mock_settings.BRAND_API_TIMEOUT = 3.0
mock_settings.BRAND_API_BASE_URL = "https://api.test.com"
mock_settings.BRAND_API_TOKEN = "test_token_123"
semaphore = asyncio.Semaphore(10)
await fetch_brand_name("123", semaphore)
# 验证请求包含 Authorization header
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_123"
async def test_fetch_brand_name_request_error(self):
"""Test brand fetch with request error."""
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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("brand_error", semaphore)
assert brand_id == "brand_error"
assert brand_name == "brand_error" # Fallback
async def test_fetch_brand_name_unexpected_error(self):
"""Test brand fetch with unexpected error."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.side_effect = ValueError("Invalid JSON")
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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("brand_json_error", semaphore)
assert brand_id == "brand_json_error"
assert brand_name == "brand_json_error" # Fallback
async def test_get_brand_names_with_exception_in_gather(self):
"""Test that exceptions in asyncio.gather are handled."""
with patch("app.services.brand_api.fetch_brand_name") as mock_fetch:
# 第二个调用抛出异常
mock_fetch.side_effect = [
("brand_001", "品牌A"),
Exception("Unexpected error"),
("brand_003", "品牌C"),
]
result = await get_brand_names(["brand_001", "brand_002", "brand_003"])
# 成功的应该在结果中
assert result["brand_001"] == "品牌A"
assert result["brand_003"] == "品牌C"
# 失败的不应该在结果中(因为是 Exception不是 tuple
assert "brand_002" not in result
async def test_fetch_brand_name_non_dict_response(self):
"""Test brand fetch with non-dict response."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = ["not", "a", "dict"] # Array instead of 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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("brand_array", semaphore)
assert brand_id == "brand_array"
assert brand_name == "brand_array" # Fallback because response is not dict