"""Tests for HTML entity resolution in XMLTV parsing (DOCTYPE injection path)."""
import os
import tempfile
from django.test import TestCase
from lxml import etree
from apps.epg.tasks import _open_xmltv_file, _parse_programme_element
def _parse_title(entity_text: str) -> str:
"""Resolve entities the same way production programme parsing does."""
xml = (
f''
f"{entity_text}"
).encode("utf-8")
root = _parse_programme_element(xml)
return root.findtext("title")
def _read_display_name_from_file(path: str) -> str:
with _open_xmltv_file(path) as handle:
for _event, elem in etree.iterparse(handle, events=("end",), tag="display-name"):
text = (elem.text or "").strip()
elem.clear()
while elem.getprevious() is not None:
del elem.getparent()[0]
return text
return ""
class ParseProgrammeElementEntityTests(TestCase):
"""Entity resolution via _parse_programme_element + injected DOCTYPE."""
def test_french_accented(self):
self.assertEqual(_parse_title("Chaîne Télé"), "Chaîne Télé")
def test_german_umlauts(self):
self.assertEqual(
_parse_title("München Übersicht ß"),
"München Übersicht ß",
)
def test_spanish(self):
self.assertEqual(_parse_title("España ¿Qué?"), "España ¿Qué?")
def test_portuguese(self):
self.assertEqual(_parse_title("Comunicação"), "Comunicação")
def test_scandinavian(self):
self.assertEqual(_parse_title("Norsk ø å æ"), "Norsk ø å æ")
def test_greek_letters(self):
self.assertEqual(_parse_title("αβγ"), "αβγ")
def test_currency_and_symbols(self):
self.assertEqual(_parse_title("© € £ ¥"), "© € £ ¥")
def test_preserves_xml_amp(self):
self.assertEqual(_parse_title("A & B"), "A & B")
def test_preserves_xml_lt_gt(self):
self.assertEqual(_parse_title("<tag>"), "")
def test_preserves_xml_quot_apos(self):
self.assertEqual(_parse_title(""hello'"), '"hello\'')
def test_mixed_html_and_xml_entities(self):
self.assertEqual(
_parse_title("Résumé & Co <test>"),
"Résumé & Co ",
)
def test_plain_ascii_unchanged(self):
self.assertEqual(_parse_title("Plain ASCII text"), "Plain ASCII text")
def test_direct_utf8_unchanged(self):
self.assertEqual(_parse_title("日本語テレビ"), "日本語テレビ")
class OpenXmltvFileEntityTests(TestCase):
"""Entity resolution via _open_xmltv_file streaming (no disk rewrite)."""
def _make_file(self, content, *, binary=False):
fd, path = tempfile.mkstemp(suffix=".xml")
with os.fdopen(fd, "wb" if binary else "w", encoding=None if binary else "utf-8") as f:
f.write(content)
self.addCleanup(lambda: os.unlink(path) if os.path.exists(path) else None)
return path
def test_resolves_entities_in_file(self):
path = self._make_file(
'\n'
"Télé"
)
self.assertEqual(_read_display_name_from_file(path), "Télé")
with open(path, "r", encoding="utf-8") as f:
self.assertIn("é", f.read(), "Source file must not be rewritten on disk")
def test_preserves_xml_entities_in_file(self):
path = self._make_file("A & B")
self.assertEqual(_read_display_name_from_file(path), "A & B")
def test_plain_file_unchanged_on_disk(self):
original = (
'\n'
"Plain"
)
path = self._make_file(original)
_read_display_name_from_file(path)
with open(path, "r", encoding="utf-8") as f:
self.assertEqual(f.read(), original)
def test_utf8_content_preserved(self):
path = self._make_file(
"日本語テレビ"
)
self.assertEqual(_read_display_name_from_file(path), "日本語テレビ")
def test_iso_8859_1_encoding(self):
xml = (
'\n'
"Chaîne"
)
path = self._make_file(xml.encode("iso-8859-1"), binary=True)
self.assertEqual(_read_display_name_from_file(path), "Cha\u00eene")
def test_iso_8859_1_with_entities_roundtrip(self):
xml_str = (
'\n'
"D\xe9j\xe0 émission"
)
path = self._make_file(xml_str.encode("iso-8859-1"), binary=True)
name = _read_display_name_from_file(path)
self.assertIn("D\xe9j\xe0", name)
self.assertIn("\xe9mission", name)
def test_existing_doctype_not_doubled(self):
"""Files that already declare DOCTYPE are opened without injecting another."""
path = self._make_file(
'\n'
"Has Doctype"
)
with open(path, "rb") as f:
original = f.read()
with _open_xmltv_file(path) as handle:
self.assertEqual(handle.read(len(original)), original)