File size: 13,868 Bytes
38691ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
"""

Tests unitaires pour utils/preprocessing.py



Ce module teste:

- preprocess_product_text(): nettoyage et préparation du texte

- validate_text_input(): validation des entrées texte

- clean_html(): suppression des balises HTML

"""
import pytest
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from utils.preprocessing import (
    preprocess_product_text,
    validate_text_input,
)


# =============================================================================
# TESTS preprocess_product_text()
# =============================================================================
@pytest.mark.unit
class TestPreprocessProductText:
    """Tests pour la fonction preprocess_product_text()."""

    def test_returns_string(self, sample_designation, sample_description):
        """Retourne une string."""
        result = preprocess_product_text(sample_designation, sample_description)
        assert isinstance(result, str)

    def test_combines_designation_and_description(self):
        """Combine désignation et description."""
        designation = "iPhone 15"
        description = "Smartphone Apple"
        result = preprocess_product_text(designation, description)

        # Le résultat doit contenir des éléments des deux
        result_lower = result.lower()
        assert "iphone" in result_lower or "15" in result_lower
        assert "smartphone" in result_lower or "apple" in result_lower

    def test_handles_empty_description(self):
        """Gère description vide."""
        result = preprocess_product_text("iPhone 15", "")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_handles_empty_designation(self):
        """Gère désignation vide."""
        result = preprocess_product_text("", "Smartphone Apple")
        assert isinstance(result, str)

    def test_handles_both_empty(self):
        """Gère les deux vides."""
        result = preprocess_product_text("", "")
        assert isinstance(result, str)

    def test_handles_none_description(self):
        """Gère description None."""
        result = preprocess_product_text("iPhone 15", None)
        assert isinstance(result, str)

    def test_handles_none_designation(self):
        """Gère désignation None."""
        result = preprocess_product_text(None, "Smartphone")
        assert isinstance(result, str)

    def test_handles_both_none(self):
        """Gère les deux None."""
        result = preprocess_product_text(None, None)
        assert isinstance(result, str)

    def test_removes_html_tags(self):
        """Supprime les balises HTML."""
        designation = "<p>iPhone <b>15</b></p>"
        result = preprocess_product_text(designation, "")

        assert "<p>" not in result
        assert "</p>" not in result
        assert "<b>" not in result
        assert "</b>" not in result

    def test_removes_script_tags(self):
        """Supprime les balises script (sécurité)."""
        designation = "<script>alert('xss')</script>iPhone"
        result = preprocess_product_text(designation, "")

        assert "<script>" not in result
        assert "alert" not in result.lower() or "iphone" in result.lower()

    def test_handles_special_characters(self):
        """Gère les caractères spéciaux."""
        designation = "iPhone™ 15® Pro©"
        result = preprocess_product_text(designation, "")
        assert isinstance(result, str)

    def test_handles_unicode(self):
        """Gère les caractères Unicode."""
        designation = "Téléphone été français"
        result = preprocess_product_text(designation, "")
        assert isinstance(result, str)

    def test_handles_emojis(self):
        """Gère les emojis."""
        designation = "📱 iPhone 15 🍎"
        result = preprocess_product_text(designation, "")
        assert isinstance(result, str)

    def test_trims_whitespace(self):
        """Supprime les espaces en début/fin."""
        designation = "   iPhone 15   "
        result = preprocess_product_text(designation, "")
        # Le résultat ne devrait pas avoir d'espaces en excès
        assert not result.startswith("   ")
        assert not result.endswith("   ")

    def test_normalizes_multiple_spaces(self):
        """Normalise les espaces multiples."""
        designation = "iPhone    15     Pro"
        result = preprocess_product_text(designation, "")
        # Ne devrait pas avoir plusieurs espaces consécutifs
        assert "    " not in result

    def test_preserves_essential_content(self):
        """Préserve le contenu essentiel."""
        designation = "Console PlayStation 5"
        description = "Jeux vidéo Sony"
        result = preprocess_product_text(designation, description)

        result_lower = result.lower()
        # Au moins une partie du contenu doit être préservée
        assert "playstation" in result_lower or "console" in result_lower or "sony" in result_lower

    @pytest.mark.parametrize("html_input,should_not_contain", [

        ("<p>Test</p>", "<p>"),

        ("<div class='x'>Test</div>", "<div"),

        ("<a href='url'>Link</a>", "<a"),

        ("<img src='img.jpg'>", "<img"),

        ("<style>css</style>", "<style"),

        ("<!--comment-->Test", "<!--"),

    ])
    def test_removes_various_html(self, html_input, should_not_contain):
        """Supprime différents types de HTML."""
        result = preprocess_product_text(html_input, "")
        assert should_not_contain not in result


