Fast IAAI implementation

This commit is contained in:
2026-04-24 00:20:42 +03:00
parent 2b0cffc118
commit 3cdb717789
31 changed files with 4880 additions and 0 deletions

13
tests/conftest.py Normal file
View File

@@ -0,0 +1,13 @@
from __future__ import annotations
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
os.environ.setdefault("DATABASE_URL", "sqlite+pysqlite:///:memory:")
os.environ.setdefault("CELERY_BROKER_URL", "redis://localhost:6379/0")
os.environ.setdefault("IAAI_BASE_URL", "https://www.iaai.com")

231
tests/test_client.py Normal file
View File

@@ -0,0 +1,231 @@
from __future__ import annotations
import json
from pathlib import Path
from iaai_sync_service.client import (
LISTING_MARKER,
HybridSessionAuth,
build_resizer_images_from_keys,
is_challenge_response,
parse_listing_page,
parse_product_details_vm,
resolve_listing_scope_paths,
)
from iaai_sync_service.settings import Settings
def _catalog_html() -> str:
return (Path(__file__).resolve().parents[1] / "html" / "catalog.html").read_text(encoding="utf-8")
def _page_html() -> str:
return (Path(__file__).resolve().parents[1] / "html" / "page.html").read_text(encoding="utf-8")
def test_parse_listing_page_extracts_hidden_payloads() -> None:
parsed = parse_listing_page(_catalog_html())
assert parsed.result_count >= len(parsed.vehicles)
assert parsed.page_size == 100
assert parsed.current_page == 1
assert parsed.vehicles
first = parsed.vehicles[0]
assert first.inventory_id.endswith("~US")
assert first.inventory_status == "RS"
def test_parse_product_details_vm_extracts_json_script() -> None:
payload = parse_product_details_vm(_page_html())
inventory_view = payload["inventoryView"]
attrs = inventory_view["attributes"]
assert attrs["Id"] == "45078011~US"
assert attrs["Make"] == "ACURA"
assert attrs["Model"] == "RSX"
def test_build_resizer_images_from_keys_deduplicates_and_orders() -> None:
keys = [
{"k": "abc~1", "w": 2000, "h": 1500, "i": 2},
{"k": "abc~1", "w": 2000, "h": 1500, "i": 2},
{"k": "abc~2", "w": 1800, "h": 1200, "i": 1},
]
images = build_resizer_images_from_keys(keys)
assert len(images) == 2
assert images[0]["order_index"] == 1
assert "imageKeys=abc~2" in str(images[0]["fullres_image"])
def test_is_challenge_response_detects_missing_marker() -> None:
html = "<html><body><h1>Blocked</h1></body></html>"
assert is_challenge_response(status_code=200, body_text=html, expected_marker='id="GBPSearchQuery"') is True
def test_is_challenge_response_detects_incapsula_marker() -> None:
html = '<html><script>var a="_Incapsula_Resource";</script></html>'
assert is_challenge_response(status_code=200, body_text=html, expected_marker=None) is True
def test_is_challenge_response_accepts_expected_marker_even_with_incapsula_script() -> None:
html = f'<html><input id="GBPSearchQuery" value="{{}}" /><script src="/_Incapsula_Resource"></script></html>'
assert is_challenge_response(status_code=200, body_text=html, expected_marker=LISTING_MARKER) is False
def test_catalog_hidden_vehicle_json_is_valid() -> None:
parsed = parse_listing_page(_catalog_html())
# smoke-check we can serialize parsed query back to JSON
dumped = json.dumps(parsed.gbp_search_query)
assert "CurrentPage" in dumped
def test_resolve_listing_scope_paths_prefers_explicit_url() -> None:
paths = resolve_listing_scope_paths(
listing_start_url="https://www.iaai.com/Search?url=abc",
brands={"acura", "bmw"},
)
assert paths == ["https://www.iaai.com/Search?url=abc"]
class _FakeResponse:
def __init__(self, status_code: int, text: str) -> None:
self.status_code = status_code
self.text = text
def close(self) -> None:
return
class _FakeSession:
def __init__(self, responses: list[_FakeResponse], *, cookies: list[object] | None = None) -> None:
self._responses = responses
self.calls = 0
self.cookies = cookies or []
def request(self, *args, **kwargs): # noqa: ANN002, ANN003, ANN202
self.calls += 1
return self._responses.pop(0)
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=0,
http_retry_backoff_ms=1,
fetch_concurrency=1,
db_commit_batch_size=10,
session_keepalive_enabled=True,
session_keepalive_interval_minutes=15,
schema_bootstrap_enabled=False,
advisory_lock_key=1,
error_summary_max_len=2000,
)
def test_hybrid_auth_retries_after_challenge(monkeypatch) -> None: # noqa: ANN001
auth = HybridSessionAuth(_settings())
cookie = type("Cookie", (), {"domain": ".iaai.com"})()
fake_session = _FakeSession(
[
_FakeResponse(403, "<html>forbidden</html>"),
_FakeResponse(200, f"<html>{LISTING_MARKER}</html>"),
],
cookies=[cookie],
)
auth._thread_local.session = fake_session # type: ignore[attr-defined]
auth._bootstrap_cookies_loaded = True # type: ignore[attr-defined]
refreshed = {"count": 0}
def _fake_refresh(*, expected_marker=None, session=None) -> None: # noqa: ANN001
refreshed["count"] += 1
monkeypatch.setattr(auth, "_refresh_session_via_playwright", _fake_refresh)
response = auth.request(
"GET",
"https://www.iaai.com/Vehiclelisting/Cars",
timeout=5,
retries=0,
retry_backoff_ms=1,
expected_marker=LISTING_MARKER,
)
assert response.status_code == 200
assert refreshed["count"] == 1
assert fake_session.calls == 2
def test_hybrid_auth_bootstraps_anonymous_session_when_no_cookies(monkeypatch) -> None: # noqa: ANN001
auth = HybridSessionAuth(_settings())
fake_session = _FakeSession(
[
_FakeResponse(200, f"<html>{LISTING_MARKER}</html>"),
],
cookies=[],
)
auth._thread_local.session = fake_session # type: ignore[attr-defined]
auth._bootstrap_cookies_loaded = True # type: ignore[attr-defined]
refreshed = {"count": 0}
def _fake_refresh(*, expected_marker=None, session=None) -> None: # noqa: ANN001
refreshed["count"] += 1
monkeypatch.setattr(auth, "_refresh_session_via_playwright", _fake_refresh)
response = auth.request(
"GET",
"https://www.iaai.com/Vehiclelisting/Cars",
timeout=5,
retries=0,
retry_backoff_ms=1,
expected_marker=LISTING_MARKER,
)
assert response.status_code == 200
assert refreshed["count"] == 1
assert fake_session.calls == 1
def test_wait_until_non_challenge_tolerates_transient_navigation_errors() -> None:
class _FakePage:
def __init__(self) -> None:
self.content_calls = 0
self.goto_calls = 0
def wait_for_timeout(self, *_args, **_kwargs) -> None: # noqa: ANN002, ANN003
return
def wait_for_load_state(self, *_args, **_kwargs) -> None: # noqa: ANN002, ANN003
return
def content(self) -> str:
self.content_calls += 1
if self.content_calls == 1:
raise RuntimeError(
"Page.content: Unable to retrieve content because the page is navigating and changing the content."
)
return f"<html>{LISTING_MARKER}</html>"
def goto(self, *_args, **_kwargs) -> None: # noqa: ANN002, ANN003
self.goto_calls += 1
page = _FakePage()
HybridSessionAuth._wait_until_non_challenge(
page=page,
target="https://www.iaai.com/Vehiclelisting/Cars",
timeout_ms=5_000,
expected_marker=LISTING_MARKER,
)
assert page.content_calls >= 2
assert page.goto_calls == 0

