stability and vps runtime updates

This commit is contained in:
qananasikq
2026-04-23 20:07:12 +03:00
parent 6aba4f0dbd
commit 86ade31f54
13 changed files with 509 additions and 82 deletions

26
tests/test_api_tasks.py Normal file
View File

@@ -0,0 +1,26 @@
from __future__ import annotations
import pytest
from pydantic import ValidationError
from iaai_scraper.api.routes.tasks import SyncListingRequest, SyncVehicleRequest
def test_sync_vehicle_request_rejects_empty_vehicle_url() -> None:
with pytest.raises(ValidationError):
SyncVehicleRequest(vehicle_url=" ")
def test_sync_vehicle_request_normalizes_whitespace_fields() -> None:
body = SyncVehicleRequest(vehicle_url=" https://www.iaai.com/VehicleDetails/123 ", lane=" ")
assert body.vehicle_url == "https://www.iaai.com/VehicleDetails/123"
assert body.lane == "iaai"
def test_sync_listing_request_normalizes_empty_filters_and_lane() -> None:
body = SyncListingRequest(make=" ", model="\t", lane=" ")
assert body.make is None
assert body.model is None
assert body.lane == "iaai_cars"

View File

@@ -314,6 +314,46 @@ class TestScraperSync(unittest.TestCase):
self.assertEqual({u for u, _ in fallback_urls}, {urls[1], urls[2]})
self.assertNotIn(urls[0], {u for u, _ in fallback_urls})
def test_sync_listing_marks_failed_on_unfiltered_zero_links_first_page(self) -> None:
scraper = self._make_scraper()
scraper.persistence.create_tables = MagicMock()
scraper.persistence.start_sync_run = MagicMock(return_value=100)
scraper.persistence.finish_sync_run = MagicMock()
scraper._sync_listing_streaming = MagicMock(return_value={
"listing": {
"status": "ok",
"pages_collected": 1,
"vehicles_collected": 0,
"vehicle_urls": [],
"early_stopped": False,
"truncated_by_time_budget": False,
"pages": [{"page_number": 1, "links_found": 0}],
},
"total": 0,
"skipped_existing": 0,
"cars_upserted": 0,
"cars_failed": 0,
"images_upserted": 0,
"protection_events": 0,
"failures": [],
"all_listing_origin_urls": set(),
})
result = scraper.sync_listing(
make=None,
model=None,
lane="iaai_cars",
limit=None,
only_new=False,
listing_url="https://www.iaai.com/Vehiclelisting/Cars",
)
self.assertEqual(result["status"], "failed")
self.assertTrue(result["anti_bot_detected"])
self.assertTrue(result["suspected_ip_block"])
self.assertTrue(any("IP/proxy block" in f.get("error", "") for f in result["failures"]))
if __name__ == "__main__":
unittest.main()

View File

@@ -10,6 +10,20 @@ from iaai_scraper.core.config import settings as base_settings
class TestWorkerTaskLockHelpers(unittest.TestCase):
def test_sync_vehicle_task_skips_empty_vehicle_url(self) -> None:
with patch.object(tasks, "_get_persistence") as get_persistence:
get_persistence.return_value = MagicMock()
tasks.sync_vehicle_task.push_request(id="task-empty-vehicle")
try:
result = tasks.sync_vehicle_task.run(vehicle_url=" ", lane=" ")
finally:
tasks.sync_vehicle_task.pop_request()
self.assertEqual(result["status"], "skipped")
self.assertEqual(result["reason"], "empty_vehicle_url")
get_persistence.assert_not_called()
def test_lock_acquire_refresh_release(self) -> None:
redis_client = MagicMock()
@@ -228,6 +242,22 @@ class TestWorkerTaskLockHelpers(unittest.TestCase):
segments = [{"make": "ACURA"}, {"make": "AUDI"}]
with patch.object(tasks, "_get_persistence") as get_persistence, \
patch.object(tasks, "_get_redis") as get_redis, \
patch.object(
tasks,
"Settings",
return_value=replace(
base_settings,
discovery=replace(
base_settings.discovery,
always_full_scan=False,
mode="listing",
),
celery=replace(
base_settings.celery,
parallel_segments=False,
),
),
), \
patch.object(tasks, "_acquire_lock", return_value=True), \
patch.object(tasks, "_is_full_scan_done", return_value=True), \
patch.object(tasks, "_start_lock_heartbeat") as start_heartbeat, \
@@ -257,7 +287,7 @@ class TestWorkerTaskLockHelpers(unittest.TestCase):
with patch.object(tasks, "IAAIScraper", return_value=scraper_ctx):
tasks.sync_listing_task.push_request(id="task-792")
try:
result = tasks.sync_listing_task.run()
result = tasks.sync_listing_task.run(limit=1)
finally:
tasks.sync_listing_task.pop_request()
@@ -420,6 +450,49 @@ class TestWorkerTaskLockHelpers(unittest.TestCase):
clear_pending.assert_called()
task_app.send_task.assert_not_called()
def test_sync_listing_task_stops_bootstrap_followup_after_no_progress_limit(self) -> None:
with patch.object(tasks, "_get_persistence") as get_persistence, \
patch.object(tasks, "_get_redis") as get_redis, \
patch.object(tasks, "_acquire_lock", return_value=True), \
patch.object(tasks, "_is_full_scan_done", return_value=False), \
patch.object(tasks, "_start_lock_heartbeat", return_value=(MagicMock(), MagicMock())), \
patch.object(tasks, "_release_lock_if_owner"), \
patch.object(tasks, "_run_browser_job", return_value={
"run_id": 101,
"status": "success",
"full_scan_completed": False,
"cars_upserted": 0,
"cars_failed": 0,
"images_upserted": 0,
"skipped_existing": 0,
"total_discovered": 0,
"listing": {"vehicles_collected": 0},
"failures": [],
}), \
patch.object(tasks, "_bump_bootstrap_no_progress_streak", return_value=(
tasks.SYNC_LISTING_BOOTSTRAP_NO_PROGRESS_STREAK_LIMIT,
False,
)), \
patch.object(tasks, "_set_full_scan_done") as set_done, \
patch.object(tasks, "_clear_sync_checkpoint") as clear_checkpoint, \
patch.object(tasks.sync_listing_task, "update_state"):
get_persistence.return_value = MagicMock()
redis_client = MagicMock()
redis_client.get.return_value = None
get_redis.return_value = redis_client
with patch.object(tasks.sync_listing_task, "app", new=MagicMock()) as task_app:
tasks.sync_listing_task.push_request(id="task-no-progress")
try:
result = tasks.sync_listing_task.run()
finally:
tasks.sync_listing_task.pop_request()
self.assertEqual(result["status"], "failed")
set_done.assert_called()
clear_checkpoint.assert_called()
task_app.send_task.assert_not_called()
def test_sync_listing_task_soft_timeout_deduplicates_continuation(self) -> None:
with patch.object(tasks, "_get_persistence") as get_persistence, \
patch.object(tasks, "_get_redis") as get_redis, \