131 lines
4.2 KiB
Python
131 lines
4.2 KiB
Python
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"] == []
|