36
tests/test_migrations.py Normal file
View File

@@ -0,0 +1,36 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
def _load_module(path: Path): # noqa: ANN202
spec = importlib.util.spec_from_file_location(path.stem, path)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_revision_chain_is_correct() -> None:
base = Path(__file__).resolve().parents[1] / "alembic" / "versions"
rev1 = _load_module(base / "0001_ensure_sync_schema.py")
rev2 = _load_module(base / "0002_extend_origin_enum_for_iaai.py")
rev3 = _load_module(base / "0003_create_iaai_source_tables.py")
assert rev1.revision == "0001_ensure_sync_schema"
assert rev1.down_revision is None
assert rev2.revision == "0002_extend_origin_enum_for_iaai"
assert rev2.down_revision == "0001_ensure_sync_schema"
assert rev3.revision == "0003_create_iaai_source_tables"
assert rev3.down_revision == "0002_extend_origin_enum_for_iaai"
def test_enum_migration_contains_required_values() -> None:
migration = (
Path(__file__).resolve().parents[1] / "alembic" / "versions" / "0002_extend_origin_enum_for_iaai.py"
).read_text(encoding="utf-8")
assert "originenum" in migration
assert "IAAI" in migration
assert "ADD VALUE IF NOT EXISTS" in migration

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
import json
from iaai_sync_service.runtime_config import RuntimeConfig, load_runtime_config
def test_load_runtime_config_reads_filters(tmp_path) -> None: # noqa: ANN001
config_path = tmp_path / "sync_runtime_config.json"
config_path.write_text(
json.dumps(
{
"sync": {
"condition_check_enabled": "true",
},
"filters": {
"brands": ["Acura", "BMW"],
"models": ["RSX", "3 SERIES"],
"years": [2021, "2022"],
"body_types": ["SUV"],
},
}
),
encoding="utf-8",
)
runtime = load_runtime_config(config_path)
assert runtime.condition_check_enabled is True
assert runtime.filters.brands == {"acura", "bmw"}
assert runtime.filters.models == {"rsx", "3 series"}
assert runtime.filters.years == {2021, 2022}
assert runtime.filters.body_types == {"suv"}
def test_load_runtime_config_returns_defaults_on_invalid_json(tmp_path) -> None: # noqa: ANN001
config_path = tmp_path / "sync_runtime_config.json"
config_path.write_text("{invalid", encoding="utf-8")
runtime = load_runtime_config(config_path)
assert runtime == RuntimeConfig()

