gendesign/tradein-mvp/backend/tests/test_cian_detail.py
lekss361 2dd76ea5e8
Some checks failed
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 39s
Deploy Trade-In / deploy (push) Has been cancelled
feat(tradein): cian_session cookies management + admin endpoints (Wave 4 Worker A) (#457)
2026-05-23 13:22:00 +00:00

324 lines
10 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.

"""Tests for cian_detail.py — Stage 5 of CianScraper v1."""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from app.services.scrapers.cian_detail import (
DetailEnrichment,
_extract_agent_profile,
_extract_price_changes,
_parse_views,
fetch_detail,
)
# ── _parse_views ──────────────────────────────────────────────────────────────
def test_parse_views_space_formatted():
assert _parse_views("1 234") == 1234
def test_parse_views_plain():
assert _parse_views("567") == 567
def test_parse_views_none():
assert _parse_views(None) is None
def test_parse_views_invalid():
assert _parse_views("invalid") is None
def test_parse_views_comma_formatted():
assert _parse_views("1,234") == 1234
# ── _extract_price_changes ────────────────────────────────────────────────────
def test_extract_price_changes_normalizes():
offer = {
"priceChanges": [
{"changeTime": "2026-04-01T00:00:00Z", "price": 5000000, "diffPercent": -2.5},
{"changeTime": "2026-04-10T00:00:00Z", "price": 4900000, "diffPercent": -2.0},
]
}
result = _extract_price_changes(offer, {})
assert len(result) == 2
assert result[0]["change_time"] == "2026-04-01T00:00:00Z"
assert result[0]["price_rub"] == 5000000
assert result[0]["diff_percent"] == -2.5
def test_extract_price_changes_handles_empty_offer():
assert _extract_price_changes({}, {}) == []
def test_extract_price_changes_handles_none_value():
assert _extract_price_changes({"priceChanges": None}, {}) == []
def test_extract_price_changes_falls_back_to_state():
state = {
"priceChanges": [
{"changeTime": "2026-03-01T00:00:00Z", "price": 6000000, "diffPercent": 0},
]
}
result = _extract_price_changes({}, state)
assert len(result) == 1
assert result[0]["price_rub"] == 6000000
def test_extract_price_changes_skips_non_dict_entries():
offer = {"priceChanges": [{"changeTime": "2026-04-01T00:00:00Z", "price": 5000000}, "bad"]}
result = _extract_price_changes(offer, {})
assert len(result) == 1
# ── _extract_agent_profile ────────────────────────────────────────────────────
def test_extract_agent_profile_from_agent_key():
offer = {
"agent": {
"id": 12345,
"companyName": "Новосёл",
"isPro": True,
"skills": [{"id": 1, "name": "Продажа"}],
"accountType": "specialist",
}
}
profile = _extract_agent_profile(offer)
assert profile is not None
assert profile["ext_agent_id"] == 12345
assert profile["company_name"] == "Новосёл"
assert profile["is_pro"] is True
assert profile["account_type"] == "specialist"
def test_extract_agent_profile_from_user_key():
offer = {
"user": {
"cianUserId": 99999,
"companyName": "Агентство Икс",
"isPro": False,
"accountType": "agency",
}
}
profile = _extract_agent_profile(offer)
assert profile is not None
assert profile["ext_agent_id"] == 99999
assert profile["company_name"] == "Агентство Икс"
def test_extract_agent_profile_returns_none_when_no_agent():
assert _extract_agent_profile({}) is None
assert _extract_agent_profile({"agent": {}, "user": {}}) is None
# ── DetailEnrichment defaults ─────────────────────────────────────────────────
def test_dataclass_defaults():
enrich = DetailEnrichment()
assert enrich.cian_id is None
assert enrich.price_changes == []
assert enrich.raw_sister_states == {}
assert enrich.bti_data is None
assert enrich.agent_profile is None
assert enrich.newbuilding_data is None
# ── fetch_detail integration (mocked) ────────────────────────────────────────
@pytest.mark.asyncio
async def test_fetch_detail_returns_none_on_no_state(monkeypatch):
"""If state extraction returns None → fetch_detail returns None gracefully."""
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_state",
lambda html, mfe, key: None,
)
response = MagicMock()
response.status_code = 200
response.text = "<html></html>"
session = MagicMock()
session.get = AsyncMock(return_value=response)
session.close = AsyncMock()
result = await fetch_detail("https://ekb.cian.ru/sale/flat/123/", session=session)
assert result is None
@pytest.mark.asyncio
async def test_fetch_detail_returns_none_on_non_200(monkeypatch):
"""Non-200 HTTP response → returns None, no exception raised."""
response = MagicMock()
response.status_code = 403
response.text = ""
session = MagicMock()
session.get = AsyncMock(return_value=response)
session.close = AsyncMock()
result = await fetch_detail("https://ekb.cian.ru/sale/flat/456/", session=session)
assert result is None
@pytest.mark.asyncio
async def test_fetch_detail_extracts_core_fields(monkeypatch):
"""Verify core fields are extracted correctly from mocked state."""
fake_state = {
"offerData": {
"offer": {
"cianId": 327830237,
"windowsViewType": "yardAndStreet",
"separateWcsCount": 1,
"combinedWcsCount": 0,
"ceilingHeight": 2.65,
"repairType": "cosmetic",
"priceChanges": [
{
"changeTime": "2026-04-01T00:00:00Z",
"price": 5500000,
"diffPercent": -1.8,
}
],
"agent": {
"id": 7654321,
"companyName": "Первичка Плюс",
"isPro": True,
"skills": [],
"accountType": "specialist",
},
},
"stats": {
"totalViewsFormattedString": "1 042",
"todayViewsFormattedString": "7",
},
}
}
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_state",
lambda html, mfe, key: fake_state if key == "defaultState" else None,
)
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_all_states",
lambda html: {},
)
response = MagicMock()
response.status_code = 200
response.text = "<html></html>"
session = MagicMock()
session.get = AsyncMock(return_value=response)
session.close = AsyncMock()
result = await fetch_detail("https://ekb.cian.ru/sale/flat/327830237/", session=session)
assert result is not None
assert result.cian_id == 327830237
assert result.windows_view_type == "yardAndStreet"
assert result.separate_wcs_count == 1
assert result.combined_wcs_count == 0
assert result.ceiling_height == 2.65
assert result.repair_type == "cosmetic"
assert result.views_total == 1042
assert result.views_today == 7
assert len(result.price_changes) == 1
assert result.price_changes[0]["price_rub"] == 5500000
assert result.agent_profile is not None
assert result.agent_profile["ext_agent_id"] == 7654321
assert result.agent_profile["company_name"] == "Первичка Плюс"
@pytest.mark.asyncio
async def test_fetch_detail_extracts_bti_from_sister_states(monkeypatch):
"""BTI sister state is captured into bti_data."""
fake_state = {
"offerData": {
"offer": {
"cianId": 111111,
}
}
}
fake_all_states = {
"frontend-offer-card": {
"bti": {
"houseData": {
"seriesName": "П-44Т",
"floorsCount": 17,
"flatCount": 120,
}
}
}
}
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_state",
lambda html, mfe, key: fake_state if key == "defaultState" else None,
)
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_all_states",
lambda html: fake_all_states,
)
response = MagicMock()
response.status_code = 200
response.text = "<html></html>"
session = MagicMock()
session.get = AsyncMock(return_value=response)
session.close = AsyncMock()
result = await fetch_detail("https://ekb.cian.ru/sale/flat/111111/", session=session)
assert result is not None
assert result.bti_data is not None
assert result.bti_data["seriesName"] == "П-44Т"
assert "bti" in result.raw_sister_states
@pytest.mark.asyncio
async def test_fetch_detail_extracts_newbuilding_link(monkeypatch):
"""Newbuilding reference is captured from offer.newbuilding."""
fake_state = {
"offerData": {
"offer": {
"cianId": 222222,
"newbuilding": {
"id": 54016,
"name": "Екатерининский парк",
"url": "https://zhk-ekaterininskiy-park-ekb-i.cian.ru/",
},
}
}
}
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_state",
lambda html, mfe, key: fake_state if key == "defaultState" else None,
)
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_all_states",
lambda html: {},
)
response = MagicMock()
response.status_code = 200
response.text = "<html></html>"
session = MagicMock()
session.get = AsyncMock(return_value=response)
session.close = AsyncMock()
result = await fetch_detail("https://ekb.cian.ru/sale/flat/222222/", session=session)
assert result is not None
assert result.newbuilding_data is not None
assert result.newbuilding_data["id"] == 54016