Some checks failed
Deploy Trade-In / changes (push) Successful in 26s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Failing after 27s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / deploy (push) Has been skipped
Co-authored-by: bot-backend <bot-backend@gendsgn.local> Co-committed-by: bot-backend <bot-backend@gendsgn.local>
196 lines
6.2 KiB
Python
196 lines
6.2 KiB
Python
"""Tests for _save_yandex_history_items — house_id linking + new columns."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
# Settings requires DATABASE_URL at init time. Set dummy DSN before any app import.
|
||
os.environ.setdefault("DATABASE_URL", "postgresql://test:test@localhost/test_db")
|
||
|
||
from datetime import date
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
from app.services.estimator import _save_yandex_history_items
|
||
from app.services.scrapers.yandex_valuation import (
|
||
ValuationHistoryItem,
|
||
ValuationHouseMeta,
|
||
YandexValuationResult,
|
||
)
|
||
|
||
|
||
def _make_result(items: list[ValuationHistoryItem]) -> YandexValuationResult:
|
||
return YandexValuationResult(
|
||
address="Россия, Свердловская область, Екатеринбург, улица Куйбышева, 106",
|
||
offer_category="APARTMENT",
|
||
offer_type="SELL",
|
||
page=1,
|
||
source_url=(
|
||
"https://realty.yandex.ru/otsenka-kvartiry-po-adresu-onlayn/?address=test&page=1"
|
||
),
|
||
house=ValuationHouseMeta(year_built=2005, total_floors=25, has_lift=True),
|
||
history_items=items,
|
||
)
|
||
|
||
|
||
def _two_items() -> list[ValuationHistoryItem]:
|
||
return [
|
||
ValuationHistoryItem(
|
||
area_m2=50.0,
|
||
rooms=2,
|
||
floor=3,
|
||
start_price=9_000_000,
|
||
last_price=9_000_000,
|
||
publish_date=date(2024, 5, 10),
|
||
removed_date=None,
|
||
exposure_days=30,
|
||
status="В продаже",
|
||
),
|
||
ValuationHistoryItem(
|
||
area_m2=55.0,
|
||
rooms=2,
|
||
floor=5,
|
||
start_price=10_000_000,
|
||
last_price=9_500_000,
|
||
publish_date=date(2024, 3, 1),
|
||
removed_date=date(2024, 6, 1),
|
||
exposure_days=92,
|
||
status="Снято",
|
||
),
|
||
]
|
||
|
||
|
||
def test_save_resolves_house_once_per_page():
|
||
"""match_or_create_house must be called exactly once per page (not per item)."""
|
||
db = MagicMock()
|
||
result = _make_result(_two_items())
|
||
with patch(
|
||
"app.services.estimator.match_or_create_house",
|
||
return_value=(12345, 0.9, "fingerprint"),
|
||
) as m:
|
||
saved = _save_yandex_history_items(db, result)
|
||
assert m.call_count == 1, "match_or_create_house must be called once per page"
|
||
assert saved == 2
|
||
|
||
|
||
def test_save_row_contains_house_id_and_confidence():
|
||
"""Each INSERT row dict carries house_id and source_confidence."""
|
||
db = MagicMock()
|
||
result = _make_result(_two_items())
|
||
with patch(
|
||
"app.services.estimator.match_or_create_house",
|
||
return_value=(54321, 0.9, "fingerprint"),
|
||
):
|
||
_save_yandex_history_items(db, result)
|
||
|
||
# db.execute called once with list-of-dicts (executemany, one round-trip)
|
||
assert db.execute.call_count == 1
|
||
rows = db.execute.call_args_list[0].args[1]
|
||
assert isinstance(rows, list) and len(rows) == 2
|
||
for row in rows:
|
||
assert row["house_id"] == 54321
|
||
assert row["confidence"] == pytest.approx(0.9)
|
||
assert row["notes"] == "match_method=fingerprint"
|
||
assert row["total_floors"] == 25
|
||
|
||
|
||
def test_save_row_contains_removed_date():
|
||
"""Item with removed_date set → row dict has removed_date populated."""
|
||
db = MagicMock()
|
||
items = [
|
||
ValuationHistoryItem(
|
||
area_m2=60.0,
|
||
rooms=2,
|
||
floor=4,
|
||
start_price=11_000_000,
|
||
last_price=10_500_000,
|
||
publish_date=date(2024, 1, 15),
|
||
removed_date=date(2024, 5, 20),
|
||
exposure_days=125,
|
||
status="Снято",
|
||
),
|
||
]
|
||
result = _make_result(items)
|
||
with patch(
|
||
"app.services.estimator.match_or_create_house",
|
||
return_value=(99, 1.0, "new"),
|
||
):
|
||
_save_yandex_history_items(db, result)
|
||
|
||
# args[1] is now the list-of-dicts passed to executemany
|
||
rows = db.execute.call_args_list[0].args[1]
|
||
assert rows[0]["removed_date"] == date(2024, 5, 20)
|
||
|
||
|
||
def test_save_row_removed_date_none_when_active():
|
||
"""Active listing (status В продаже) → removed_date=None in row."""
|
||
db = MagicMock()
|
||
items = [
|
||
ValuationHistoryItem(
|
||
area_m2=42.0,
|
||
rooms=1,
|
||
floor=2,
|
||
start_price=7_500_000,
|
||
last_price=7_500_000,
|
||
publish_date=date(2024, 6, 1),
|
||
removed_date=None,
|
||
exposure_days=10,
|
||
status="В продаже",
|
||
),
|
||
]
|
||
result = _make_result(items)
|
||
with patch(
|
||
"app.services.estimator.match_or_create_house",
|
||
return_value=(7, 1.0, "new"),
|
||
):
|
||
_save_yandex_history_items(db, result)
|
||
|
||
rows = db.execute.call_args_list[0].args[1]
|
||
assert rows[0]["removed_date"] is None
|
||
|
||
|
||
def test_save_handles_match_failure_gracefully():
|
||
"""If match_or_create_house raises, save continues with house_id=None, confidence=0."""
|
||
db = MagicMock()
|
||
items = [
|
||
ValuationHistoryItem(
|
||
area_m2=42.0,
|
||
rooms=1,
|
||
floor=2,
|
||
start_price=7_500_000,
|
||
last_price=7_500_000,
|
||
publish_date=date(2024, 6, 1),
|
||
removed_date=None,
|
||
exposure_days=10,
|
||
status="В продаже",
|
||
),
|
||
]
|
||
result = _make_result(items)
|
||
with patch(
|
||
"app.services.estimator.match_or_create_house",
|
||
side_effect=RuntimeError("simulated lookup failure"),
|
||
):
|
||
saved = _save_yandex_history_items(db, result)
|
||
|
||
assert saved == 1
|
||
rows = db.execute.call_args_list[0].args[1]
|
||
assert rows[0]["house_id"] is None
|
||
assert rows[0]["confidence"] == pytest.approx(0.0)
|
||
assert rows[0]["notes"] is None
|
||
|
||
|
||
def test_save_match_called_with_address_and_year():
|
||
"""match_or_create_house receives correct address and year_built from result.house."""
|
||
db = MagicMock()
|
||
result = _make_result(_two_items())
|
||
with patch(
|
||
"app.services.estimator.match_or_create_house",
|
||
return_value=(1, 1.0, "new"),
|
||
) as m:
|
||
_save_yandex_history_items(db, result)
|
||
|
||
kwargs = m.call_args.kwargs
|
||
assert kwargs["address"] == result.address
|
||
assert kwargs["year_built"] == result.house.year_built
|
||
assert kwargs["ext_source"] == "yandex_valuation"
|