test(backend): tests endpoint /api/identify — PlantNet, YOLO fallback, cache

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 12:23:41 +01:00
parent 883299272f
commit 2065ff7c8a

View File

@@ -0,0 +1,130 @@
from unittest.mock import AsyncMock, patch
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
# Bytes JPEG minimaux valides (header JFIF)
FAKE_IMAGE = (
b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"
b"\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\t\t"
b"\x08\n\x0c\x14\r\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a"
b"\x1f\x1e\x1d\x1a\x1c\x1c $.' \",#\x1c\x1c(7),01444\x1f'9=82<.342\x1e"
b"\xff\xc0\x00\x0b\x08\x00\x01\x00\x01\x01\x01\x11\x00"
b"\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b"
b"\xff\xda\x00\x08\x01\x01\x00\x00?\x00\xf5\x00\xff\xd9"
)
def test_identify_returns_plantnet_results():
"""PlantNet disponible → retourne ses résultats avec source=plantnet."""
fake_results = [
{
"species": "Solanum lycopersicum",
"common_name": "Tomate",
"confidence": 0.95,
"image_url": "",
}
]
with (
patch("app.services.redis_cache.get", return_value=None),
patch("app.services.redis_cache.set"),
patch(
"app.services.plantnet.identify",
new_callable=AsyncMock,
return_value=fake_results,
),
):
resp = client.post(
"/api/identify",
files={"file": ("test.jpg", FAKE_IMAGE, "image/jpeg")},
)
assert resp.status_code == 200
data = resp.json()
assert data["source"] == "plantnet"
assert len(data["results"]) == 1
assert data["results"][0]["species"] == "Solanum lycopersicum"
assert data["results"][0]["confidence"] == 0.95
def test_identify_falls_back_to_yolo_when_plantnet_fails():
"""PlantNet indisponible (retourne []) → fallback YOLO avec source=yolo."""
fake_yolo = [
{
"species": "Tomato healthy",
"common_name": "Tomate",
"confidence": 0.80,
"image_url": "",
}
]
with (
patch("app.services.redis_cache.get", return_value=None),
patch("app.services.redis_cache.set"),
patch(
"app.services.plantnet.identify",
new_callable=AsyncMock,
return_value=[],
),
patch(
"app.services.yolo_service.identify",
new_callable=AsyncMock,
return_value=fake_yolo,
),
):
resp = client.post(
"/api/identify",
files={"file": ("test.jpg", FAKE_IMAGE, "image/jpeg")},
)
assert resp.status_code == 200
data = resp.json()
assert data["source"] == "yolo"
assert len(data["results"]) == 1
assert data["results"][0]["common_name"] == "Tomate"
def test_identify_uses_cache():
"""Cache Redis hit → retourne les données en cache sans appeler PlantNet."""
cached = [
{
"species": "Rosa canina",
"common_name": "Églantier",
"confidence": 0.9,
"image_url": "",
}
]
with patch("app.services.redis_cache.get", return_value=cached):
resp = client.post(
"/api/identify",
files={"file": ("test.jpg", FAKE_IMAGE, "image/jpeg")},
)
assert resp.status_code == 200
data = resp.json()
assert data["source"] == "cache"
assert data["results"][0]["species"] == "Rosa canina"
def test_identify_returns_empty_when_both_fail():
"""PlantNet et YOLO indisponibles → retourne résultats vides."""
with (
patch("app.services.redis_cache.get", return_value=None),
patch("app.services.redis_cache.set"),
patch(
"app.services.plantnet.identify",
new_callable=AsyncMock,
return_value=[],
),
patch(
"app.services.yolo_service.identify",
new_callable=AsyncMock,
return_value=[],
),
):
resp = client.post(
"/api/identify",
files={"file": ("test.jpg", FAKE_IMAGE, "image/jpeg")},
)
assert resp.status_code == 200
data = resp.json()
assert data["source"] == "yolo"
assert data["results"] == []