Files

392 lines
15 KiB
Python
Raw Permalink 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