66
tests/test_settings.py Normal file
View File

@@ -0,0 +1,66 @@
from __future__ import annotations
import pytest
from iaai_sync_service.settings import Settings
def test_settings_from_env_reads_required_values(monkeypatch) -> None: # noqa: ANN001
monkeypatch.setenv("DATABASE_URL", "postgres://user:pass@localhost:5432/db")
monkeypatch.setenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
monkeypatch.setenv("IAAI_BASE_URL", "https://www.iaai.com")
monkeypatch.setenv("IAAI_LISTING_START_URL", "https://www.iaai.com/Search?url=abc")
monkeypatch.setenv("IAAI_FETCH_CONCURRENCY", "4")
monkeypatch.setenv("IAAI_SESSION_KEEPALIVE_ENABLED", "false")
monkeypatch.setenv("IAAI_SESSION_KEEPALIVE_INTERVAL_MINUTES", "20")
monkeypatch.setenv("IAAI_SCHEMA_BOOTSTRAP_ENABLED", "true")
settings = Settings.from_env()
assert settings.database_url == "postgresql+psycopg://user:pass@localhost:5432/db"
assert settings.celery_broker_url == "redis://localhost:6379/0"
assert settings.iaai_base_url == "https://www.iaai.com"
assert settings.iaai_listing_start_url == "https://www.iaai.com/Search?url=abc"
assert settings.fetch_concurrency == 4
assert settings.session_keepalive_enabled is False
assert settings.session_keepalive_interval_minutes == 20
assert settings.schema_bootstrap_enabled is True
def test_settings_rejects_non_positive_concurrency(monkeypatch) -> None: # noqa: ANN001
monkeypatch.setenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/db")
monkeypatch.setenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
monkeypatch.setenv("IAAI_BASE_URL", "https://www.iaai.com")
monkeypatch.setenv("IAAI_FETCH_CONCURRENCY", "0")
with pytest.raises(RuntimeError, match="IAAI_FETCH_CONCURRENCY"):
Settings.from_env()
def test_settings_bootstrap_flag_defaults_to_false(monkeypatch) -> None: # noqa: ANN001
monkeypatch.setenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/db")
monkeypatch.setenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
monkeypatch.setenv("IAAI_BASE_URL", "https://www.iaai.com")
settings = Settings.from_env()
assert settings.schema_bootstrap_enabled is False
def test_settings_rejects_invalid_listing_start_url(monkeypatch) -> None: # noqa: ANN001
monkeypatch.setenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/db")
monkeypatch.setenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
monkeypatch.setenv("IAAI_BASE_URL", "https://www.iaai.com")
monkeypatch.setenv("IAAI_LISTING_START_URL", "Search?url=abc")
with pytest.raises(RuntimeError, match="IAAI_LISTING_START_URL"):
Settings.from_env()
def test_settings_rejects_invalid_keepalive_interval(monkeypatch) -> None: # noqa: ANN001
monkeypatch.setenv("DATABASE_URL", "postgresql://user:pass@localhost:5432/db")
monkeypatch.setenv("CELERY_BROKER_URL", "redis://localhost:6379/0")
monkeypatch.setenv("IAAI_BASE_URL", "https://www.iaai.com")
monkeypatch.setenv("IAAI_SESSION_KEEPALIVE_INTERVAL_MINUTES", "0")
with pytest.raises(RuntimeError, match="IAAI_SESSION_KEEPALIVE_INTERVAL_MINUTES"):
Settings.from_env()

