gendesign/tradein-mvp/backend/tests/test_domclick_admin_apis.py
lekss361 ced616d150
All checks were successful
Deploy Trade-In / test (push) Successful in 49s
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 2m13s
Deploy Trade-In / build-backend (push) Successful in 1m32s
Deploy Trade-In / deploy (push) Successful in 7m16s
feat(tradein/domclick): cookie-injection MVP для обхода QRATOR-блока (#2000) (#2433)
2026-07-04 17:49:40 +00:00

174 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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