Spaces:
Running
Running
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(
"<script>alert&apos;xss'</script>",
" ©®"
)
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
|