"""Offline smoke tests для avito_imv модуля. Без сети — только cache_key вычисление, парсинг заголовков, unix→date, dataclass конструирование. """ from datetime import date import pytest from app.services.scrapers.avito_imv import ( IMVEvaluation, IMVGeo, IMVPlacementHistoryItem, IMVSuggestion, _parse_placement_item, _parse_suggestion, _parse_title, _unix_to_date, compute_imv_cache_key, ) # --------------------------------------------------------------------------- # cache_key # --------------------------------------------------------------------------- def test_cache_key_deterministic(): k1 = compute_imv_cache_key( "ЕКБ ул. Чайковского, 66А", 2, 42.0, 4, 5, "panel", "cosmetic", True, False ) k2 = compute_imv_cache_key( "ЕКБ ул. Чайковского, 66А", 2, 42.0, 4, 5, "panel", "cosmetic", True, False ) assert k1 == k2 assert len(k1) == 64 # sha256 hex def test_cache_key_differs_by_rooms(): k1 = compute_imv_cache_key( "ЕКБ ул. Чайковского, 66А", 2, 42.0, 4, 5, "panel", "cosmetic", True, False ) k3 = compute_imv_cache_key( "ЕКБ ул. Чайковского, 66А", 3, 42.0, 4, 5, "panel", "cosmetic", True, False ) assert k1 != k3 def test_cache_key_differs_by_balcony(): k_with = compute_imv_cache_key( "ул. Ленина, 1", 1, 30.0, 1, 9, "brick", "euro", True, False ) k_without = compute_imv_cache_key( "ул. Ленина, 1", 1, 30.0, 1, 9, "brick", "euro", False, False ) assert k_with != k_without def test_cache_key_differs_by_address(): k1 = compute_imv_cache_key("ул. А, 1", 2, 42.0, 4, 5, "panel", "cosmetic", True, False) k2 = compute_imv_cache_key("ул. Б, 2", 2, 42.0, 4, 5, "panel", "cosmetic", True, False) assert k1 != k2 # --------------------------------------------------------------------------- # _unix_to_date # --------------------------------------------------------------------------- def test_unix_date_none(): assert _unix_to_date(None) is None def test_unix_date_zero(): assert _unix_to_date(0) is None def test_unix_date_positive(): result = _unix_to_date(1715000000) assert isinstance(result, date) # 1715000000 примерно 2024-05-06 UTC+5 (Екатеринбург) assert result.year == 2024 assert result.month in (5, 6) # зависит от локали def test_unix_date_float(): result = _unix_to_date(1697000000.0) assert isinstance(result, date) assert result.year == 2023 # --------------------------------------------------------------------------- # _parse_title # --------------------------------------------------------------------------- def test_parse_title_full(): parsed = _parse_title("2-к. квартира, 42 м², 4/5 эт.") assert parsed["rooms"] == 2 assert parsed["area_m2"] == 42.0 assert parsed["floor"] == 4 assert parsed["total_floors"] == 5 def test_parse_title_comma_decimal(): parsed = _parse_title("1-к. квартира, 27,1 м², 2/5 эт.") assert parsed["rooms"] == 1 assert parsed["area_m2"] == pytest.approx(27.1) assert parsed["floor"] == 2 assert parsed["total_floors"] == 5 def test_parse_title_none(): parsed = _parse_title(None) assert parsed["rooms"] is None assert parsed["area_m2"] is None def test_parse_title_no_match(): parsed = _parse_title("студия, 20 м²") assert parsed["rooms"] is None # --------------------------------------------------------------------------- # _parse_placement_item # --------------------------------------------------------------------------- def test_parse_placement_item_full(): raw = { "id": 9876543210, "title": "2-к. квартира, 42 м², 4/5 эт.", "startPrice": 6500000, "startPriceDate": 1697000000, "lastPrice": 6290000, "lastPriceDate": 1715000000, "removedDate": 1715500000, "exposure": 210, "itemImage": {"url": "https://avito.ru/img.jpg"}, # должен быть отфильтрован } item = _parse_placement_item(raw) assert item.ext_item_id == "9876543210" assert item.title == "2-к. квартира, 42 м², 4/5 эт." assert item.rooms == 2 assert item.area_m2 == 42.0 assert item.floor == 4 assert item.total_floors == 5 assert item.start_price == 6500000 assert isinstance(item.start_price_date, date) assert item.last_price == 6290000 assert isinstance(item.last_price_date, date) assert isinstance(item.removed_date, date) assert item.exposure_days == 210 # itemImage должен быть отфильтрован из raw_payload assert item.raw_payload is not None assert "itemImage" not in item.raw_payload def test_parse_placement_item_no_removed_date(): raw = { "id": 111, "title": "1-к. квартира, 30 м², 2/9 эт.", "startPrice": 3000000, "startPriceDate": 1697000000, "lastPrice": 2900000, "lastPriceDate": 1715000000, "exposure": 50, } item = _parse_placement_item(raw) assert item.removed_date is None assert item.ext_item_id == "111" # --------------------------------------------------------------------------- # _parse_suggestion # --------------------------------------------------------------------------- def test_parse_suggestion_full(): raw = { "id": 7986882804, "title": "2-к. квартира, 42 м², 4/5 эт.", "address": "Авиационная улица, 63к2, Екатеринбург", "price": 6300000, "exposure": 14, "publishDate": 1716000000, "itemLink": "/ekaterinburg/kvartiry/some-path", "metro": {"name": "Чкаловская", "distance": "2,7 км", "colors": ["#cf2734"]}, "hasGoodPriceBadge": False, "imageLink": "https://avito.ru/img.jpg", # должен быть отфильтрован } sugg = _parse_suggestion(raw) assert sugg.ext_item_id == "7986882804" assert sugg.price_rub == 6300000 assert sugg.metro_name == "Чкаловская" assert sugg.metro_distance == "2,7 км" assert sugg.metro_color == "#cf2734" assert sugg.item_url == "/ekaterinburg/kvartiry/some-path" assert sugg.has_good_price_badge is False assert isinstance(sugg.publish_date, date) # imageLink должен быть отфильтрован assert sugg.raw_payload is not None assert "imageLink" not in sugg.raw_payload def test_parse_suggestion_no_metro(): raw = { "id": 999, "title": "1-к. квартира, 25 м², 3/5 эт.", "price": 4000000, "exposure": 5, "publishDate": 1716000000, "itemLink": "/path", } sugg = _parse_suggestion(raw) assert sugg.metro_name is None assert sugg.metro_color is None # --------------------------------------------------------------------------- # IMVEvaluation dataclass конструирование # --------------------------------------------------------------------------- def test_dataclass_minimal_construct(): e = IMVEvaluation( cache_key="a" * 64, address="X", rooms=2, area_m2=42.0, floor=4, floor_at_home=5, house_type="panel", renovation_type="cosmetic", has_balcony=True, has_loggia=False, geo=IMVGeo(geo_hash="JWT.TOKEN.HERE"), recommended_price=6_290_000, lower_price=6_100_000, higher_price=6_600_000, ) assert e.recommended_price == 6_290_000 assert e.lower_price == 6_100_000 assert e.higher_price == 6_600_000 assert e.geo.geo_hash == "JWT.TOKEN.HERE" assert e.placement_history == [] assert e.suggestions == [] assert e.market_count is None assert e.raw_response is None def test_dataclass_with_history(): item = IMVPlacementHistoryItem( ext_item_id="123", title="2-к. квартира, 42 м², 4/5 эт.", rooms=2, area_m2=42.0, floor=4, total_floors=5, start_price=6500000, last_price=6290000, removed_date=date(2024, 5, 12), exposure_days=210, ) e = IMVEvaluation( cache_key="b" * 64, address="Y", rooms=2, area_m2=42.0, floor=4, floor_at_home=5, house_type="panel", renovation_type="euro", has_balcony=False, has_loggia=True, geo=IMVGeo(geo_hash="JWT2"), recommended_price=6_000_000, lower_price=5_800_000, higher_price=6_300_000, market_count=800, placement_history=[item], ) assert len(e.placement_history) == 1 assert e.placement_history[0].removed_date == date(2024, 5, 12) assert e.market_count == 800