From 19b60812427a8250252b1f09d5e9030838b0b0d9 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 17 Sep 2026 12:50:20 +0500 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC?= =?UTF-8?q?=20detail-=D0=B4=D0=BE=D0=B1=D0=BE=D1=80=D0=B0=20=D0=90=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D0=BE=20=D0=BE=D0=B4=D0=B8=D0=BD=D0=B0=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B2=D0=BE=20=D0=B2=D1=81=D0=B5=D1=85=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80=D0=B0=D1=85=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B7=D0=B0=20backend;=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D0=BC=D1=91=D1=80=D1=82=D0=B2=D0=B0=D1=8F?= =?UTF-8?q?=20YANDEX=5FCOOKIES=5FFILE=20(#3182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прод 17.09, printenv: AVITO_DETAIL_BACKFILL_USE_CURL=false у tradein-scraper и true у tradein-backend/tgbot/browser. Источник true — хостовый backend/.env.runtime:37; false задан только environment'ом сервиса scraper. tradein-backend исполняет добор сам (ручка админки scrape_avito_detail_backfill), то есть ручной прогон шёл curl'ом, который QRATOR PoW не проходит. - docker-compose.prod.yml: AVITO_DETAIL_BACKFILL_USE_CURL "false" у backend и tgbot — у всех сервисов образа backend одно значение (environment перекрывает env_file). tgbot добор не исполняет; строка там, чтобы правило «одно значение на образ» проверялось без списка исключений. - Остальные флаги режима проверены на проде: SCRAPER_FETCH_MODE=browser и AVITO_IMV_USE_BROWSER_FETCHER=true одинаковы во всех четырёх контейнерах. - yandex_cookies_file удалён из Settings: его не читал ни один модуль (только мок в test_scraper_proxy.py), файла /app/yandex_cookies.json нет ни в одном контейнере, куки Яндекса давно живут в БД (#3192). extra="ignore" держит старт при оставшейся переменной в .env.runtime. Тест по значению: разобранный compose — у каждого сервиса образа backend значение "false"; Settings с YANDEX_COOKIES_FILE в env стартует без поля. Фальсификация: без строки у backend — «{'backend': None} != {'backend': 'false'}»; с возвращённым полем — «assert not True». Co-Authored-By: Claude Opus 5 --- tradein-mvp/backend/app/core/config.py | 8 ---- .../test_3182_detail_transport_one_value.py | 45 +++++++++++++++++++ .../backend/tests/test_scraper_proxy.py | 6 +-- tradein-mvp/docker-compose.prod.yml | 7 +++ 4 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 tradein-mvp/backend/tests/test_3182_detail_transport_one_value.py diff --git a/tradein-mvp/backend/app/core/config.py b/tradein-mvp/backend/app/core/config.py index a6f5a5d8..46e1df2a 100644 --- a/tradein-mvp/backend/app/core/config.py +++ b/tradein-mvp/backend/app/core/config.py @@ -1194,14 +1194,6 @@ class Settings(BaseSettings): # False = старое поведение (без фильтра). ENV: AVITO_SERP_EKB_ONLY. avito_serp_ekb_only: bool = Field(default=True, validation_alias="AVITO_SERP_EKB_ONLY") - # ── Yandex SERP cookies (#801/T4) ─────────────────────────────────────── - # Путь к JSON-файлу с cookies браузера (формат: [{name, value, ...}, ...]). - # Если задан и файл существует — cookies передаются в curl_cffi-сессию при - # Yandex SERP-запросах; снижает вероятность captcha на datacenter IP. - # Пусто / файл не найден = запросы без cookies (не падаем, только warning). - # ENV: YANDEX_COOKIES_FILE. - yandex_cookies_file: str | None = None - # ── #639: Cian browser auto-login (Variant B) ──────────────────────────── # Провалидировано вживую 2026-05-31: email+пароль, без SMS/капчи. Флоу 2-шаговый # (после 1-го сабмита экран «Введите пароль» → повтор). Селекторы env-overridable. diff --git a/tradein-mvp/backend/tests/test_3182_detail_transport_one_value.py b/tradein-mvp/backend/tests/test_3182_detail_transport_one_value.py new file mode 100644 index 00000000..446a74dd --- /dev/null +++ b/tradein-mvp/backend/tests/test_3182_detail_transport_one_value.py @@ -0,0 +1,45 @@ +"""#3182: режим detail-добора Авито не зависит от того, какой контейнер взял задачу. + +Добор исполняют два процесса образа backend: планировщик (tradein-scraper) и ручка +админки scrape_avito_detail_backfill (tradein-backend). Хостовый backend/.env.runtime +задаёт AVITO_DETAIL_BACKFILL_USE_CURL=true, а environment сервиса перекрывает env_file — +до правки "false" стояло только у scraper, и printenv на проде давал scraper=false, +backend=true. Проверка по значению: разобранный compose, каждый сервис образа backend. +""" + +import os +from pathlib import Path + +os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") + +import pytest +import yaml + +from app.core.config import Settings + +_COMPOSE = Path(__file__).resolve().parents[2] / "docker-compose.prod.yml" + + +def test_every_backend_image_service_pins_browser_transport() -> None: + services = yaml.safe_load(_COMPOSE.read_text())["services"] + backend_image = { + name: svc + for name, svc in services.items() + if "gendesign-tradein-backend" in svc.get("image", "") + } + # Контроль разбора: без него пустой словарь дал бы зелёный тест ни о чём. + assert {"backend", "scraper"} <= set(backend_image), sorted(backend_image) + + values = { + name: (svc.get("environment") or {}).get("AVITO_DETAIL_BACKFILL_USE_CURL") + for name, svc in backend_image.items() + } + assert values == dict.fromkeys(backend_image, "false"), values + + +def test_leftover_yandex_cookies_env_is_ignored(monkeypatch: pytest.MonkeyPatch) -> None: + """YANDEX_COOKIES_FILE удалён из Settings (никто не читал); хостовый .env.runtime + его ещё несёт — старт не должен падать, а поле не должно всплыть обратно.""" + monkeypatch.setenv("YANDEX_COOKIES_FILE", "/app/yandex_cookies.json") + + assert not hasattr(Settings(_env_file=None), "yandex_cookies_file") diff --git a/tradein-mvp/backend/tests/test_scraper_proxy.py b/tradein-mvp/backend/tests/test_scraper_proxy.py index 165539d8..b202ffdc 100644 --- a/tradein-mvp/backend/tests/test_scraper_proxy.py +++ b/tradein-mvp/backend/tests/test_scraper_proxy.py @@ -40,10 +40,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -def _mock_settings( - scraper_proxy_url: str | None, - yandex_cookies_file: str | None = None, -) -> SimpleNamespace: +def _mock_settings(scraper_proxy_url: str | None) -> SimpleNamespace: """Minimal settings stand-in with fields read by proxy helpers and scraper __aenter__.""" return SimpleNamespace( scraper_proxy_url=scraper_proxy_url, @@ -52,7 +49,6 @@ def _mock_settings( # removed — dead mobileproxy accounts). Mirrored here for test compat. cian_proxy_url=scraper_proxy_url, yandex_proxy_url=scraper_proxy_url, - yandex_cookies_file=yandex_cookies_file, ) diff --git a/tradein-mvp/docker-compose.prod.yml b/tradein-mvp/docker-compose.prod.yml index 191dfd8a..5aa3ef03 100644 --- a/tradein-mvp/docker-compose.prod.yml +++ b/tradein-mvp/docker-compose.prod.yml @@ -340,6 +340,11 @@ services: # деградация. У scraper те же значения с #2126/#2160. USE_PROXY_POOL_CURL: "true" USE_PROXY_POOL_BROWSER: "true" + # #3182: ручка админки scrape_avito_detail_backfill исполняет добор В ЭТОМ + # процессе. Хостовый backend/.env.runtime задаёт true (curl не проходит QRATOR + # PoW, см. scraper ниже) — environment перекрывает env_file. Значение одно на + # все сервисы образа backend, чтобы режим не зависел от того, кто взял задачу. + AVITO_DETAIL_BACKFILL_USE_CURL: "false" depends_on: browser: condition: service_started @@ -453,6 +458,8 @@ services: # Settings()-объект образа где-то читает флаг при импорте (defense-in-depth, # аналогично backend). tgbot_main.py не должен зависеть от этого значения. SCHEDULER_ENABLE: "false" + # #3182: то же значение, что у backend/scraper (см. комментарий у backend). + AVITO_DETAIL_BACKFILL_USE_CURL: "false" depends_on: postgres: condition: service_healthy