hasari-api / services /backend /tests /test_inspect_sync.py
erdoganpeker's picture
v0.3.0 β€” multimodal vehicle damage MVP
e327f0d
ο»Ώ"""
Sync inspection E2E testleri.
Akis: register/login -> /api/v1/inspect/sync -> response sema dogrulamasi
(parts[], summary, total_cost_range_tl)
ML servisi conftest'te mocklanmistir; gercek YOLO calismaz, sabit JSON doner.
"""
from __future__ import annotations
from typing import Any
import httpx
import pytest
from models import SyncInspectionResponse
# ---------------- yardimcilar ----------------
def _install_rich_ml_mock(monkeypatch) -> None:
"""ML mock'unu parts dolu bir sonuc dondurecek sekilde gucverin β€”
response sema dogrulamasini gercek bir aggregate sonucu uzerinden yap.
"""
from ml_service import ml_pipeline
rich = {
"inspection_id": "will-be-overwritten",
"timestamp": "2026-05-16T00:00:00Z",
"image": {"url": "test://img", "width": 640, "height": 480},
"parts": [
{
"name": "front_bumper",
"name_tr": "On tampon",
"confidence": 0.92,
"status": "moderate_damage",
"damage_count": 1,
"polygon_normalized": [],
"bbox": [10.0, 10.0, 100.0, 100.0],
"damages": [
{
"id": 1,
"type": "dent",
"type_tr": "Gocuk",
"confidence": 0.88,
"severity": {
"level": "orta",
"level_tr": "Orta",
"confidence": 0.81,
"method": "rule",
},
"bbox": [12.0, 12.0, 80.0, 80.0],
"polygon_normalized": [],
"area_ratio": 0.02,
"cost": {
"min_tl": 1500.0,
"max_tl": 3500.0,
"midpoint_tl": 2500.0,
"confidence": "medium",
"source": "cost_table.yaml",
},
}
],
"part_cost_min_tl": 1500.0,
"part_cost_max_tl": 3500.0,
"cost_note": None,
}
],
"summary": {
"total_parts_inspected": 1,
"damaged_parts_count": 1,
"clean_parts_count": 0,
"total_damage_count": 1,
"unknown_part_damages_count": 0,
"multi_part_damages_count": 0,
"most_severe_level": "orta",
"most_severe_level_tr": "Orta",
"total_damage_area_ratio": 0.02,
"total_cost_range_tl": [1500.0, 3500.0],
"total_cost_midpoint_tl": 2500.0,
"cost_confidence": "medium",
"repair_recommendation": "tamir_boya",
"repair_recommendation_tr": "Tamir + boya gerekli",
"estimated_repair_days": 1,
},
}
monkeypatch.setattr(ml_pipeline, "analyze", lambda img, retries=2: dict(rich))
def _bearer(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
# ---------------- tests ----------------
async def test_sync_single_image_returns_valid_schema(
async_client: httpx.AsyncClient, png_bytes: bytes, monkeypatch,
reset_user_store, reset_inspection_store
):
_install_rich_ml_mock(monkeypatch)
# Register
reg = await async_client.post(
"/auth/register",
json={"email": "syncuser@test.example.com", "password": "strong-pass-1234"},
)
token = reg.json()["access_token"]
# POST sync β€” multipart upload
files = {"file": ("car.png", png_bytes, "image/png")}
r = await async_client.post(
"/api/v1/inspect/sync",
files=files,
headers=_bearer(token),
)
assert r.status_code == 200, r.text
# Pydantic ile sema dogrulamasi β€” SyncInspectionResponse validate eder
body = r.json()
parsed = SyncInspectionResponse.model_validate(body)
# Top-level fields
assert parsed.inspection_id
assert parsed.processed_at
assert parsed.result is not None
# parts[]
assert len(parsed.result.parts) == 1
part = parsed.result.parts[0]
assert part.name == "front_bumper"
assert part.status == "moderate_damage"
assert part.damage_count == 1
assert len(part.damages) == 1
# summary
summary = parsed.result.summary
assert summary.total_parts_inspected == 1
assert summary.damaged_parts_count == 1
assert summary.total_damage_count == 1
assert summary.most_severe_level == "orta"
# total_cost_range_tl β€” sema gerektirir
cost_range = summary.total_cost_range_tl
assert isinstance(cost_range, tuple)
assert len(cost_range) == 2
assert cost_range[0] == 1500.0
assert cost_range[1] == 3500.0
assert summary.total_cost_midpoint_tl == 2500.0
async def test_sync_response_has_x_inspection_id_header(
async_client: httpx.AsyncClient, png_bytes: bytes,
reset_user_store, reset_inspection_store
):
reg = await async_client.post(
"/auth/register",
json={"email": "hdr@test.example.com", "password": "strong-pass-1234"},
)
token = reg.json()["access_token"]
files = {"file": ("a.png", png_bytes, "image/png")}
r = await async_client.post(
"/api/v1/inspect/sync",
files=files,
headers=_bearer(token),
)
assert r.status_code == 200
# main.py _process_sync header'i ekliyor β€” ama /sync wrapper'in JSONResponse'unda
# body dondurur. Hala JSON icinde inspection_id olmali.
assert r.json()["inspection_id"]
async def test_sync_inspect_multi_endpoint_with_sync_mode(
async_client: httpx.AsyncClient, png_bytes: bytes, monkeypatch,
reset_user_store, reset_inspection_store
):
"""/api/v1/inspect?mode=sync de calismali β€” ayni semayi dondurmeli."""
_install_rich_ml_mock(monkeypatch)
reg = await async_client.post(
"/auth/register",
json={"email": "msync@test.example.com", "password": "strong-pass-1234"},
)
token = reg.json()["access_token"]
files = [("files", ("a.png", png_bytes, "image/png"))]
r = await async_client.post(
"/api/v1/inspect?mode=sync",
files=files,
headers=_bearer(token),
)
assert r.status_code == 200, r.text
body = r.json()
assert "inspection_id" in body
assert "result" in body
assert body["result"]["summary"]["total_cost_range_tl"] == [1500.0, 3500.0]
async def test_sync_rejects_empty_file(
async_client: httpx.AsyncClient,
reset_user_store, reset_inspection_store
):
reg = await async_client.post(
"/auth/register",
json={"email": "empty@test.example.com", "password": "strong-pass-1234"},
)
token = reg.json()["access_token"]
files = {"file": ("empty.png", b"", "image/png")}
r = await async_client.post(
"/api/v1/inspect/sync",
files=files,
headers=_bearer(token),
)
assert r.status_code == 400
async def test_sync_rejects_non_image_mime(
async_client: httpx.AsyncClient,
reset_user_store, reset_inspection_store
):
reg = await async_client.post(
"/auth/register",
json={"email": "txt@test.example.com", "password": "strong-pass-1234"},
)
token = reg.json()["access_token"]
files = {"file": ("doc.txt", b"hello world content", "text/plain")}
r = await async_client.post(
"/api/v1/inspect/sync",
files=files,
headers=_bearer(token),
)
assert r.status_code == 400