feat(backend): CRUD jardins + tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,17 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
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")
|
@app.get("/api/health")
|
||||||
def health():
|
def health():
|
||||||
|
|||||||
82
backend/app/routers/gardens.py
Normal file
82
backend/app/routers/gardens.py
Normal file
@@ -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
|
||||||
2
backend/app/routers/media.py
Normal file
2
backend/app/routers/media.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
router = APIRouter()
|
||||||
2
backend/app/routers/plantings.py
Normal file
2
backend/app/routers/plantings.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
router = APIRouter()
|
||||||
2
backend/app/routers/settings.py
Normal file
2
backend/app/routers/settings.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
router = APIRouter()
|
||||||
2
backend/app/routers/tasks.py
Normal file
2
backend/app/routers/tasks.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
router = APIRouter()
|
||||||
2
backend/app/routers/varieties.py
Normal file
2
backend/app/routers/varieties.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
router = APIRouter()
|
||||||
31
backend/tests/conftest.py
Normal file
31
backend/tests/conftest.py
Normal file
@@ -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()
|
||||||
54
backend/tests/test_gardens.py
Normal file
54
backend/tests/test_gardens.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user