"""Routes HTTP du support de formation (routes/document/) — création, édition directe (sans page intermédiaire), CRUD des éléments en AJAX, publication, et isolation par propriétaire (core/auth_guard.py).""" from typing import Any from flask.testing import FlaskClient import auth import db def test_documents_new_creates_a_support_not_a_game(client: FlaskClient) -> None: resp = client.post("/documents/new", data={"name": "Sécurité incendie"}, follow_redirects=False) assert resp.status_code == 302 assert "/document/" in resp.headers["Location"] assert "/edit" in resp.headers["Location"] slug = resp.headers["Location"].rstrip("/").split("/")[-2] try: assert db.support_meta(slug)["name"] == "Sécurité incendie" finally: db.delete_support(slug) def test_document_edit_renders_directly_no_intermediate_page(client: FlaskClient, support: str) -> None: resp = client.get(f"/document/{support}/edit") assert resp.status_code == 200 def test_document_element_add_returns_rendered_html(client: FlaskClient, support: str) -> None: resp = client.post(f"/document/{support}/elements/add", data={"kind": "titre"}) assert resp.status_code == 200 payload = resp.get_json() assert payload["kind"] == "titre" assert "docText" in payload["rendered_html"] def test_document_element_add_rejects_unknown_kind(client: FlaskClient, support: str) -> None: assert client.post(f"/document/{support}/elements/add", data={"kind": "n-importe-quoi"}).status_code == 400 def test_document_element_add_accepts_row_for_the_layout_engine(client: FlaskClient, support: str) -> None: """ "row" n'apparaît jamais dans la bibliothèque (voir document_engine.ELEMENT_LIBRARY) mais reste un kind valide pour cette route — c'est le moteur de layout côté client qui en crée une à la volée au moment d'un dépôt latéral (voir static/document/js/document-editor.js).""" resp = client.post(f"/document/{support}/elements/add", data={"kind": "row"}) assert resp.status_code == 200 assert resp.get_json()["kind"] == "row" def test_document_element_update_persists_attributes(client: FlaskClient, support: str) -> None: add_resp = client.post(f"/document/{support}/elements/add", data={"kind": "paragraphe"}) element_id = add_resp.get_json()["id"] resp = client.post( f"/document/{support}/elements/{element_id}/update", json={"content": "Texte mis à jour", "style": "legende"}, ) assert resp.status_code == 200 assert resp.get_json()["ok"] is True reread = client.post(f"/document/{support}/elements/add", data={"kind": "titre"}) assert reread.status_code == 200 def test_document_element_update_sanitizes_quiz_config_on_write(client: FlaskClient, support: str) -> None: """Quiz et Association sont revalidés côté serveur (voir routes/document/document_element_update.py) : un choix en trop (5e) doit être tronqué, et la réponse renvoie les attributs RÉELLEMENT persistés (pas ceux envoyés tels quels) pour que le client ne dérive jamais de la vérité serveur.""" add_resp = client.post(f"/document/{support}/elements/add", data={"kind": "quiz"}) element_id = add_resp.get_json()["id"] resp = client.post( f"/document/{support}/elements/{element_id}/update", json={ "theme_color": "#ff5f2e", "timer_enabled": False, "timer_seconds": 30, "questions": [ {"text": "Une question", "choices": ["A", "B", "C", "D", "E"], "correct_index": 0, "points": 10}, ], }, ) assert resp.status_code == 200 payload = resp.get_json() assert payload["ok"] is True assert payload["attributes"]["questions"][0]["choices"] == ["A", "B", "C", "D"] assert "1 question" in payload["rendered_html"] def test_document_element_update_sanitizes_association_config_on_write(client: FlaskClient, support: str) -> None: add_resp = client.post(f"/document/{support}/elements/add", data={"kind": "association"}) element_id = add_resp.get_json()["id"] resp = client.post( f"/document/{support}/elements/{element_id}/update", json={ "theme_color": "#ffb020", "pairs": [ {"left": "Chat", "right": "Un félin"}, {"left": "Sans correspondance", "right": ""}, ], }, ) assert resp.status_code == 200 payload = resp.get_json() assert payload["ok"] is True assert payload["attributes"]["pairs"] == [{"left": "Chat", "right": "Un félin"}] assert "1 paire" in payload["rendered_html"] def test_document_element_update_sanitizes_memory_config_on_write(client: FlaskClient, support: str) -> None: add_resp = client.post(f"/document/{support}/elements/add", data={"kind": "memory"}) element_id = add_resp.get_json()["id"] resp = client.post( f"/document/{support}/elements/{element_id}/update", json={ "theme_color": "#ffb020", "mode": "single", "cards": [ {"recto": {"text": "?"}, "verso": {"text": "Chat"}}, {"recto": {"text": "?"}, "verso": {"image": "", "text": ""}}, ], }, ) assert resp.status_code == 200 payload = resp.get_json() assert payload["ok"] is True assert payload["attributes"]["mode"] == "single" assert len(payload["attributes"]["cards"]) == 1 assert "1 carte" in payload["rendered_html"] assert "mode simple" in payload["rendered_html"] def test_document_element_move_and_delete(client: FlaskClient, support: str) -> None: first_id = client.post(f"/document/{support}/elements/add", data={"kind": "titre"}).get_json()["id"] second_id = client.post(f"/document/{support}/elements/add", data={"kind": "paragraphe"}).get_json()["id"] move_resp = client.post( f"/document/{support}/elements/{second_id}/move", json={"new_parent_id": None, "new_index": 0}, ) assert move_resp.status_code == 200 assert move_resp.get_json()["ok"] is True delete_resp = client.post(f"/document/{support}/elements/{first_id}/delete") assert delete_resp.status_code == 200 assert delete_resp.get_json()["ok"] is True def test_restricted_user_can_have_one_game_and_one_support_at_once( user_client: FlaskClient, tmp_game_slug_cleanup: Any, tmp_support_slug_cleanup: Any ) -> None: with user_client.session_transaction() as sess: user_id = sess["user_id"] game_slug = db.create_game("Un environnement 2D", owner_folder=str(user_id)) tmp_game_slug_cleanup(game_slug) auth.set_project_slug(user_id, game_slug) resp = user_client.post("/documents/new", data={"name": "Un support"}, follow_redirects=False) assert resp.status_code == 302 support_slug = resp.headers["Location"].rstrip("/").split("/")[-2] tmp_support_slug_cleanup(support_slug) user = auth.get_user_by_id(user_id) assert user is not None assert user["project_slug"] == game_slug assert user["support_slug"] == support_slug def test_cannot_open_another_owners_support(client: FlaskClient, user_client: FlaskClient) -> None: """Isolation par propriétaire (core/auth_guard.py, split_slug déjà générique quel que soit le type de projet) — le compte admin (`client`) ne doit pas pouvoir ouvrir le support d'un autre compte.""" with user_client.session_transaction() as sess: victim_user_id = sess["user_id"] victim_slug = db.create_support("Support de la victime", owner_folder=str(victim_user_id)) try: assert client.get(f"/document/{victim_slug}/edit").status_code == 403 assert client.post(f"/document/{victim_slug}/delete").status_code == 403 finally: db.delete_support(victim_slug)