gendesign/tradein-mvp/backend/tests/test_cian_detail.py

142 lines
4.3 KiB
Python

import pytest
from unittest.mock import AsyncMock, MagicMock
from app.services.scrapers.cian_detail import (
DetailEnrichment,
_parse_float,
_parse_views,
_extract_price_changes,
_extract_agent_profile,
fetch_detail,
)
def test_parse_views():
assert _parse_views("1 234") == 1234
assert _parse_views("567") == 567
assert _parse_views(None) is None
assert _parse_views("nope") is None
def test_parse_float():
assert _parse_float("2.7") == 2.7
assert _parse_float(3) == 3.0
assert _parse_float(None) is None
assert _parse_float("bad") is None
def test_extract_price_changes_normalizes():
offer = {
"priceChanges": [
{"changeTime": "2026-04-01T00:00:00Z", "price": 5000000, "diffPercent": -2.5},
]
}
result = _extract_price_changes(offer, {})
assert len(result) == 1
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_empty():
assert _extract_price_changes({}, {}) == []
assert _extract_price_changes({"priceChanges": None}, {}) == []
def test_extract_agent_profile_from_agent():
offer = {"agent": {"id": 999, "companyName": "ACME", "isPro": True, "skills": ["x"]}}
profile = _extract_agent_profile(offer)
assert profile is not None
assert profile["ext_agent_id"] == 999
assert profile["company_name"] == "ACME"
assert profile["is_pro"] is True
def test_extract_agent_profile_from_user_fallback():
offer = {"user": {"cianUserId": 111, "companyName": "Sole"}}
profile = _extract_agent_profile(offer)
assert profile is not None
assert profile["ext_agent_id"] == 111
assert profile["company_name"] == "Sole"
def test_extract_agent_profile_none_when_empty():
assert _extract_agent_profile({}) is None
def test_dataclass_defaults():
e = DetailEnrichment()
assert e.cian_id is None
assert e.price_changes == []
assert e.raw_sister_states == {}
@pytest.mark.asyncio
async def test_fetch_detail_returns_none_on_no_state(monkeypatch):
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_state",
lambda html, mfe, key: None,
)
session = MagicMock()
response = MagicMock()
response.status_code = 200
response.text = "<html></html>"
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_http_error(monkeypatch):
session = MagicMock()
response = MagicMock()
response.status_code = 403
response.text = ""
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_parses_core_fields(monkeypatch):
fake_state = {
"offerData": {
"offer": {
"cianId": 42,
"windowsViewType": "yard",
"separateWcsCount": 1,
"combinedWcsCount": 0,
"ceilingHeight": 2.7,
"repairType": "cosmetic",
}
},
"stats": {
"totalViewsFormattedString": "1 234",
"todayViewsFormattedString": "12",
},
}
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_state",
lambda html, mfe, key: fake_state,
)
monkeypatch.setattr(
"app.services.scrapers.cian_detail.extract_all_states",
lambda html: {"frontend-offer-card": {"defaultState": fake_state}},
)
session = MagicMock()
response = MagicMock()
response.status_code = 200
response.text = "<html></html>"
session.get = AsyncMock(return_value=response)
session.close = AsyncMock()
result = await fetch_detail("https://ekb.cian.ru/sale/flat/42/", session=session)
assert result is not None
assert result.cian_id == 42
assert result.windows_view_type == "yard"
assert result.separate_wcs_count == 1
assert result.ceiling_height == 2.7
assert result.repair_type == "cosmetic"
assert result.views_total == 1234
assert result.views_today == 12