diff --git a/src/plesna_gerance/api/app.py b/src/plesna_gerance/api/app.py index 8f7b8ea..477122f 100644 --- a/src/plesna_gerance/api/app.py +++ b/src/plesna_gerance/api/app.py @@ -3,7 +3,7 @@ import mimetypes from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles @@ -87,6 +87,13 @@ if FRONTEND_DIST.exists(): @app.get("/{full_path:path}", include_in_schema=False) async def serve_spa(full_path: str): """Serve the SPA for all non-API routes.""" + # Une URL d'API inconnue doit se dire inconnue. Sans cette garde, elle + # tomberait dans le catch-all et repartirait en index.html avec un 200 : + # côté client, une faute de frappe dans une URL ne ressemblerait plus à + # une erreur mais à une réponse vide. + if full_path == "api" or full_path.startswith("api/"): + raise HTTPException(status_code=404, detail="Endpoint inconnu") + # If requesting a file that exists (and stays within dist), serve it file_path = (FRONTEND_DIST / full_path).resolve() if file_path.is_file() and file_path.is_relative_to(_dist_root): diff --git a/tests/test_routage_spa.py b/tests/test_routage_spa.py new file mode 100644 index 0000000..6a9265f --- /dev/null +++ b/tests/test_routage_spa.py @@ -0,0 +1,41 @@ +"""Tests du partage des URL entre l'API et le SPA. + +L'application sert l'API et l'interface sur le meme port : une route attrape +tout ce qui ne correspond a aucun endpoint pour le renvoyer au routeur Vue. Elle +ne doit pas avaler les URL d'API, sous peine de transformer une adresse erronee +en page HTML repondue avec un 200. +""" + +import pytest + +from plesna_gerance.api.app import FRONTEND_DIST + + +def test_une_url_d_api_inconnue_donne_404(api_client): + response = api_client.get("/api/nexiste-pas") + + assert response.status_code == 404 + assert response.headers["content-type"].startswith("application/json") + + +def test_une_url_d_api_inconnue_ne_renvoie_pas_le_spa(api_client): + """Meme forme que les vraies routes : le prefixe seul ne suffit pas.""" + response = api_client.get("/api/revenus/inconnu") + + assert response.status_code == 404 + + +def test_les_routes_d_api_existantes_repondent(api_client): + assert api_client.get("/api/health").status_code == 200 + assert api_client.get("/api/tags").status_code == 200 + + +@pytest.mark.skipif( + not FRONTEND_DIST.exists(), reason="frontend non construit (frontend/dist)" +) +def test_une_route_du_spa_renvoie_l_interface(api_client): + """Les URL de l'interface restent servies par index.html.""" + response = api_client.get("/documents") + + assert response.status_code == 200 + assert response.headers["content-type"].startswith("text/html")