"""Offline tests для DomClick cookie-injection admin-эндпоинтов (#2000 MVP). Покрытие 2 эндпоинтов (db/BrowserFetcher.fetch мокаются, NO live network/DB), зеркалит паттерн test_scraper_admin_apis.py (dependency_overrides[get_db] + TestClient): - POST /api/v1/admin/scrape/domclick/upload-cookies - POST /api/v1/admin/scrape/domclick/debug/detail-fetch """ from __future__ import annotations import os os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from scraper_kit.providers.domclick.detail import DomClickDetailEnrichment @pytest.fixture def client() -> TestClient: from app.api.v1 import admin as admin_module from app.core.db import get_db app = FastAPI() app.include_router(admin_module.router, prefix="/api/v1/admin") def fake_db(): yield MagicMock() app.dependency_overrides[get_db] = fake_db return TestClient(app) _UPLOAD_URL = "/api/v1/admin/scrape/domclick/upload-cookies" _DEBUG_URL = "/api/v1/admin/scrape/domclick/debug/detail-fetch" # ── POST /scrape/domclick/upload-cookies ────────────────────────────────────── def test_upload_cookies_503_when_no_encryption_key(client: TestClient) -> None: with patch("app.api.v1.admin.settings.cookie_encryption_key", ""): resp = client.post(_UPLOAD_URL, json={"CAS_ID": "12345"}) assert resp.status_code == 503 def test_upload_cookies_400_when_no_recognized_cookies(client: TestClient) -> None: with patch("app.api.v1.admin.settings.cookie_encryption_key", "test-key"): resp = client.post(_UPLOAD_URL, json={"unrelated_cookie": "x"}) assert resp.status_code == 400 assert "No recognized DomClick cookies" in resp.json()["detail"] def test_upload_cookies_400_when_cas_id_missing(client: TestClient) -> None: with patch("app.api.v1.admin.settings.cookie_encryption_key", "test-key"): resp = client.post(_UPLOAD_URL, json={"qrator_jsid2": "abc"}) assert resp.status_code == 400 assert "CAS_ID" in resp.json()["detail"] def test_upload_cookies_400_when_cas_id_non_numeric(client: TestClient) -> None: with patch("app.api.v1.admin.settings.cookie_encryption_key", "test-key"): resp = client.post(_UPLOAD_URL, json={"CAS_ID": "not-a-number", "qrator_jsid2": "abc"}) assert resp.status_code == 400 assert "not numeric" in resp.json()["detail"] def test_upload_cookies_success_filters_and_calls_save_session(client: TestClient) -> None: with ( patch("app.api.v1.admin.settings.cookie_encryption_key", "test-key"), patch("app.api.v1.admin.domclick_session_svc.save_session") as mock_save, ): resp = client.post( _UPLOAD_URL, json={"CAS_ID": "12345", "qrator_jsid2": "abc", "unrelated_field": "y"}, ) assert resp.status_code == 200 assert resp.json() == {"ok": True, "accountCasId": 12345, "cookieCount": 2} mock_save.assert_called_once() _, kwargs = mock_save.call_args assert kwargs["account_cas_id"] == 12345 assert kwargs["cookies"] == {"CAS_ID": "12345", "qrator_jsid2": "abc"} assert "unrelated_field" not in kwargs["cookies"] # ── POST /scrape/domclick/debug/detail-fetch ────────────────────────────────── _CARD_URL = "https://ekaterinburg.domclick.ru/card/sale__flat__2075729321" def test_debug_detail_fetch_400_when_host_not_allowed(client: TestClient) -> None: """SSRF guard: хост не *.domclick.ru → 400, никакого fetch не происходит.""" resp = client.post(_DEBUG_URL, json={"card_url": "https://evil.example.com/x"}) assert resp.status_code == 400 def test_debug_detail_fetch_success_without_session_cookies(client: TestClient) -> None: """load_session()→None (нет сохранённой сессии) → fetch без инъекции, cookies_injected=False.""" enrichment = DomClickDetailEnrichment( item_id="2075729321", source_url=_CARD_URL, repair_state="good", living_area_m2=45.5, year_built=2015, price_changes=[{"change_time": "x", "price_rub": 5_000_000}], raw_extra={"wall_type": "brick"}, ) with ( patch("app.api.v1.admin.domclick_session_svc.load_session", return_value=None), patch( "scraper_kit.providers.domclick.detail.fetch_detail", new=AsyncMock(return_value=enrichment), ) as mock_fetch, ): resp = client.post(_DEBUG_URL, json={"card_url": _CARD_URL}) assert resp.status_code == 200 body = resp.json() assert body["ok"] is True assert body["cookies_injected"] is False assert body["item_id"] == "2075729321" assert body["repair_state"] == "good" assert body["price_changes_count"] == 1 assert body["raw_extra"] == {"wall_type": "brick"} mock_fetch.assert_called_once() _, kwargs = mock_fetch.call_args assert kwargs["cookies"] is None def test_debug_detail_fetch_success_with_session_cookies(client: TestClient) -> None: """load_session() возвращает сессию → cookies_injected=True, cookies проброшены в fetch_detail. Не завязан на конкретные HTML/enrichment-детали — cookies-проброс проверяется напрямую через call_args мока fetch_detail. """ session_cookies = {"CAS_ID": "12345", "qrator_jsid2": "abc"} enrichment = DomClickDetailEnrichment(item_id="2075729321", source_url=_CARD_URL) with ( patch( "app.api.v1.admin.domclick_session_svc.load_session", return_value=session_cookies, ), patch( "scraper_kit.providers.domclick.detail.fetch_detail", new=AsyncMock(return_value=enrichment), ) as mock_fetch, ): resp = client.post(_DEBUG_URL, json={"card_url": _CARD_URL}) assert resp.status_code == 200 assert resp.json()["cookies_injected"] is True mock_fetch.assert_called_once() _, kwargs = mock_fetch.call_args assert kwargs["cookies"] == session_cookies def test_debug_detail_fetch_502_on_fetch_failure(client: TestClient) -> None: with ( patch("app.api.v1.admin.domclick_session_svc.load_session", return_value=None), patch( "scraper_kit.providers.domclick.detail.fetch_detail", new=AsyncMock(side_effect=RuntimeError("blocked")), ), ): resp = client.post(_DEBUG_URL, json={"card_url": _CARD_URL}) assert resp.status_code == 502