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