mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 17:16:26 +00:00
149 lines
5.7 KiB
Python
149 lines
5.7 KiB
Python
"""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'<programme start="20000101000000 +0000" '
|
|
f'stop="20000101000000 +0000" channel="test.ch">'
|
|
f"<title>{entity_text}</title></programme>"
|
|
).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>"), "<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 <test>",
|
|
)
|
|
|
|
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(
|
|
'<?xml version="1.0"?>\n'
|
|
"<tv><channel><display-name>Télé</display-name></channel></tv>"
|
|
)
|
|
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("<tv><channel><display-name>A & B</display-name></channel></tv>")
|
|
self.assertEqual(_read_display_name_from_file(path), "A & B")
|
|
|
|
def test_plain_file_unchanged_on_disk(self):
|
|
original = (
|
|
'<?xml version="1.0"?>\n'
|
|
"<tv><channel><display-name>Plain</display-name></channel></tv>"
|
|
)
|
|
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(
|
|
"<tv><channel><display-name>日本語テレビ</display-name></channel></tv>"
|
|
)
|
|
self.assertEqual(_read_display_name_from_file(path), "日本語テレビ")
|
|
|
|
def test_iso_8859_1_encoding(self):
|
|
xml = (
|
|
'<?xml version="1.0" encoding="ISO-8859-1"?>\n'
|
|
"<tv><channel><display-name>Chaîne</display-name></channel></tv>"
|
|
)
|
|
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 = (
|
|
'<?xml version="1.0" encoding="ISO-8859-1"?>\n'
|
|
"<tv><channel><display-name>D\xe9j\xe0 émission</display-name></channel></tv>"
|
|
)
|
|
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(
|
|
'<!DOCTYPE tv SYSTEM "xmltv.dtd">\n'
|
|
"<tv><channel><display-name>Has Doctype</display-name></channel></tv>"
|
|
)
|
|
with open(path, "rb") as f:
|
|
original = f.read()
|
|
with _open_xmltv_file(path) as handle:
|
|
self.assertEqual(handle.read(len(original)), original)
|