"""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