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