"""다중 소스 검색 핸들러 테스트 — 병합·소스별 실패 격리·전체 실패 시 잡 실패 (fake 어댑터).""" import pytest from common.enums import JobType from services.search.contract import NormalizedProduct, AdapterError from worker.handlers import build_search_handler class FakeAdapter: def __init__(self, source, products=None, fail=False): self.source = source self._products = products or [] self._fail = fail async def search(self, query, limit=40): if self._fail: raise AdapterError("boom", source=self.source, blocked=True) return self._products def _np(source, price): return NormalizedProduct(source=source, name=f"{source}-{price}", price=price) def _job(): return {"job_type": JobType.SEARCH.value, "attempts": 1, "payload": {"product_name": "x"}} async def test_merges_and_ranks_across_sources(): adapters = { "coupang": FakeAdapter("coupang", [_np("coupang", 3000), _np("coupang", 1000)]), "naver": FakeAdapter("naver", [_np("naver", 2000), _np("naver", 500)]), } r = await build_search_handler(adapters, top_n=3)(_job()) assert r["lowest"]["price"] == 500 and r["lowest"]["source"] == "naver" assert [p["price"] for p in r["top"]] == [500, 1000, 2000] assert r["sources"]["coupang"]["count"] == 2 and r["sources"]["naver"]["count"] == 2 async def test_isolates_single_source_failure(): adapters = { "coupang": FakeAdapter("coupang", fail=True), "naver": FakeAdapter("naver", [_np("naver", 900)]), } r = await build_search_handler(adapters)(_job()) assert "error" in r["sources"]["coupang"] # 실패 격리 assert r["sources"]["naver"]["count"] == 1 assert r["lowest"]["price"] == 900 # 성공 소스로 결과 산출 async def test_all_sources_fail_raises(): adapters = { "coupang": FakeAdapter("coupang", fail=True), "naver": FakeAdapter("naver", fail=True), } with pytest.raises(RuntimeError): await build_search_handler(adapters)(_job())