From c9daf8e268a3f81f9c01bec25753f1f7ebb7338f Mon Sep 17 00:00:00 2001 From: bot-backend Date: Mon, 15 Jun 2026 18:41:08 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20yandex=20sweep=20=E2=80=94=20gu?= =?UTF-8?q?ard=20ext=5Fid=3DNULL=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20fetch?= =?UTF-8?q?=5Fjk=20+=20cleanup=20httpx=20(#974=20review=20M1/M3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M1: добавлен guard перед вызовом fetch_jk — если ext_id пустой/None при наличии jk_slug, логируем warning, incrementим failed_fetch и пропускаем (continue). Пустой jk_id формирует битый URL /{slug}-/ и коллизию на UNIQUE ext_id=''. M3: в admin.py /scrape/yandex-newbuilding заменён `async with YandexNewbuildingScraper()` на прямое `scraper = YandexNewbuildingScraper()` — fetch_jk использует внутренний BrowserFetcher, httpx-клиент BaseScraper открывался вхолостую. Тесты: добавлен test_sweep_slug_present_but_ext_id_null_skips_fetch. --- tradein-mvp/backend/app/api/v1/admin.py | 5 +-- .../app/tasks/yandex_newbuilding_sweep.py | 13 +++++++- .../tasks/test_yandex_newbuilding_sweep.py | 31 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/tradein-mvp/backend/app/api/v1/admin.py b/tradein-mvp/backend/app/api/v1/admin.py index e1044d81..46a4f178 100644 --- a/tradein-mvp/backend/app/api/v1/admin.py +++ b/tradein-mvp/backend/app/api/v1/admin.py @@ -1468,8 +1468,9 @@ async def scrape_yandex_newbuilding( URL: /{city}/kupit/novostrojka/-/ """ - async with YandexNewbuildingScraper() as scraper: - result = await scraper.fetch_jk(jk_slug=slug, jk_id=id, city=city) + # fetch_jk использует внутренний BrowserFetcher, httpx-клиент BaseScraper не нужен + scraper = YandexNewbuildingScraper() + result = await scraper.fetch_jk(jk_slug=slug, jk_id=id, city=city) if result is None: raise HTTPException(404, f"Could not parse Yandex JK: {slug}-{id} in {city}") return YandexNewbuildingTriggerResp( diff --git a/tradein-mvp/backend/app/tasks/yandex_newbuilding_sweep.py b/tradein-mvp/backend/app/tasks/yandex_newbuilding_sweep.py index 596c3587..c65ac1d1 100644 --- a/tradein-mvp/backend/app/tasks/yandex_newbuilding_sweep.py +++ b/tradein-mvp/backend/app/tasks/yandex_newbuilding_sweep.py @@ -312,11 +312,22 @@ async def enrich_yandex_newbuilding_sweep( jk_slug = resolved result.resolved_slug += 1 + # ── Guard: ext_id обязателен для fetch_jk (формирует URL /{slug}-{id}/) ── + if not ext_id: + logger.warning( + "house_id=%s: jk_slug=%s известен, но ext_id пустой — " + "fetch_jk пропущен (пустой jk_id даёт битый URL)", + house_id, + jk_slug, + ) + result.failed_fetch += 1 + continue + # ── Fetch через BrowserFetcher ──────────────────────────────────── info = None try: scraper = YandexNewbuildingScraper() - info = await scraper.fetch_jk(jk_slug=jk_slug, jk_id=ext_id or "", city=city) + info = await scraper.fetch_jk(jk_slug=jk_slug, jk_id=ext_id, city=city) except Exception as exc: logger.warning( "fetch_jk failed house_id=%s jk_slug=%s ext_id=%s: %s", diff --git a/tradein-mvp/backend/tests/tasks/test_yandex_newbuilding_sweep.py b/tradein-mvp/backend/tests/tasks/test_yandex_newbuilding_sweep.py index 0bbb5cef..a2e753cc 100644 --- a/tradein-mvp/backend/tests/tasks/test_yandex_newbuilding_sweep.py +++ b/tradein-mvp/backend/tests/tasks/test_yandex_newbuilding_sweep.py @@ -364,3 +364,34 @@ async def test_sweep_no_ext_id_fails_resolve(): assert result.failed_resolve == 1 assert result.succeeded == 0 + + +@pytest.mark.asyncio +async def test_sweep_slug_present_but_ext_id_null_skips_fetch(): + """House с jk_slug но ext_id=NULL → guard срабатывает, fetch_jk НЕ вызван. + + M1 review: пустой jk_id даёт URL /{slug}-/, который является битым. + Ожидаем failed_fetch += 1 и scraper.fetch_jk не вызван. + """ + db = FakeDB( + [ + { + "house_id": 70, + "ext_id": None, + "yandex_jk_slug": "tatlin", + "yandex_jk_id": None, + } + ] + ) + + with patch("app.services.scrapers.yandex_newbuilding.YandexNewbuildingScraper") as mock_scraper: + scraper_instance = MagicMock() + scraper_instance.fetch_jk = AsyncMock() + mock_scraper.return_value = scraper_instance + + result = await enrich_yandex_newbuilding_sweep(db, limit=5, request_delay_sec=0) + + scraper_instance.fetch_jk.assert_not_called() + assert result.failed_fetch == 1 + assert result.succeeded == 0 + assert result.rows_inserted == 0