"""Tests for Rentman equipment import pipeline (T04).""" import pytest import pytest_asyncio from unittest.mock import AsyncMock, patch, MagicMock from sqlalchemy import select from app.models.equipment import EquipmentCache from app.models.sync_log import SyncLog from app.services.sync_service import SyncService from app.services.rentman_service import RentmanService def make_raw_equipment(rid: str, name: str, category: str = "Lautsprecher") -> dict: """Raw equipment item using REAL Rentman API field names.""" return { "id": rid, "name": name, "displayname": name, "code": f"{name[:3].upper()}-001", "folder": f"/folders/{int(rid) % 5 + 1}", "shop_description_long": f"
Description for {name}
", "external_remark": "", "power": 750, "weight": 50, "empty_weight": 50, "image": f"/files/{rid}", "price": 150.00, "in_shop": True, "in_archive": False, "temporary": False, "updateHash": f"hash-{rid}", } def make_transformed(raw: dict, folder_map=None) -> dict: """Transformed equipment dict as produced by the real Rentman mapping (no network).""" return { "rentman_id": str(raw["id"]), "name": raw["name"], "number": raw.get("code", ""), "category": "Tontechnik", "subcategory": "Mikrofone", "description": f"Description for {raw['name']}", "specifications": None, "images": [], "rental_price": 150.00, "brand": "", "available": True, "update_hash": raw.get("updateHash", ""), } @pytest.mark.asyncio async def test_transform_equipment(): raw = make_raw_equipment("42", "Shure SM58 Mikrofon dynamisch") service = RentmanService.__new__(RentmanService) # no network on init path folder_map = { f"/folders/{42 % 5 + 1}": {"category": "Tontechnik", "subcategory": "Mikrofone"}, } with patch.object(RentmanService, "get_file_url", new=AsyncMock(return_value="https://example.com/42.jpg")): result = await service.transform_equipment(raw, folder_map=folder_map) assert result["rentman_id"] == "42" assert result["name"] == "Shure SM58 Mikrofon dynamisch" assert result["category"] == "Tontechnik" assert result["subcategory"] == "Mikrofone" assert result["rental_price"] == 150.00 assert result["brand"] == "Shure" assert result["available"] is True assert result["description"] == "Description for Shure SM58 Mikrofon dynamisch" assert result["images"] == ["https://example.com/42.jpg"] @pytest.mark.asyncio async def test_paginated_import(test_db): """Import service iterates all pages (3 pages with 250 items).""" page1 = {"data": [make_raw_equipment(str(i), f"Item {i}") for i in range(100)], "itemCount": 250} page2 = {"data": [make_raw_equipment(str(i), f"Item {i}") for i in range(100, 200)], "itemCount": 250} page3 = {"data": [make_raw_equipment(str(i), f"Item {i}") for i in range(200, 250)], "itemCount": 250} page4 = {"data": [], "itemCount": 250} mock_rentman = MagicMock() mock_rentman.get_all_equipment = AsyncMock(return_value=[ *[make_raw_equipment(str(i), f"Item {i}") for i in range(250)] ]) mock_rentman.get_folder_map = AsyncMock(return_value={}) mock_rentman.transform_equipment = AsyncMock(side_effect=make_transformed) with patch("app.services.sync_service.cache") as mock_cache: mock_cache.delete_pattern = AsyncMock(return_value=0) sync_service = SyncService(test_db, rentman=mock_rentman) result = await sync_service.run_sync() assert result["status"] == "completed" assert result["items_processed"] == 250 assert result["items_failed"] == 0 # Verify equipment was upserted db_result = await test_db.execute(select(EquipmentCache)) items = db_result.scalars().all() assert len(items) == 250 # Verify sync_log entry log_result = await test_db.execute(select(SyncLog)) logs = log_result.scalars().all() assert len(logs) == 1 assert logs[0].status == "completed" assert logs[0].items_processed == 250 @pytest.mark.asyncio async def test_sync_upsert_existing(test_db): """Upsert should update existing equipment, not duplicate.""" existing = EquipmentCache(rentman_id="100", name="Old Name", category="Old") test_db.add(existing) await test_db.commit() mock_rentman = MagicMock() mock_rentman.get_all_equipment = AsyncMock(return_value=[ make_raw_equipment("100", "New Name", "Lautsprecher") ]) mock_rentman.get_folder_map = AsyncMock(return_value={}) mock_rentman.transform_equipment = AsyncMock(side_effect=make_transformed) with patch("app.services.sync_service.cache") as mock_cache: mock_cache.delete_pattern = AsyncMock(return_value=0) sync_service = SyncService(test_db, rentman=mock_rentman) result = await sync_service.run_sync() assert result["items_processed"] == 1 db_result = await test_db.execute(select(EquipmentCache)) items = db_result.scalars().all() assert len(items) == 1 assert items[0].name == "New Name" @pytest.mark.asyncio async def test_sync_failure_logs_error(test_db): """Sync failure should be logged with error message.""" mock_rentman = MagicMock() mock_rentman.get_all_equipment = AsyncMock(side_effect=Exception("API unreachable")) with patch("app.services.sync_service.cache") as mock_cache: mock_cache.delete_pattern = AsyncMock(return_value=0) sync_service = SyncService(test_db, rentman=mock_rentman) result = await sync_service.run_sync() assert result["status"] == "failed" assert result["items_processed"] == 0 log_result = await test_db.execute(select(SyncLog)) log = log_result.scalar_one() assert log.status == "failed" assert "API unreachable" in (log.error_message or "") @pytest.mark.asyncio async def test_get_all_equipment_paginates(): """RentmanService.get_all_equipment iterates until data is empty.""" page1 = {"data": [{"id": str(i), "name": f"Item {i}"} for i in range(100)]} page2 = {"data": [{"id": str(i), "name": f"Item {i}"} for i in range(100, 150)]} page3 = {"data": []} svc = RentmanService(token="test-token") svc.get_equipment_page = AsyncMock(side_effect=[page1, page2, page3]) result = await svc.get_all_equipment(limit=100) assert len(result) == 150 assert svc.get_equipment_page.call_count == 3