From aa379aa1b4200149fc12f67408c742133b68e0f5 Mon Sep 17 00:00:00 2001 From: gilles Date: Sat, 21 Feb 2026 21:20:02 +0100 Subject: [PATCH] feat(backend): CRUD jardins + tests Co-Authored-By: Claude Sonnet 4.6 --- backend/app/main.py | 11 +++++ backend/app/routers/gardens.py | 82 ++++++++++++++++++++++++++++++++ backend/app/routers/media.py | 2 + backend/app/routers/plantings.py | 2 + backend/app/routers/settings.py | 2 + backend/app/routers/tasks.py | 2 + backend/app/routers/varieties.py | 2 + backend/tests/conftest.py | 31 ++++++++++++ backend/tests/test_gardens.py | 54 +++++++++++++++++++++ 9 files changed, 188 insertions(+) create mode 100644 backend/app/routers/gardens.py create mode 100644 backend/app/routers/media.py create mode 100644 backend/app/routers/plantings.py create mode 100644 backend/app/routers/settings.py create mode 100644 backend/app/routers/tasks.py create mode 100644 backend/app/routers/varieties.py create mode 100644 backend/tests/conftest.py create mode 100644 backend/tests/test_gardens.py diff --git a/backend/app/main.py b/backend/app/main.py index 577dc94..b694c1b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -27,6 +27,17 @@ app.add_middleware( allow_headers=["*"], ) +from app.routers import gardens, varieties, plantings, tasks, settings, media + +app.include_router(gardens.router, prefix="/api") +app.include_router(varieties.router, prefix="/api") +app.include_router(plantings.router, prefix="/api") +app.include_router(tasks.router, prefix="/api") +app.include_router(settings.router, prefix="/api") +app.include_router(media.router, prefix="/api") + +# Note: le mount StaticFiles sera ajouté ici dans Task 6 + @app.get("/api/health") def health(): diff --git a/backend/app/routers/gardens.py b/backend/app/routers/gardens.py new file mode 100644 index 0000000..b6938d7 --- /dev/null +++ b/backend/app/routers/gardens.py @@ -0,0 +1,82 @@ +from datetime import datetime +from typing import List + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlmodel import Session, select + +from app.database import get_session +from app.models.garden import Garden, GardenCell, GardenImage, Measurement + +router = APIRouter(tags=["jardins"]) + + +@router.get("/gardens", response_model=List[Garden]) +def list_gardens(session: Session = Depends(get_session)): + return session.exec(select(Garden)).all() + + +@router.post("/gardens", response_model=Garden, status_code=status.HTTP_201_CREATED) +def create_garden(garden: Garden, session: Session = Depends(get_session)): + session.add(garden) + session.commit() + session.refresh(garden) + return garden + + +@router.get("/gardens/{id}", response_model=Garden) +def get_garden(id: int, session: Session = Depends(get_session)): + g = session.get(Garden, id) + if not g: + raise HTTPException(status_code=404, detail="Jardin introuvable") + return g + + +@router.put("/gardens/{id}", response_model=Garden) +def update_garden(id: int, data: Garden, session: Session = Depends(get_session)): + g = session.get(Garden, id) + if not g: + raise HTTPException(status_code=404, detail="Jardin introuvable") + for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items(): + setattr(g, k, v) + g.updated_at = datetime.utcnow() + session.add(g) + session.commit() + session.refresh(g) + return g + + +@router.delete("/gardens/{id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_garden(id: int, session: Session = Depends(get_session)): + g = session.get(Garden, id) + if not g: + raise HTTPException(status_code=404, detail="Jardin introuvable") + session.delete(g) + session.commit() + + +@router.get("/gardens/{id}/cells", response_model=List[GardenCell]) +def list_cells(id: int, session: Session = Depends(get_session)): + return session.exec(select(GardenCell).where(GardenCell.garden_id == id)).all() + + +@router.post("/gardens/{id}/cells", response_model=GardenCell, status_code=status.HTTP_201_CREATED) +def create_cell(id: int, cell: GardenCell, session: Session = Depends(get_session)): + cell.garden_id = id + session.add(cell) + session.commit() + session.refresh(cell) + return cell + + +@router.get("/gardens/{id}/measurements", response_model=List[Measurement]) +def list_measurements(id: int, session: Session = Depends(get_session)): + return session.exec(select(Measurement).where(Measurement.garden_id == id)).all() + + +@router.post("/gardens/{id}/measurements", response_model=Measurement, status_code=status.HTTP_201_CREATED) +def create_measurement(id: int, m: Measurement, session: Session = Depends(get_session)): + m.garden_id = id + session.add(m) + session.commit() + session.refresh(m) + return m diff --git a/backend/app/routers/media.py b/backend/app/routers/media.py new file mode 100644 index 0000000..da060ab --- /dev/null +++ b/backend/app/routers/media.py @@ -0,0 +1,2 @@ +from fastapi import APIRouter +router = APIRouter() diff --git a/backend/app/routers/plantings.py b/backend/app/routers/plantings.py new file mode 100644 index 0000000..da060ab --- /dev/null +++ b/backend/app/routers/plantings.py @@ -0,0 +1,2 @@ +from fastapi import APIRouter +router = APIRouter() diff --git a/backend/app/routers/settings.py b/backend/app/routers/settings.py new file mode 100644 index 0000000..da060ab --- /dev/null +++ b/backend/app/routers/settings.py @@ -0,0 +1,2 @@ +from fastapi import APIRouter +router = APIRouter() diff --git a/backend/app/routers/tasks.py b/backend/app/routers/tasks.py new file mode 100644 index 0000000..da060ab --- /dev/null +++ b/backend/app/routers/tasks.py @@ -0,0 +1,2 @@ +from fastapi import APIRouter +router = APIRouter() diff --git a/backend/app/routers/varieties.py b/backend/app/routers/varieties.py new file mode 100644 index 0000000..da060ab --- /dev/null +++ b/backend/app/routers/varieties.py @@ -0,0 +1,2 @@ +from fastapi import APIRouter +router = APIRouter() diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..2295634 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,31 @@ +import pytest +from fastapi.testclient import TestClient +from sqlmodel import SQLModel, create_engine, Session +from sqlmodel.pool import StaticPool + +import app.models # noqa — force l'enregistrement des modèles +from app.main import app +from app.database import get_session + + +@pytest.fixture(name="session") +def session_fixture(): + engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +@pytest.fixture(name="client") +def client_fixture(session: Session): + def get_session_override(): + yield session + + app.dependency_overrides[get_session] = get_session_override + client = TestClient(app) + yield client + app.dependency_overrides.clear() diff --git a/backend/tests/test_gardens.py b/backend/tests/test_gardens.py new file mode 100644 index 0000000..d1be02d --- /dev/null +++ b/backend/tests/test_gardens.py @@ -0,0 +1,54 @@ +def test_health(client): + r = client.get("/api/health") + assert r.status_code == 200 + + +def test_create_garden(client): + r = client.post("/api/gardens", json={"nom": "Mon potager", "type": "plein_air"}) + assert r.status_code == 201 + data = r.json() + assert data["nom"] == "Mon potager" + assert data["id"] is not None + + +def test_list_gardens(client): + client.post("/api/gardens", json={"nom": "Jardin 1", "type": "plein_air"}) + client.post("/api/gardens", json={"nom": "Jardin 2", "type": "serre"}) + r = client.get("/api/gardens") + assert r.status_code == 200 + assert len(r.json()) == 2 + + +def test_get_garden(client): + r = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}) + id = r.json()["id"] + r2 = client.get(f"/api/gardens/{id}") + assert r2.status_code == 200 + assert r2.json()["nom"] == "Potager" + + +def test_update_garden(client): + r = client.post("/api/gardens", json={"nom": "Vieux nom", "type": "plein_air"}) + id = r.json()["id"] + r2 = client.put(f"/api/gardens/{id}", json={"nom": "Nouveau nom", "type": "serre"}) + assert r2.status_code == 200 + assert r2.json()["nom"] == "Nouveau nom" + + +def test_delete_garden(client): + r = client.post("/api/gardens", json={"nom": "À supprimer", "type": "plein_air"}) + id = r.json()["id"] + r2 = client.delete(f"/api/gardens/{id}") + assert r2.status_code == 204 + r3 = client.get(f"/api/gardens/{id}") + assert r3.status_code == 404 + + +def test_create_measurement(client): + r = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}) + id = r.json()["id"] + r2 = client.post( + f"/api/gardens/{id}/measurements", + json={"temp_air": 22.5, "humidite_air": 60.0}, + ) + assert r2.status_code == 201