287 lines
8.8 KiB
Python
287 lines
8.8 KiB
Python
"""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
|