288
tests/test_sync_engine.py Normal file
View File

@@ -0,0 +1,288 @@
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

167
tests/test_tasks.py Normal file
View File

@@ -0,0 +1,167 @@
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
import pytest
from iaai_sync_service.models import SyncRun
from iaai_sync_service.sync_engine import SyncStats
from iaai_sync_service.tasks import execute_keepalive_job, execute_sync_job
class _ScalarResult:
def __init__(self, value): # noqa: ANN001
self._value = value
def scalar_one(self): # noqa: ANN201
return self._value
class FakeSession:
def __init__(self, *, lock_acquired: bool) -> None:
self.lock_acquired = lock_acquired
self.runs: dict[int, SyncRun] = {}
self.closed = False
def execute(self, stmt, params=None): # noqa: ANN001, ANN201
sql = str(stmt)
if "pg_try_advisory_lock" in sql:
return _ScalarResult(self.lock_acquired)
if "pg_advisory_unlock" in sql:
return _ScalarResult(True)
raise AssertionError(f"Unexpected SQL execute call: {sql}")
def add(self, obj) -> None: # noqa: ANN001
if isinstance(obj, SyncRun):
if obj.id is None:
obj.id = len(self.runs) + 1
self.runs[obj.id] = obj
def commit(self) -> None:
return
def rollback(self) -> None:
return
def get(self, model, run_id): # noqa: ANN001, ANN201
assert model is SyncRun
return self.runs.get(run_id)
def close(self) -> None:
self.closed = True
def _settings() -> SimpleNamespace:
return SimpleNamespace(
iaai_base_url="https://www.iaai.com",
sync_runtime_config_file=Path("sync_runtime_config.json"),
schema_bootstrap_enabled=True,
advisory_lock_key=123,
error_summary_max_len=2000,
)
def test_execute_sync_job_skips_when_lock_not_available(monkeypatch) -> None: # noqa: ANN001
session = FakeSession(lock_acquired=False)
monkeypatch.setattr("iaai_sync_service.tasks.get_settings", _settings)
monkeypatch.setattr("iaai_sync_service.tasks.get_session", lambda: session)
monkeypatch.setattr("iaai_sync_service.tasks.ensure_schema", lambda _session: None)
result = execute_sync_job()
assert result == {"status": "skipped_locked"}
assert session.closed is True
def test_execute_sync_job_success_writes_sync_run(monkeypatch) -> None: # noqa: ANN001
session = FakeSession(lock_acquired=True)
monkeypatch.setattr("iaai_sync_service.tasks.get_settings", _settings)
monkeypatch.setattr("iaai_sync_service.tasks.get_session", lambda: session)
monkeypatch.setattr("iaai_sync_service.tasks.ensure_schema", lambda _session: None)
monkeypatch.setattr(
"iaai_sync_service.tasks.run_sync_once",
lambda **kwargs: (SyncStats(ids_fetched=5, cars_upserted=4, cars_failed=0, images_upserted=12), []),
)
result = execute_sync_job()
assert result["status"] == "success"
assert result["ids_fetched"] == 5
run = session.runs[result["run_id"]]
assert run.status == "success"
assert run.cars_upserted == 4
def test_execute_sync_job_marks_failed_when_sync_raises(monkeypatch) -> None: # noqa: ANN001
session = FakeSession(lock_acquired=True)
monkeypatch.setattr("iaai_sync_service.tasks.get_settings", _settings)
monkeypatch.setattr("iaai_sync_service.tasks.get_session", lambda: session)
monkeypatch.setattr("iaai_sync_service.tasks.ensure_schema", lambda _session: None)
def _raise(**kwargs): # noqa: ANN003, ANN202
raise RuntimeError("boom")
monkeypatch.setattr("iaai_sync_service.tasks.run_sync_once", _raise)
with pytest.raises(RuntimeError, match="boom"):
execute_sync_job()
failed = session.runs[1]
assert failed.status == "failed"
assert failed.error_summary is not None
def test_execute_sync_job_skips_schema_bootstrap_when_disabled(monkeypatch) -> None: # noqa: ANN001
session = FakeSession(lock_acquired=False)
settings = _settings()
settings.schema_bootstrap_enabled = False
monkeypatch.setattr("iaai_sync_service.tasks.get_settings", lambda: settings)
monkeypatch.setattr("iaai_sync_service.tasks.get_session", lambda: session)
def _fail_if_called(_session): # noqa: ANN001, ANN202
raise AssertionError("ensure_schema should not be called")
monkeypatch.setattr("iaai_sync_service.tasks.ensure_schema", _fail_if_called)
result = execute_sync_job()
assert result == {"status": "skipped_locked"}
def test_execute_keepalive_job_skips_when_lock_not_available(monkeypatch) -> None: # noqa: ANN001
session = FakeSession(lock_acquired=False)
monkeypatch.setattr("iaai_sync_service.tasks.get_settings", _settings)
monkeypatch.setattr("iaai_sync_service.tasks.get_session", lambda: session)
result = execute_keepalive_job()
assert result == {"status": "skipped_locked"}
assert session.closed is True
def test_execute_keepalive_job_success(monkeypatch) -> None: # noqa: ANN001
session = FakeSession(lock_acquired=True)
monkeypatch.setattr("iaai_sync_service.tasks.get_settings", _settings)
monkeypatch.setattr("iaai_sync_service.tasks.get_session", lambda: session)
class _FakeClient:
def __init__(self, settings): # noqa: ANN001
self._settings = settings
def keep_session_alive(self): # noqa: ANN201
return {
"scope": "https://www.iaai.com/Search?url=abc",
"marker_present": True,
"result_count": 1234,
"elapsed_ms": 456,
}
monkeypatch.setattr("iaai_sync_service.tasks.IAAIClient", _FakeClient)
result = execute_keepalive_job()
assert result["status"] == "success"
assert result["marker_present"] is True
assert result["result_count"] == 1234
assert session.closed is True