168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
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
|