From 2065ff7c8a1b06bcfc27b8dca588da8f4e1551fa Mon Sep 17 00:00:00 2001 From: gilles Date: Sun, 22 Feb 2026 12:23:41 +0100 Subject: [PATCH] =?UTF-8?q?test(backend):=20tests=20endpoint=20/api/identi?= =?UTF-8?q?fy=20=E2=80=94=20PlantNet,=20YOLO=20fallback,=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- backend/tests/test_identify.py | 130 +++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 backend/tests/test_identify.py diff --git a/backend/tests/test_identify.py b/backend/tests/test_identify.py new file mode 100644 index 0000000..bc040a6 --- /dev/null +++ b/backend/tests/test_identify.py @@ -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"] == []