"""Modèle de données du mini-jeu Scénario
(document_engine/labels/scenario_config.py) — sans Flask, teste
directement sanitize_scenario_config, et le rendu du plateau."""
from typing import Any
import document_engine as doc_engine
def test_sanitize_scenario_config_on_missing_input_returns_full_defaults() -> None:
assert doc_engine.sanitize_scenario_config(None) == {"theme_color": "#ff5f2e", "scenarios": []}
def test_sanitize_scenario_config_keeps_a_valid_scenario_as_is() -> None:
raw = {
"theme_color": "#123456",
"scenarios": [
{
"situation": "Un collègue vous demande son mot de passe.",
"choices": [
{"text": "Le lui donner", "consequence": "Violation de sécurité."},
{"text": "Refuser et l'orienter vers l'IT", "consequence": "Bonne pratique respectée."},
],
"correct_index": 1,
}
],
}
config = doc_engine.sanitize_scenario_config(raw)
assert config["theme_color"] == "#123456"
assert config["scenarios"] == [
{
"situation": "Un collègue vous demande son mot de passe.",
"choices": [
{"text": "Le lui donner", "consequence": "Violation de sécurité."},
{"text": "Refuser et l'orienter vers l'IT", "consequence": "Bonne pratique respectée."},
],
"correct_index": 1,
}
]
def test_sanitize_scenario_config_allows_an_empty_consequence() -> None:
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "A"}, {"text": "B"}]}]}
config = doc_engine.sanitize_scenario_config(raw)
assert config["scenarios"][0]["choices"] == [{"text": "A", "consequence": ""}, {"text": "B", "consequence": ""}]
def test_sanitize_scenario_config_drops_a_scenario_with_empty_situation() -> None:
raw = {"scenarios": [{"situation": " ", "choices": [{"text": "A"}, {"text": "B"}]}]}
assert doc_engine.sanitize_scenario_config(raw)["scenarios"] == []
def test_sanitize_scenario_config_drops_a_scenario_with_fewer_than_two_choices() -> None:
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "Seul choix"}]}]}
assert doc_engine.sanitize_scenario_config(raw)["scenarios"] == []
def test_sanitize_scenario_config_drops_blank_choices_before_counting() -> None:
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "A"}, {"text": " "}, {"text": ""}]}]}
assert doc_engine.sanitize_scenario_config(raw)["scenarios"] == []
def test_sanitize_scenario_config_caps_choices_at_max() -> None:
raw = {
"scenarios": [
{
"situation": "Situation",
"choices": [{"text": f"Choix {i}"} for i in range(doc_engine.MAX_SCENARIO_CHOICES + 3)],
}
]
}
config = doc_engine.sanitize_scenario_config(raw)
assert len(config["scenarios"][0]["choices"]) == doc_engine.MAX_SCENARIO_CHOICES
def test_sanitize_scenario_config_resets_out_of_range_correct_index() -> None:
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "A"}, {"text": "B"}], "correct_index": 7}]}
config = doc_engine.sanitize_scenario_config(raw)
assert config["scenarios"][0]["correct_index"] == 0
def test_sanitize_scenario_config_rejects_a_boolean_as_correct_index() -> None:
"""True/False sont des int en Python (bool hérite de int) — même
garde-fou que sanitize_quiz_config::correct_index."""
raw = {"scenarios": [{"situation": "Situation", "choices": [{"text": "A"}, {"text": "B"}], "correct_index": True}]}
config = doc_engine.sanitize_scenario_config(raw)
assert config["scenarios"][0]["correct_index"] == 0
def test_sanitize_scenario_config_keeps_multiple_scenarios_in_order() -> None:
raw = {
"scenarios": [
{"situation": "Situation 1", "choices": [{"text": "A"}, {"text": "B"}]},
{"situation": "Situation 2", "choices": [{"text": "C"}, {"text": "D"}]},
]
}
config = doc_engine.sanitize_scenario_config(raw)
assert [s["situation"] for s in config["scenarios"]] == ["Situation 1", "Situation 2"]
def test_sanitize_scenario_config_ignores_garbage_top_level_input() -> None:
assert doc_engine.sanitize_scenario_config("n'importe quoi") == doc_engine.DEFAULT_SCENARIO_CONFIG
def _scenario_element(attributes: dict[str, Any]) -> dict[str, Any]:
return {"id": 1, "kind": "scenario", "parent_id": None, "order_index": 0, "attributes": attributes}
def test_render_scenario_without_scenarios_shows_only_the_badge() -> None:
html = doc_engine.render_document_element(_scenario_element(doc_engine.DEFAULT_SCENARIO_CONFIG), {})
assert "0 scénario" in html
assert "docScenarioPlayer" not in html
def test_render_scenario_with_scenarios_includes_the_interactive_player() -> None:
config = doc_engine.sanitize_scenario_config(
{
"scenarios": [
{
"situation": "Un collègue vous demande son mot de passe.",
"choices": [{"text": "Refuser"}, {"text": "Accepter"}],
"correct_index": 0,
}
]
}
)
html = doc_engine.render_document_element(_scenario_element(config), {})
assert "docScenarioPlayer" in html
assert "docQuizOptions" in html
assert "data-scenario-config=" in html
assert "1 scénario" in html
def test_render_scenario_escapes_situation_text_in_embedded_json() -> None:
config = doc_engine.sanitize_scenario_config(
{
"scenarios": [
{
"situation": '">',
"choices": [{"text": "A"}, {"text": "B"}],
}
]
}
)
html = doc_engine.render_document_element(_scenario_element(config), {})
assert "" not in html
assert "<script>" in html