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:
109
backend/tests/test_shares.py
Normal file
109
backend/tests/test_shares.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from tests.conftest import build_map
|
||||
|
||||
|
||||
def test_create_share_returns_two_distinct_tokens(client: TestClient) -> None:
|
||||
response = client.post("/api/shares", json={"title": "Homelab", "map": build_map()})
|
||||
assert response.status_code == 201, response.text
|
||||
body = response.json()
|
||||
|
||||
assert body["share_token"]
|
||||
assert body["management_token"]
|
||||
assert body["share_token"] != body["management_token"]
|
||||
# Entropie suffisante : token_urlsafe(12) et token_urlsafe(32).
|
||||
assert len(body["share_token"]) >= 16
|
||||
assert len(body["management_token"]) >= 40
|
||||
assert body["share_path"] == "/s/" + body["share_token"]
|
||||
|
||||
|
||||
def test_read_share_returns_map_without_management_token(
|
||||
client: TestClient, created_share: dict
|
||||
) -> None:
|
||||
response = client.get("/api/shares/" + created_share["share_token"])
|
||||
assert response.status_code == 200
|
||||
|
||||
body = response.json()
|
||||
assert body["title"] == "Carte de test"
|
||||
assert body["map_version"] == 1
|
||||
assert len(body["map"]["nodes"]) == 3
|
||||
assert "management_token" not in body
|
||||
assert created_share["management_token"] not in response.text
|
||||
|
||||
|
||||
def test_read_unknown_share_returns_404(client: TestClient) -> None:
|
||||
response = client.get("/api/shares/inexistant-token")
|
||||
assert response.status_code == 404
|
||||
assert "existe pas" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_update_share_with_valid_management_token(client: TestClient, created_share: dict) -> None:
|
||||
updated_map = build_map(node_count=6, name="Homelab v2")
|
||||
response = client.put(
|
||||
"/api/shares/" + created_share["share_token"],
|
||||
json={"title": "Homelab v2", "map": updated_map},
|
||||
headers={"X-Management-Token": created_share["management_token"]},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["title"] == "Homelab v2"
|
||||
|
||||
public = client.get("/api/shares/" + created_share["share_token"]).json()
|
||||
assert len(public["map"]["nodes"]) == 6
|
||||
assert public["title"] == "Homelab v2"
|
||||
|
||||
|
||||
def test_update_share_with_wrong_management_token_is_refused(
|
||||
client: TestClient, created_share: dict
|
||||
) -> None:
|
||||
response = client.put(
|
||||
"/api/shares/" + created_share["share_token"],
|
||||
json={"title": "Pirate", "map": build_map()},
|
||||
headers={"X-Management-Token": "mauvais-jeton"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
public = client.get("/api/shares/" + created_share["share_token"]).json()
|
||||
assert public["title"] == "Carte de test"
|
||||
|
||||
|
||||
def test_update_share_without_management_token_is_refused(
|
||||
client: TestClient, created_share: dict
|
||||
) -> None:
|
||||
response = client.put(
|
||||
"/api/shares/" + created_share["share_token"],
|
||||
json={"title": "Pirate", "map": build_map()},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_share_token_alone_cannot_delete(client: TestClient, created_share: dict) -> None:
|
||||
response = client.delete(
|
||||
"/api/shares/" + created_share["share_token"],
|
||||
headers={"X-Management-Token": created_share["share_token"]},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert client.get("/api/shares/" + created_share["share_token"]).status_code == 200
|
||||
|
||||
|
||||
def test_delete_share_then_read_returns_404(client: TestClient, created_share: dict) -> None:
|
||||
response = client.delete(
|
||||
"/api/shares/" + created_share["share_token"],
|
||||
headers={"X-Management-Token": created_share["management_token"]},
|
||||
)
|
||||
assert response.status_code == 204
|
||||
assert client.get("/api/shares/" + created_share["share_token"]).status_code == 404
|
||||
|
||||
|
||||
def test_delete_unknown_share_returns_404(client: TestClient) -> None:
|
||||
response = client.delete(
|
||||
"/api/shares/inexistant", headers={"X-Management-Token": "peu-importe"}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_share_tokens_are_unique(client: TestClient) -> None:
|
||||
tokens = set()
|
||||
for _ in range(15):
|
||||
body = client.post("/api/shares", json={"title": "x", "map": build_map()}).json()
|
||||
tokens.add(body["share_token"])
|
||||
assert len(tokens) == 15
|
||||
Reference in New Issue
Block a user