OpenMind v1 : cartes mentales libres, local-first et dockerisees
Premiere version fonctionnelle, demarrable avec `docker compose up -d --build` sur http://localhost:8080. Architecture - Frontend React 18 / TypeScript strict / Vite / Tailwind, canvas @xyflow/react, etat Zustand, persistance IndexedDB via Dexie, auto-layout dagre. - Backend FastAPI / SQLAlchemy 2 / Pydantic v2 / SQLite, qui sert aussi les fichiers statiques du frontend compile. - Image Docker multi-stage, un seul service, bind mount ./data pour SQLite. Local-first Les cartes restent dans le navigateur. Le serveur ne recoit une carte que sur demande explicite de partage. Partage Deux secrets distincts : share_token public en lecture seule dans l'URL, et management_token conserve uniquement cote client, transmis en en-tete pour les modifications et stocke hashe en base. Fonctionnalites Tableau de bord, editeur complet (undo/redo, copier/coller, raccourcis, branches repliables, recherche), 9 themes, 4 dispositions automatiques, panneau de proprietes, import/export .mmap, export PNG/SVG/PDF, impression, page publique en lecture seule, PWA. Qualite 25 tests pytest et 43 tests vitest au vert, ruff et ESLint sans erreur, TypeScript strict sans erreur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
78
backend/tests/test_validation.py
Normal file
78
backend/tests/test_validation.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Tests des garde-fous : payload invalide, limites de taille et de noeuds."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.config import get_settings
|
||||
from tests.conftest import build_map
|
||||
|
||||
|
||||
def test_missing_map_returns_422(client: TestClient) -> None:
|
||||
response = client.post("/api/shares", json={"title": "Sans carte"})
|
||||
assert response.status_code == 422
|
||||
assert "carte valide" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_map_without_version_returns_422(client: TestClient) -> None:
|
||||
payload = build_map()
|
||||
del payload["version"]
|
||||
response = client.post("/api/shares", json={"title": "x", "map": payload})
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_node_without_id_returns_422(client: TestClient) -> None:
|
||||
payload = build_map()
|
||||
del payload["nodes"][0]["id"]
|
||||
response = client.post("/api/shares", json={"title": "x", "map": payload})
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_unknown_top_level_field_is_rejected(client: TestClient) -> None:
|
||||
response = client.post(
|
||||
"/api/shares", json={"title": "x", "map": build_map(), "is_premium": True}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_garbage_body_returns_422(client: TestClient) -> None:
|
||||
response = client.post("/api/shares", json={"map": "ceci n est pas une carte"})
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_node_count_limit_is_enforced(client: TestClient) -> None:
|
||||
os.environ["MAX_NODES"] = "10"
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
response = client.post("/api/shares", json={"title": "x", "map": build_map(node_count=25)})
|
||||
assert response.status_code == 422
|
||||
assert "10" in response.json()["detail"]
|
||||
finally:
|
||||
os.environ["MAX_NODES"] = "5000"
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_map_size_limit_is_enforced(client: TestClient) -> None:
|
||||
os.environ["MAX_MAP_SIZE_MB"] = "0.001"
|
||||
get_settings.cache_clear()
|
||||
try:
|
||||
response = client.post("/api/shares", json={"title": "x", "map": build_map(node_count=60)})
|
||||
assert response.status_code == 413
|
||||
assert "volumineuse" in response.json()["detail"] or "limite" in response.json()["detail"]
|
||||
finally:
|
||||
os.environ["MAX_MAP_SIZE_MB"] = "10"
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_node_text_is_stored_as_plain_text(client: TestClient) -> None:
|
||||
"""Le backend stocke le texte tel quel : il ne l interprete jamais comme du HTML."""
|
||||
payload = build_map()
|
||||
payload["nodes"][0]["text"] = "<script>alert(1)</script>"
|
||||
created = client.post("/api/shares", json={"title": "x", "map": payload})
|
||||
assert created.status_code == 201
|
||||
|
||||
public = client.get("/api/shares/" + created.json()["share_token"]).json()
|
||||
assert public["map"]["nodes"][0]["text"] == "<script>alert(1)</script>"
|
||||
assert public["map"]["nodes"][0]["type"] == "root"
|
||||
Reference in New Issue
Block a user