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