"""Tests unitaires pour la fonction sanitize_plaintext dans pronote_sync.utils.text. Ce module valide le comportement de sanitize_plaintext qui prépare du texte pour les corps de message XMPP en appliquant plusieurs transformations : - Suppression des balises HTML (via beautifulsoup4) - Suppression des caractères de contrôle ASCII non imprimables - Préservation des emojis autorisés - Idempotence de la fonction """ import pytest from pronote_sync.utils.text import sanitize_plaintext class TestSanitizePlaintext: """Tests de la fonction sanitize_plaintext.""" def test_empty_string_returns_empty(self) -> None: """Test que la chaîne vide retourne une chaîne vide. :return: None """ assert sanitize_plaintext("") == "" def test_plain_text_unchanged(self) -> None: """Test qu'un texte simple sans balises ni caractères spéciaux reste inchangé. :return: None """ assert sanitize_plaintext("Hello world") == "Hello world" def test_html_tags_stripped(self) -> None: """Test que les balises HTML simples sont supprimées. :return: None """ assert sanitize_plaintext("Hello world") == "Hello world" def test_nested_html_stripped(self) -> None: """Test que les balises HTML imbriquées sont supprimées. :return: None """ assert sanitize_plaintext("

Nested

") == "Nested" def test_html_entities_decoded(self) -> None: """Test que les entités HTML sont décodées. La fonction doit décoder les entités HTML comme & en &. Si beautifulsoup4 décode les entités, le résultat attendu est "&". :return: None """ result = sanitize_plaintext("&") # beautifulsoup4 décode les entités par défaut, donc & devient & assert result == "&" @pytest.mark.parametrize( "input_text,expected", [ ("Hello\x00\x01\x02world", "Helloworld"), ("Hello\x03world", "Helloworld"), ("Hello\x04world", "Helloworld"), ("Hello\x05world", "Helloworld"), ("Hello\x06world", "Helloworld"), ("Hello\x07world", "Helloworld"), ("Hello\x08world", "Helloworld"), ("Hello\x0e\x0fworld", "Helloworld"), ("Hello\x10\x11\x12world", "Helloworld"), ("Hello\x13\x14\x15\x16\x17world", "Helloworld"), ("Hello\x18\x19\x1a\x1b\x1c\x1d\x1e\x1fworld", "Helloworld"), ], ) def test_control_chars_stripped(self, input_text: str, expected: str) -> None: """Test que les caractères de contrôle ASCII non imprimables sont supprimés. Les caractères à supprimer sont : \x00-\x08, \x0b, \x0c, \x0e-\x1f Les caractères à préserver sont : \t, \n, \r :param input_text: Texte avec caractères de contrôle :param expected: Texte attendu après nettoyage :return: None """ assert sanitize_plaintext(input_text) == expected def test_tab_preserved(self) -> None: """Test que la tabulation est préservée. :return: None """ assert sanitize_plaintext("Hello\tworld") == "Hello\tworld" def test_newline_preserved(self) -> None: """Test que le saut de ligne est préservé. :return: None """ assert sanitize_plaintext("Hello\nworld") == "Hello\nworld" def test_carriage_return_preserved(self) -> None: """Test que le retour chariot est préservé. :return: None """ assert sanitize_plaintext("Hello\rworld") == "Hello\rworld" def test_vertical_tab_stripped(self) -> None: """Test que la tabulation verticale est supprimée. :return: None """ assert sanitize_plaintext("Hello\x0bworld") == "Helloworld" def test_form_feed_stripped(self) -> None: """Test que le saut de page est supprimé. :return: None """ assert sanitize_plaintext("Hello\x0cworld") == "Helloworld" def test_emojis_preserved(self) -> None: """Test que les emojis autorisés sont préservés. :return: None """ assert sanitize_plaintext("📌📅📚💬📢") == "📌📅📚💬📢" def test_emoji_with_text(self) -> None: """Test qu'un emoji combiné avec du texte est préservé. :return: None """ assert sanitize_plaintext("📌 Devoir: Math") == "📌 Devoir: Math" @pytest.mark.parametrize( "test_input", [ "", "Hello world", "Hello", "Hello\x00world", "📌📅", "&", "
Test
", ], ) def test_idempotent(self, test_input: str) -> None: """Test que la fonction est idempotente. Pour tout texte d'entrée x, sanitize_plaintext(sanitize_plaintext(x)) doit être égal à sanitize_plaintext(x). :param test_input: Texte à tester :return: None """ first_pass = sanitize_plaintext(test_input) second_pass = sanitize_plaintext(first_pass) assert second_pass == first_pass def test_mixed_html_control_emoji(self) -> None: """Test une combinaison de balises HTML, caractères de contrôle et emojis. :return: None """ assert sanitize_plaintext("📌\x00 Hello") == "📌 Hello" def test_unicode_text_preserved(self) -> None: """Test que le texte Unicode avec accents est préservé. :return: None """ assert sanitize_plaintext("Café résumé") == "Café résumé" def test_del_char_stripped(self) -> None: """Test que le caractère ASCII DEL (\\x7f) est supprimé. :return: None """ assert sanitize_plaintext("a\x7fb") == "ab" @pytest.mark.parametrize("c1_char", ["\x80", "\x85", "\x9f"]) def test_c1_controls_stripped(self, c1_char: str) -> None: """Test que les caractères de contrôle C1 (\\x80-\\x9f) sont supprimés. :param c1_char: Caractère de contrôle C1 à tester :return: None """ assert sanitize_plaintext(f"a{c1_char}b") == "ab" def test_del_and_c1_idempotent(self) -> None: """Test que la suppression de DEL et des contrôles C1 est idempotente. :return: None """ text = "a\x7f\x80\x9fb" assert sanitize_plaintext(sanitize_plaintext(text)) == sanitize_plaintext(text)