gendesign/tradein-mvp/backend/tests/test_avito_imv_parse.py
lekss361 6a2f734356
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been cancelled
feat(tradein): avito_imv.py — Avito IMV evaluation API client (2 requests) (#444)
2026-05-23 12:16:47 +00:00

287 lines
8.8 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.

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