"""Modèle de données du mini-jeu Quiz (document_engine/labels/quiz_config.py) — sans Flask, teste directement sanitize_quiz_config/quiz_total_points.""" from typing import Any import document_engine as doc_engine def test_sanitize_quiz_config_on_missing_input_returns_full_defaults() -> None: config = doc_engine.sanitize_quiz_config(None) assert config == { "theme_color": "#ff5f2e", "timer_enabled": False, "timer_seconds": 30, "questions": [], } def test_sanitize_quiz_config_keeps_a_valid_question_as_is() -> None: raw = { "theme_color": "#123456", "timer_enabled": True, "timer_seconds": 45, "questions": [ {"text": "2 + 2 ?", "choices": ["3", "4", "5"], "correct_index": 1, "points": 10}, ], } config = doc_engine.sanitize_quiz_config(raw) assert config["theme_color"] == "#123456" assert config["timer_enabled"] is True assert config["timer_seconds"] == 45 assert config["questions"] == [{"text": "2 + 2 ?", "choices": ["3", "4", "5"], "correct_index": 1, "points": 10}] def test_sanitize_quiz_config_drops_a_question_with_empty_text() -> None: raw = {"questions": [{"text": " ", "choices": ["A", "B"], "correct_index": 0, "points": 5}]} assert doc_engine.sanitize_quiz_config(raw)["questions"] == [] def test_sanitize_quiz_config_drops_a_question_with_fewer_than_two_choices() -> None: raw = {"questions": [{"text": "Une question", "choices": ["Seul choix"], "correct_index": 0, "points": 5}]} assert doc_engine.sanitize_quiz_config(raw)["questions"] == [] def test_sanitize_quiz_config_drops_blank_choices_before_counting() -> None: raw = {"questions": [{"text": "Une question", "choices": ["A", " ", ""], "correct_index": 0, "points": 5}]} assert doc_engine.sanitize_quiz_config(raw)["questions"] == [] def test_sanitize_quiz_config_caps_choices_at_four() -> None: raw = { "questions": [ {"text": "Une question", "choices": ["A", "B", "C", "D", "E"], "correct_index": 0, "points": 5}, ], } config = doc_engine.sanitize_quiz_config(raw) assert config["questions"][0]["choices"] == ["A", "B", "C", "D"] def test_sanitize_quiz_config_resets_out_of_range_correct_index() -> None: raw = {"questions": [{"text": "Une question", "choices": ["A", "B"], "correct_index": 7, "points": 5}]} config = doc_engine.sanitize_quiz_config(raw) assert config["questions"][0]["correct_index"] == 0 def test_sanitize_quiz_config_rejects_a_boolean_as_correct_index() -> None: """True/False sont des int en Python (bool hérite de int) — un JSON malveillant/malformé pourrait glisser `true` là où un index est attendu, ce qui vaudrait 1 après coercition implicite sans ce garde-fou explicite.""" raw = {"questions": [{"text": "Une question", "choices": ["A", "B"], "correct_index": True, "points": 5}]} config = doc_engine.sanitize_quiz_config(raw) assert config["questions"][0]["correct_index"] == 0 def test_sanitize_quiz_config_clamps_negative_points_to_zero() -> None: raw = {"questions": [{"text": "Une question", "choices": ["A", "B"], "correct_index": 0, "points": -5}]} assert doc_engine.sanitize_quiz_config(raw)["questions"][0]["points"] == 0 def test_sanitize_quiz_config_clamps_timer_seconds_into_bounds() -> None: assert doc_engine.sanitize_quiz_config({"timer_seconds": 1})["timer_seconds"] == doc_engine.MIN_TIMER_SECONDS assert doc_engine.sanitize_quiz_config({"timer_seconds": 9999})["timer_seconds"] == doc_engine.MAX_TIMER_SECONDS def test_sanitize_quiz_config_ignores_garbage_top_level_input() -> None: assert doc_engine.sanitize_quiz_config("n'importe quoi") == doc_engine.DEFAULT_QUIZ_CONFIG def test_quiz_total_points_sums_every_question() -> None: config = doc_engine.sanitize_quiz_config( { "questions": [ {"text": "Q1", "choices": ["A", "B"], "correct_index": 0, "points": 10}, {"text": "Q2", "choices": ["A", "B"], "correct_index": 1, "points": 5}, ] } ) assert doc_engine.quiz_total_points(config) == 15 def test_quiz_total_points_on_empty_quiz_is_zero() -> None: assert doc_engine.quiz_total_points(doc_engine.DEFAULT_QUIZ_CONFIG) == 0 def _quiz_element(attributes: dict[str, Any]) -> dict[str, Any]: return {"id": 1, "kind": "quiz", "parent_id": None, "order_index": 0, "attributes": attributes} def test_render_quiz_without_questions_shows_only_the_badge() -> None: html = doc_engine.render_document_element(_quiz_element(doc_engine.DEFAULT_QUIZ_CONFIG), {}) assert "0 question" in html assert "docQuizPlayer" not in html def test_render_quiz_with_questions_includes_the_interactive_player() -> None: config = doc_engine.sanitize_quiz_config( {"questions": [{"text": "Vrai ou faux ?", "choices": ["Vrai", "Faux"], "correct_index": 0, "points": 10}]} ) html = doc_engine.render_document_element(_quiz_element(config), {}) assert "docQuizPlayer" in html assert "docQuizOptions" in html assert "data-quiz-config=" in html assert "Vrai ou faux ?" in html def test_render_quiz_escapes_question_text_in_embedded_json() -> None: """Le JSON des questions est embarqué dans un attribut HTML (jamais un ', "choices": ["A", "B"], "correct_index": 0, "points": 1}, ] } ) html = doc_engine.render_document_element(_quiz_element(config), {}) assert "" not in html assert "<script>" in html