# =============================================================================
# TESTS validate_text_input()
# =============================================================================
@pytest.mark.unit
class TestValidateTextInput:
    """Tests pour la fonction validate_text_input()."""

    def test_returns_tuple(self, sample_designation):
        """Retourne un tuple (is_valid, message)."""
        result = validate_text_input(sample_designation)
        assert isinstance(result, tuple)
        assert len(result) == 2

    def test_valid_text_returns_true(self, sample_designation):
        """Texte valide retourne (True, ...)."""
        is_valid, message = validate_text_input(sample_designation)
        assert is_valid is True

    def test_valid_text_message(self, sample_designation):
        """Texte valide a un message approprié."""
        is_valid, message = validate_text_input(sample_designation)
        assert isinstance(message, str)

    def test_empty_string_invalid(self):
        """String vide est invalide."""
        is_valid, message = validate_text_input("")
        assert is_valid is False
        assert len(message) > 0  # Message d'erreur présent

    def test_whitespace_only_invalid(self):
        """Espaces seulement est invalide."""
        is_valid, message = validate_text_input("   ")
        assert is_valid is False

    def test_none_invalid(self):
        """None est invalide."""
        is_valid, message = validate_text_input(None)
        assert is_valid is False

    def test_minimum_length(self):
        """Vérifie la longueur minimale."""
        # Texte trop court
        is_valid_short, _ = validate_text_input("a")
        # Texte assez long
        is_valid_long, _ = validate_text_input("iPhone 15 Pro Max")

        # Au moins le texte long devrait être valide
        assert is_valid_long is True

    def test_maximum_length(self):
        """Gère les textes très longs."""
        long_text = "a" * 100000
        is_valid, message = validate_text_input(long_text)
        # Soit valide, soit message d'erreur approprié
        assert isinstance(is_valid, bool)
        assert isinstance(message, str)

    def test_special_characters_handled(self):
        """Gère les caractères spéciaux."""
        is_valid, message = validate_text_input("Test!@#$%^&*()")
        assert isinstance(is_valid, bool)

    def test_unicode_handled(self):
        """Gère l'Unicode correctement."""
        is_valid, message = validate_text_input("Téléphone français été")
        assert isinstance(is_valid, bool)


# =============================================================================
# TESTS Security (XSS Prevention)
# =============================================================================
@pytest.mark.unit
@pytest.mark.security
class TestPreprocessingSecurity:
    """Tests de sécurité pour le preprocessing."""

    XSS_PAYLOADS = [
        "<script>alert('xss')</script>",
        "<img src=x onerror=alert('xss')>",
        "<svg onload=alert('xss')>",
        "javascript:alert('xss')",
        "<iframe src='javascript:alert(1)'>",
        "<body onload=alert('xss')>",
        "<input onfocus=alert('xss') autofocus>",
        "'-alert(1)-'",
        "\"><script>alert('xss')</script>",
    ]

    @pytest.mark.parametrize("payload", XSS_PAYLOADS)
    def test_xss_payloads_neutralized(self, payload):
        """Les payloads XSS sont neutralisés."""
        result = preprocess_product_text(payload, "")

        # Aucune balise script ne doit rester
        assert "<script>" not in result.lower()
        assert "javascript:" not in result.lower()
        assert "onerror=" not in result.lower()
        assert "onload=" not in result.lower()
        assert "onfocus=" not in result.lower()

    def test_sql_injection_patterns_handled(self):
        """Les patterns d'injection SQL sont gérés."""
        sql_payloads = [
            "'; DROP TABLE users; --",
            "1 OR 1=1",
            "admin'--",
        ]
        for payload in sql_payloads:
            result = preprocess_product_text(payload, "")
            # Le preprocessing ne devrait pas exécuter ces patterns
            assert isinstance(result, str)


