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