289 lines
9.2 KiB
Python
289 lines
9.2 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
from iaai_sync_service.client import ListingVehicle
|
|
from iaai_sync_service.models import Base, Car, Image
|
|
from iaai_sync_service.runtime_config import RuntimeConfig, RuntimeFilters
|
|
from iaai_sync_service.settings import Settings
|
|
from iaai_sync_service.sync_engine import (
|
|
prepare_car,
|
|
replace_images_if_changed,
|
|
run_sync_once,
|
|
)
|
|
|
|
|
|
def _session() -> Session:
|
|
engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
|
|
Base.metadata.create_all(engine)
|
|
return Session(bind=engine)
|
|
|
|
|
|
def _settings() -> Settings:
|
|
return Settings(
|
|
database_url="sqlite+pysqlite:///:memory:",
|
|
celery_broker_url="redis://localhost:6379/0",
|
|
celery_result_backend="redis://localhost:6379/1",
|
|
iaai_base_url="https://www.iaai.com",
|
|
iaai_listing_start_url="",
|
|
sync_runtime_config_file=Path("sync_runtime_config.json"),
|
|
iaai_session_cookies="",
|
|
iaai_storage_state_path=Path("iaai_storage_state.json"),
|
|
iaai_login="",
|
|
iaai_password="",
|
|
http_timeout=10,
|
|
http_retries=1,
|
|
http_retry_backoff_ms=1,
|
|
fetch_concurrency=2,
|
|
db_commit_batch_size=20,
|
|
session_keepalive_enabled=True,
|
|
session_keepalive_interval_minutes=15,
|
|
schema_bootstrap_enabled=False,
|
|
advisory_lock_key=1,
|
|
error_summary_max_len=2000,
|
|
)
|
|
|
|
|
|
def _listing_vehicle(inventory_id: str, *, status: str = "RS", closed: bool = False) -> ListingVehicle:
|
|
return ListingVehicle(
|
|
inventory_id=inventory_id,
|
|
tenant="US",
|
|
auction_id="1_1",
|
|
auction_date="2026-04-08T08:30:00+00:00",
|
|
inventory_status=status,
|
|
currency="USD",
|
|
timed_auction_closed=closed,
|
|
timed_auction_indicator=False,
|
|
prebid_indicator=True,
|
|
buynow_indicator=False,
|
|
)
|
|
|
|
|
|
def _detail_payload(
|
|
inventory_id: str,
|
|
*,
|
|
make: str = "ACURA",
|
|
model: str = "RSX",
|
|
series: str = "BASE",
|
|
year: int = 2002,
|
|
bid: int = 500,
|
|
) -> dict:
|
|
return {
|
|
"inventoryView": {
|
|
"attributes": {
|
|
"Id": inventory_id,
|
|
"Year": str(year),
|
|
"Make": make,
|
|
"Model": model,
|
|
"Series": series,
|
|
"Currency": "USD",
|
|
"ODOValue": "219187",
|
|
"ExteriorColor": "GRAY",
|
|
"DriveLineTypeDesc": "FWD",
|
|
"Transmission": "Automatic",
|
|
"BodyStyleName": "COUPE",
|
|
"EngineSize": "2.0L I-4",
|
|
"VehicleGrade": "50",
|
|
"PrimaryDamageDesc": "NORMAL WEAR & TEAR",
|
|
"SecondaryDamageDesc": " ",
|
|
},
|
|
"imageDimensions": {
|
|
"keys": {
|
|
"$values": [
|
|
{"k": f"{inventory_id}~I1", "w": 1600, "h": 1200, "i": 0},
|
|
{"k": f"{inventory_id}~I2", "w": 1600, "h": 1200, "i": 1},
|
|
]
|
|
}
|
|
},
|
|
},
|
|
"auctionInformation": {
|
|
"biddingInformation": {
|
|
"buyNowAmount": 0,
|
|
"buyNowPrice": "$0",
|
|
},
|
|
"prebidInformation": {
|
|
"decimalHighBidAmount": str(bid),
|
|
"highBidAmount": f"${bid}",
|
|
"buyNowPrice": "$0",
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
class FakeClient:
|
|
def __init__(self, listing: list[ListingVehicle], detail_payloads: dict[str, dict]) -> None:
|
|
self._listing = listing
|
|
self._detail_payloads = detail_payloads
|
|
self.detail_calls = 0
|
|
self.persist_calls = 0
|
|
|
|
def iter_listing_vehicles(self, filters: RuntimeFilters): # noqa: ANN201, ARG002
|
|
return iter(self._listing)
|
|
|
|
def fetch_vehicle_detail_payload(self, inventory_id: str): # noqa: ANN201
|
|
self.detail_calls += 1
|
|
return self._detail_payloads[inventory_id]
|
|
|
|
def persist_session_state(self) -> None:
|
|
self.persist_calls += 1
|
|
|
|
|
|
def test_prepare_car_maps_fields() -> None:
|
|
row = prepare_car(
|
|
listing_vehicle=_listing_vehicle("45078011~US"),
|
|
detail_payload=_detail_payload("45078011~US", bid=600),
|
|
currency_labels={"USD", "CAD", "NA"},
|
|
country_labels={"US", "CA", "NA"},
|
|
drive_labels={"FWD", "RWD", "4WD", "NA"},
|
|
gearbox_labels={"AT", "MT", "NA"},
|
|
body_type_labels={"COUPE", "OTHER"},
|
|
body_type_default="OTHER",
|
|
steering_left="LEFT",
|
|
selling_type_auction="AUCTION",
|
|
origin_code="IAAI",
|
|
)
|
|
|
|
assert row.origin_id == "iaai:45078011~US"
|
|
assert row.origin_url == "https://www.iaai.com/VehicleDetail/45078011~US"
|
|
assert row.brand == "ACURA"
|
|
assert row.model == "RSX BASE"
|
|
assert row.year == 2002
|
|
assert row.price == 600
|
|
assert row.currency == "USD"
|
|
assert row.country == "US"
|
|
assert row.drive == "FWD"
|
|
assert row.gearbox == "AT"
|
|
assert row.body_type == "COUPE"
|
|
assert row.engine_volume == 2000
|
|
assert row.is_damaged is False
|
|
assert len(row.images) == 2
|
|
|
|
|
|
def test_replace_images_if_changed_returns_zero_for_same_set() -> None:
|
|
session = _session()
|
|
car = Car(
|
|
parser_id="car-aaaaaaaaaaaaaaaaaaaaaa",
|
|
brand="ACURA",
|
|
model="RSX",
|
|
origin_id="iaai:45078011~US",
|
|
origin_url="https://www.iaai.com/VehicleDetail/45078011~US",
|
|
slug="acura-rsx-2002",
|
|
is_sold=False,
|
|
last_seen_at=datetime(2026, 4, 1, 12, 0, tzinfo=UTC),
|
|
)
|
|
session.add(car)
|
|
session.flush()
|
|
session.add(
|
|
Image(
|
|
car_id=car.id,
|
|
fullres_image="https://vis.iaai.com/resizer?imageKeys=a&width=1600&height=1200",
|
|
preview_image="https://vis.iaai.com/resizer?imageKeys=a&width=640&height=480",
|
|
order_index=0,
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
written = replace_images_if_changed(
|
|
session=session,
|
|
car_id=car.id,
|
|
images=[
|
|
{
|
|
"order_index": 0,
|
|
"fullres_image": "https://vis.iaai.com/resizer?imageKeys=a&width=1600&height=1200",
|
|
"preview_image": "https://vis.iaai.com/resizer?imageKeys=a&width=640&height=480",
|
|
}
|
|
],
|
|
)
|
|
|
|
assert written == 0
|
|
|
|
|
|
def test_run_sync_once_upserts_and_marks_missing_as_sold() -> None:
|
|
session = _session()
|
|
|
|
listing_first = [_listing_vehicle("45078011~US"), _listing_vehicle("45268167~US")]
|
|
detail_first = {
|
|
"45078011~US": _detail_payload("45078011~US", make="ACURA", model="RSX", bid=500),
|
|
"45268167~US": _detail_payload("45268167~US", make="BMW", model="X5", bid=900),
|
|
}
|
|
client_first = FakeClient(listing_first, detail_first)
|
|
|
|
stats1, errors1 = run_sync_once(session=session, client=client_first, settings=_settings())
|
|
assert errors1 == []
|
|
assert stats1.ids_fetched == 2
|
|
assert stats1.cars_upserted == 2
|
|
assert client_first.persist_calls == 1
|
|
|
|
listing_second = [_listing_vehicle("45078011~US")]
|
|
detail_second = {
|
|
"45078011~US": _detail_payload("45078011~US", make="ACURA", model="RSX", bid=650),
|
|
}
|
|
client_second = FakeClient(listing_second, detail_second)
|
|
|
|
stats2, errors2 = run_sync_once(session=session, client=client_second, settings=_settings())
|
|
assert errors2 == []
|
|
assert stats2.ids_fetched == 1
|
|
assert stats2.cars_upserted == 1
|
|
|
|
active = session.query(Car).filter_by(origin_id="iaai:45078011~US").one()
|
|
sold = session.query(Car).filter_by(origin_id="iaai:45268167~US").one()
|
|
assert active.is_sold is False
|
|
assert active.price == 650
|
|
assert sold.is_sold is True
|
|
|
|
|
|
def test_run_sync_once_skips_reconcile_when_runtime_filters_enabled() -> None:
|
|
session = _session()
|
|
session.add(
|
|
Car(
|
|
parser_id="car-zzzzzzzzzzzzzzzzzzzzzz",
|
|
brand="BMW",
|
|
model="X5",
|
|
origin_id="iaai:99999999~US",
|
|
origin_url="https://www.iaai.com/VehicleDetail/99999999~US",
|
|
slug="bmw-x5-2020",
|
|
is_sold=False,
|
|
last_seen_at=datetime(2026, 4, 1, 12, 0, tzinfo=UTC),
|
|
)
|
|
)
|
|
session.commit()
|
|
|
|
listing = [_listing_vehicle("45078011~US")]
|
|
details = {"45078011~US": _detail_payload("45078011~US", make="ACURA", model="RSX", bid=500)}
|
|
runtime = RuntimeConfig(filters=RuntimeFilters(brands={"bmw"}))
|
|
|
|
stats, errors = run_sync_once(
|
|
session=session,
|
|
client=FakeClient(listing, details),
|
|
settings=_settings(),
|
|
runtime_config=runtime,
|
|
)
|
|
|
|
assert errors == []
|
|
assert stats.ids_fetched == 0
|
|
|
|
row = session.query(Car).filter_by(origin_id="iaai:99999999~US").one()
|
|
assert row.is_sold is False
|
|
|
|
|
|
def test_run_sync_once_condition_check_skips_closed_rows() -> None:
|
|
session = _session()
|
|
listing = [_listing_vehicle("45078011~US", status="SOLD", closed=True)]
|
|
details = {"45078011~US": _detail_payload("45078011~US", bid=500)}
|
|
|
|
runtime = RuntimeConfig(condition_check_enabled=True)
|
|
stats, errors = run_sync_once(
|
|
session=session,
|
|
client=FakeClient(listing, details),
|
|
settings=_settings(),
|
|
runtime_config=runtime,
|
|
)
|
|
|
|
assert errors == []
|
|
assert stats.ids_fetched == 0
|
|
assert session.query(Car).count() == 0
|