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