"""Tests for vehicle CRUD endpoints and mobile.de integration.""" import uuid from datetime import date from decimal import Decimal from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio from httpx import ASGITransport, AsyncClient from app.database import Base, get_db from app.main import app from app.models.vehicle import MobileDeListing, Vehicle @pytest_asyncio.fixture async def sample_vehicle_data(): """Valid vehicle data for creation.""" return { "make": "Mercedes-Benz", "model": "Actros", "fin": "WDB9066351L123456", "year": 2020, "first_registration": "2020-03-15", "power_kw": 300, "fuel_type": "Diesel", "transmission": "Manual", "color": "White", "condition": "used", "location": "Berlin", "availability": "available", "price": 45000.00, "vehicle_type": "lkw", "lkw_type": "sattelzugmaschine", "mileage_km": 120000, "description": "Well maintained truck", } @pytest_asyncio.fixture async def created_vehicle(admin_client, sample_vehicle_data): """Create a vehicle via API and return the response.""" response = await admin_client.post("/api/v1/vehicles/", json=sample_vehicle_data) assert response.status_code == 201, response.text return response.json() class TestVehicleList: """GET /api/v1/vehicles tests.""" @pytest.mark.asyncio async def test_list_vehicles_returns_200_with_pagination(self, admin_client, created_vehicle): """GET /api/v1/vehicles returns 200 with paginated list.""" response = await admin_client.get("/api/v1/vehicles/?page=1&page_size=20") assert response.status_code == 200 data = response.json() assert "items" in data assert "total" in data assert "page" in data assert "page_size" in data assert data["page"] == 1 assert data["page_size"] == 20 assert data["total"] >= 1 assert len(data["items"]) >= 1 @pytest.mark.asyncio async def test_list_vehicles_filter_by_type(self, admin_client, created_vehicle): """GET /api/v1/vehicles?type=lkw returns filtered results.""" response = await admin_client.get("/api/v1/vehicles/?type=lkw") assert response.status_code == 200 data = response.json() for item in data["items"]: assert item["vehicle_type"] == "lkw" @pytest.mark.asyncio async def test_list_vehicles_filter_by_availability(self, admin_client, created_vehicle): """GET /api/v1/vehicles?availability=available returns filtered results.""" response = await admin_client.get("/api/v1/vehicles/?availability=available") assert response.status_code == 200 data = response.json() for item in data["items"]: assert item["availability"] == "available" @pytest.mark.asyncio async def test_list_vehicles_sort_descending(self, admin_client, created_vehicle): """GET /api/v1/vehicles?sort=-created_at returns sorted results.""" response = await admin_client.get("/api/v1/vehicles/?sort=-created_at") assert response.status_code == 200 data = response.json() if len(data["items"]) >= 2: assert data["items"][0]["created_at"] >= data["items"][1]["created_at"] @pytest.mark.asyncio async def test_list_vehicles_filter_by_price_range(self, admin_client, created_vehicle): """GET /api/v1/vehicles?min_price=40000&max_price=50000 returns filtered results.""" response = await admin_client.get("/api/v1/vehicles/?min_price=40000&max_price=50000") assert response.status_code == 200 data = response.json() for item in data["items"]: assert float(item["price"]) >= 40000 assert float(item["price"]) <= 50000 @pytest.mark.asyncio async def test_list_vehicles_search(self, admin_client, created_vehicle): """GET /api/v1/vehicles?search=Mercedes returns matching results.""" response = await admin_client.get("/api/v1/vehicles/?search=Mercedes") assert response.status_code == 200 data = response.json() for item in data["items"]: assert "Mercedes" in item["make"] or "Mercedes" in item["model"] @pytest.mark.asyncio async def test_list_vehicles_requires_auth(self, client): """GET /api/v1/vehicles without auth returns 401.""" response = await client.get("/api/v1/vehicles/") assert response.status_code == 401 class TestVehicleCreate: """POST /api/v1/vehicles tests.""" @pytest.mark.asyncio async def test_create_vehicle_returns_201(self, admin_client, sample_vehicle_data): """POST /api/v1/vehicles with valid data returns 201.""" response = await admin_client.post("/api/v1/vehicles/", json=sample_vehicle_data) assert response.status_code == 201 data = response.json() assert data["make"] == sample_vehicle_data["make"] assert data["model"] == sample_vehicle_data["model"] assert data["fin"] == sample_vehicle_data["fin"] assert data["vehicle_type"] == "lkw" assert data["id"] is not None @pytest.mark.asyncio async def test_create_vehicle_missing_make_returns_422(self, admin_client, sample_vehicle_data): """POST /api/v1/vehicles without make returns 422.""" del sample_vehicle_data["make"] response = await admin_client.post("/api/v1/vehicles/", json=sample_vehicle_data) assert response.status_code == 422 @pytest.mark.asyncio async def test_create_vehicle_missing_fin_returns_422(self, admin_client, sample_vehicle_data): """POST /api/v1/vehicles without fin returns 422.""" del sample_vehicle_data["fin"] response = await admin_client.post("/api/v1/vehicles/", json=sample_vehicle_data) assert response.status_code == 422 @pytest.mark.asyncio async def test_create_vehicle_short_fin_returns_422(self, admin_client, sample_vehicle_data): """POST /api/v1/vehicles with short FIN returns 422.""" sample_vehicle_data["fin"] = "SHORT" response = await admin_client.post("/api/v1/vehicles/", json=sample_vehicle_data) assert response.status_code == 422 @pytest.mark.asyncio async def test_create_vehicle_duplicate_fin_returns_409(self, admin_client, sample_vehicle_data, created_vehicle): """POST /api/v1/vehicles with duplicate FIN returns 409.""" response = await admin_client.post("/api/v1/vehicles/", json=sample_vehicle_data) assert response.status_code == 409 @pytest.mark.asyncio async def test_create_vehicle_auto_computes_power_hp(self, admin_client, sample_vehicle_data): """POST /api/v1/vehicles auto-computes power_hp from power_kw.""" sample_vehicle_data["power_kw"] = 100 sample_vehicle_data.pop("power_hp", None) response = await admin_client.post("/api/v1/vehicles/", json=sample_vehicle_data) assert response.status_code == 201 data = response.json() assert data["power_hp"] == 136 # 100 * 1.35962 ≈ 136 class TestVehicleDetail: """GET /api/v1/vehicles/:id tests.""" @pytest.mark.asyncio async def test_get_vehicle_returns_200(self, admin_client, created_vehicle): """GET /api/v1/vehicles/:id returns 200 with detail.""" vehicle_id = created_vehicle["id"] response = await admin_client.get(f"/api/v1/vehicles/{vehicle_id}") assert response.status_code == 200 data = response.json() assert data["id"] == vehicle_id assert data["make"] == created_vehicle["make"] @pytest.mark.asyncio async def test_get_vehicle_nonexistent_returns_404(self, admin_client): """GET /api/v1/vehicles/:id with nonexistent ID returns 404.""" fake_id = str(uuid.uuid4()) response = await admin_client.get(f"/api/v1/vehicles/{fake_id}") assert response.status_code == 404 class TestVehicleUpdate: """PUT /api/v1/vehicles/:id tests.""" @pytest.mark.asyncio async def test_update_vehicle_returns_200(self, admin_client, created_vehicle): """PUT /api/v1/vehicles/:id returns 200 with updated data.""" vehicle_id = created_vehicle["id"] response = await admin_client.put( f"/api/v1/vehicles/{vehicle_id}", json={"price": "42000.00", "availability": "reserved"}, ) assert response.status_code == 200 data = response.json() assert float(data["price"]) == 42000.00 assert data["availability"] == "reserved" @pytest.mark.asyncio async def test_update_vehicle_nonexistent_returns_404(self, admin_client): """PUT /api/v1/vehicles/:id with nonexistent ID returns 404.""" fake_id = str(uuid.uuid4()) response = await admin_client.put( f"/api/v1/vehicles/{fake_id}", json={"price": "42000.00"}, ) assert response.status_code == 404 @pytest.mark.asyncio async def test_update_vehicle_no_fields_returns_400(self, admin_client, created_vehicle): """PUT /api/v1/vehicles/:id with no fields returns 400.""" vehicle_id = created_vehicle["id"] response = await admin_client.put( f"/api/v1/vehicles/{vehicle_id}", json={}, ) assert response.status_code == 400 class TestVehicleDelete: """DELETE /api/v1/vehicles/:id tests.""" @pytest.mark.asyncio async def test_delete_vehicle_returns_200_with_deleted_at(self, admin_client, created_vehicle): """DELETE /api/v1/vehicles/:id returns 200 and sets deleted_at.""" vehicle_id = created_vehicle["id"] response = await admin_client.delete(f"/api/v1/vehicles/{vehicle_id}") assert response.status_code == 200 data = response.json() assert data["deleted_at"] is not None @pytest.mark.asyncio async def test_delete_vehicle_nonexistent_returns_404(self, admin_client): """DELETE /api/v1/vehicles/:id with nonexistent ID returns 404.""" fake_id = str(uuid.uuid4()) response = await admin_client.delete(f"/api/v1/vehicles/{fake_id}") assert response.status_code == 404 @pytest.mark.asyncio async def test_deleted_vehicle_not_in_list(self, admin_client, created_vehicle): """After soft-delete, vehicle does not appear in list.""" vehicle_id = created_vehicle["id"] await admin_client.delete(f"/api/v1/vehicles/{vehicle_id}") response = await admin_client.get("/api/v1/vehicles/") assert response.status_code == 200 data = response.json() for item in data["items"]: assert item["id"] != vehicle_id @pytest.mark.asyncio async def test_deleted_vehicle_returns_404_on_detail(self, admin_client, created_vehicle): """After soft-delete, GET /api/v1/vehicles/:id returns 404.""" vehicle_id = created_vehicle["id"] await admin_client.delete(f"/api/v1/vehicles/{vehicle_id}") response = await admin_client.get(f"/api/v1/vehicles/{vehicle_id}") assert response.status_code == 404 class TestMobileDePush: """POST /api/v1/vehicles/:id/mobile-de/push tests.""" @pytest.mark.asyncio async def test_push_returns_202(self, admin_client, created_vehicle): """POST /api/v1/vehicles/:id/mobile-de/push returns 202.""" vehicle_id = created_vehicle["id"] with patch("app.services.mobilede_service.httpx.AsyncClient") as mock_client_cls: mock_response = MagicMock() mock_response.status_code = 201 mock_response.json.return_value = {"id": "mobile-de-ad-123"} mock_response.raise_for_status = MagicMock() mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_cls.return_value = mock_client response = await admin_client.post(f"/api/v1/vehicles/{vehicle_id}/mobile-de/push") assert response.status_code == 202 data = response.json() assert data["message"] == "Push queued" assert data["vehicle_id"] == vehicle_id assert data["listing_id"] is not None @pytest.mark.asyncio async def test_push_nonexistent_vehicle_returns_404(self, admin_client): """POST /api/v1/vehicles/:id/mobile-de/push with nonexistent ID returns 404.""" fake_id = str(uuid.uuid4()) response = await admin_client.post(f"/api/v1/vehicles/{fake_id}/mobile-de/push") assert response.status_code == 404 class TestMobileDeStatus: """GET /api/v1/vehicles/:id/mobile-de/status tests.""" @pytest.mark.asyncio async def test_status_returns_200_with_no_listing(self, admin_client, created_vehicle): """GET /api/v1/vehicles/:id/mobile-de/status returns 200 with pending status when no listing exists.""" vehicle_id = created_vehicle["id"] response = await admin_client.get(f"/api/v1/vehicles/{vehicle_id}/mobile-de/status") assert response.status_code == 200 data = response.json() assert data["synced"] is False assert data["sync_status"] == "pending" @pytest.mark.asyncio async def test_status_returns_200_with_synced_listing(self, admin_client, created_vehicle): """GET /api/v1/vehicles/:id/mobile-de/status returns 200 with sync info after push.""" vehicle_id = created_vehicle["id"] with patch("app.services.mobilede_service.httpx.AsyncClient") as mock_client_cls: mock_response = MagicMock() mock_response.status_code = 201 mock_response.json.return_value = {"id": "mobile-de-ad-456"} mock_response.raise_for_status = MagicMock() mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_cls.return_value = mock_client await admin_client.post(f"/api/v1/vehicles/{vehicle_id}/mobile-de/push") response = await admin_client.get(f"/api/v1/vehicles/{vehicle_id}/mobile-de/status") assert response.status_code == 200 data = response.json() assert data["synced"] is True assert data["ad_id"] == "mobile-de-ad-456" assert data["synced_at"] is not None @pytest.mark.asyncio async def test_status_nonexistent_vehicle_returns_404(self, admin_client): """GET /api/v1/vehicles/:id/mobile-de/status with nonexistent ID returns 404.""" fake_id = str(uuid.uuid4()) response = await admin_client.get(f"/api/v1/vehicles/{fake_id}/mobile-de/status") assert response.status_code == 404