# =============================================================================
# TESTS Edge Cases
# =============================================================================
@pytest.mark.unit
class TestPreprocessingEdgeCases:
    """Tests des cas limites."""

    def test_very_long_text(self):
        """Gère texte très long (100K+ caractères)."""
        long_text = "produit " * 15000  # ~120K chars
        result = preprocess_product_text(long_text, "")
        assert isinstance(result, str)

    def test_only_numbers(self):
        """Gère texte avec seulement des chiffres."""
        result = preprocess_product_text("123456789", "")
        assert isinstance(result, str)

    def test_only_punctuation(self):
        """Gère texte avec seulement de la ponctuation."""
        result = preprocess_product_text("!@#$%^&*()", "")
        assert isinstance(result, str)

    def test_mixed_languages(self):
        """Gère texte multilingue."""
        result = preprocess_product_text(
            "Hello Bonjour Hola 你好 Привет",
            "Description in multiple languages"
        )
        assert isinstance(result, str)

    def test_newlines_and_tabs(self):
        """Gère les retours à la ligne et tabulations."""
        result = preprocess_product_text(
            "Line1\nLine2\tTabbed",
            "Description\r\nwith\rreturns"
        )
        assert isinstance(result, str)
        # Ne devrait pas garder des caractères de contrôle bruts problématiques
        assert "\r" not in result or "\n" not in result or isinstance(result, str)

    def test_html_entities(self):
        """Gère les entités HTML."""
        result = preprocess_product_text(
            "&lt;script&gt;alert&amp;apos;xss&apos;&lt;/script&gt;",
            "&nbsp;&copy;&reg;"
        )
        assert isinstance(result, str)

    def test_urls_in_text(self):
        """Gère les URLs dans le texte."""
        result = preprocess_product_text(
            "Visit https://example.com for details",
            "See http://test.com"
        )
        assert isinstance(result, str)

    def test_email_addresses(self):
        """Gère les adresses email."""
        result = preprocess_product_text(
            "Contact: test@example.com",
            ""
        )
        assert isinstance(result, str)


# =============================================================================
# TESTS Consistency
# =============================================================================
@pytest.mark.unit
class TestPreprocessingConsistency:
    """Tests de cohérence."""

    def test_idempotent(self):
        """Appliquer deux fois donne le même résultat."""
        original = "iPhone 15 Pro <b>Max</b>"
        result1 = preprocess_product_text(original, "")
        result2 = preprocess_product_text(result1, "")

        assert result1 == result2

    def test_deterministic(self):
        """Même input = même output."""
        text = "Console PlayStation 5"
        results = [preprocess_product_text(text, "") for _ in range(5)]

        assert all(r == results[0] for r in results)

    def test_order_independent_for_description(self):
        """Le résultat contient les deux parties."""
        designation = "iPhone 15"
        description = "Smartphone Apple"

        result = preprocess_product_text(designation, description)

        # Les deux devraient contribuer au résultat
        result_lower = result.lower()
        has_designation = "iphone" in result_lower or "15" in result_lower
        has_description = "smartphone" in result_lower or "apple" in result_lower

        # Au moins un des deux doit être présent
        assert has_designation or has_description