Files
erp-nutzfahrzeuge/backend/tests/test_vehicles.py
T

392 lines
15 KiB
Python
Raw Normal View History

"""Tests for vehicle CRUD endpoints and mobile.de integration."""
import uuid
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import pytest_asyncio
@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