2026-05-08 11:44:29 +02:00
|
|
|
"""exercise_rich_text: §11 Normalisierung und ID-Sammlung."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from fastapi import HTTPException
|
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
|
|
|
|
from exercise_rich_text import (
|
|
|
|
|
assert_no_inline_media_references_on_create,
|
|
|
|
|
collect_inline_exercise_media_ids,
|
|
|
|
|
normalize_inline_exercise_media_markup,
|
2026-10-13 16:47:57 +02:00
|
|
|
sanitize_exercise_rich_html,
|
2026-05-08 11:44:29 +02:00
|
|
|
validate_inline_exercise_media_ids_for_exercise,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-10-13 16:47:57 +02:00
|
|
|
def test_normalize_strips_script_and_handlers() -> None:
|
|
|
|
|
raw = '<p onclick="alert(1)">Hallo <script>alert(1)</script></p><a href="javascript:alert(1)">Link</a>'
|
|
|
|
|
out = normalize_inline_exercise_media_markup(raw)
|
|
|
|
|
assert out is not None
|
|
|
|
|
lowered = out.lower()
|
|
|
|
|
assert "script" not in lowered
|
|
|
|
|
assert "onclick" not in lowered
|
|
|
|
|
assert "javascript:" not in lowered
|
|
|
|
|
assert "Hallo" in out
|
|
|
|
|
assert "Link" in out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_normalize_keeps_inline_media_span() -> None:
|
|
|
|
|
raw = '<span class="shinkan-inline-media evil" data-shinkan-exercise-media="7" data-shinkan-exercise-media-size="full">x</span>'
|
|
|
|
|
out = sanitize_exercise_rich_html(raw)
|
|
|
|
|
assert out is not None
|
|
|
|
|
assert 'data-shinkan-exercise-media="7"' in out
|
|
|
|
|
assert 'data-shinkan-exercise-media-size="full"' in out
|
|
|
|
|
assert "shinkan-inline-media" in out
|
|
|
|
|
assert "evil" not in out
|
2026-05-08 11:44:29 +02:00
|
|
|
s = '<p>Vor {{exerciseMedia: 42 }} nach</p>'
|
|
|
|
|
out = normalize_inline_exercise_media_markup(s)
|
|
|
|
|
assert 'data-shinkan-exercise-media="42"' in out
|
2026-05-08 12:00:02 +02:00
|
|
|
assert 'data-shinkan-exercise-media-size="medium"' in out
|
2026-05-08 11:44:29 +02:00
|
|
|
assert "{{" not in out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_collect_merges_braces_and_data_attr() -> None:
|
|
|
|
|
html = '{{ExercisemEDIA: 1}}\n<span data-shinkan-exercise-media="2"></span>'
|
|
|
|
|
assert collect_inline_exercise_media_ids(html) == {1, 2}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_assert_no_inline_on_create_raises() -> None:
|
|
|
|
|
with pytest.raises(HTTPException) as ei:
|
|
|
|
|
assert_no_inline_media_references_on_create({5})
|
|
|
|
|
assert ei.value.status_code == 400
|
|
|
|
|
body = ei.value.detail
|
|
|
|
|
assert isinstance(body, dict)
|
|
|
|
|
assert body["code"] == "INLINE_EXERCISE_MEDIA_ON_CREATE"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_ids_sql_mock() -> None:
|
|
|
|
|
mock_cur = MagicMock()
|
|
|
|
|
mock_cur.fetchall.return_value = [{"id": 1}]
|
|
|
|
|
with pytest.raises(HTTPException) as ei:
|
|
|
|
|
validate_inline_exercise_media_ids_for_exercise(mock_cur, 100, {1, 99})
|
|
|
|
|
assert ei.value.detail["invalid_exercise_media_ids"] == [99]
|