kol-insight/backend/tests/test_brand_api.py
zfc cdc364cb2a test: 提升测试覆盖率至 99%
- 新增 brand_api 测试: 覆盖所有异常处理分支
- 新增 main.py 测试: root 和 health 端点
- 新增 logging 测试: setup_logging 和 get_logger
- 新增 KolVideo __repr__ 测试
- 总计 67 个测试全部通过

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 14:44:54 +08:00

240 lines
9.8 KiB
Python
Raw 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_nested_data(self):
"""Test successful brand fetch with nested data structure."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": {"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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("brand_nested", semaphore)
assert brand_id == "brand_nested"
assert brand_name == "嵌套品牌名"
async def test_fetch_brand_name_200_with_flat_data(self):
"""Test successful brand fetch with flat data structure."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"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):
semaphore = asyncio.Semaphore(10)
brand_id, brand_name = await fetch_brand_name("brand_flat", semaphore)
assert brand_id == "brand_flat"
assert brand_name == "扁平品牌名"
async def test_fetch_brand_name_200_no_name(self):
"""Test brand fetch with 200 but no name in response."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"data": {"id": "123"}} # No 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_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