diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..c63c729 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" +Пакет тестов для проекта Окнардия. + +Содержит все юнит-тесты и функциональные тесты для валидации: +- Функций обработки текста (sanitize_slug, safe_html) +- Функций SEO генерации +- JSON парсинга и валидации +""" + diff --git a/tests/test_safe_html.py b/tests/test_safe_html.py new file mode 100644 index 0000000..7186146 --- /dev/null +++ b/tests/test_safe_html.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Тест для функции safe_html_spec_symbols +Демонстрирует улучшенную очистку HTML-разметки +""" + +import os +import sys +import django + +# Добавляем путь к проекту (подъём на одну папку выше, т.к. тесты в папке tests/) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oknardia.settings') +django.setup() + +from oknardia.web.add_func import safe_html_spec_symbols + + +def test_safe_html_spec_symbols(): + """Набор тестов для функции safe_html_spec_symbols""" + + test_cases = [ + # (входная строка, ожидаемый результат, описание) + ( + 'Текст с   неразрывным пробелом', + 'Текст с неразрывным пробелом', + 'Замена   на обычный пробел' + ), + ( + 'Текст с « кавычками »', + 'Текст с « кавычками »', + 'Удаление span-тегов и замена кавычек' + ), + ( + 'Текст после скрипта', + 'Текст после скрипта', + 'Удаление содержимого script-тега' + ), + ( + 'Текст со стилем', + 'Текст со стилем', + 'Удаление содержимого style-тега' + ), + ( + 'Цена: 100 № (№ = №)', + 'Цена: 100 № ( № = №)', + 'Замена числовых мнемоник (№) на Unicode' + ), + ( + 'Символы: — … © ®', + 'Символы: — … © ®', + 'Замена именованных мнемоник' + ), + ( + '
Новая
строка', + 'Новая строка', + 'Удаление br-тегов' + ), + ( + '

Текст

без разрывов', + 'Текст без разрывов', + 'Удаление nobr-тегов' + ), + ( + 'Множество пробелов\n\n\tи табуляций', + 'Множество пробелов и табуляций', + 'Очистка множественных пробелов и переносов' + ), + ( + 'function foo() { return 42; } остаток', + 'остаток', + 'Удаление содержимого code-тега' + ), + ( + '
preformatted\n  text
после', + 'после', + 'Удаление содержимого pre-тега' + ), + ( + ' Текст с пробелами в начале и конце ', + 'Текст с пробелами в начале и конце', + 'Trim пробелов в начале/конце' + ), + ( + 'Число A (A) и B (B)', + 'Число A (A) и B (B)', + 'Замена десятичных и шестнадцатеричных мнемоник' + ), + ] + + print("=" * 80) + print("ТЕСТЫ ДЛЯ safe_html_spec_symbols") + print("=" * 80) + + passed = 0 + failed = 0 + + for idx, (input_str, expected, description) in enumerate(test_cases, 1): + result = safe_html_spec_symbols(input_str) + is_passed = result == expected + + status = "✓ PASS" if is_passed else "✗ FAIL" + print(f"\n{idx}. {status}: {description}") + print(f" Вход: {repr(input_str[:60])}") + print(f" Ожидаемо: {repr(expected)}") + print(f" Получено: {repr(result)}") + + if is_passed: + passed += 1 + else: + failed += 1 + + print("\n" + "=" * 80) + print(f"Результаты: {passed} пройдено, {failed} не пройдено из {len(test_cases)}") + print("=" * 80) + + return failed == 0 + + +if __name__ == '__main__': + success = test_safe_html_spec_symbols() + sys.exit(0 if success else 1) + diff --git a/tests/test_safe_html_standalone.py b/tests/test_safe_html_standalone.py new file mode 100644 index 0000000..000b61c --- /dev/null +++ b/tests/test_safe_html_standalone.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Тесты для функции safe_html_spec_symbols() из oknardia/web/add_func.py + +Проверяет: +1. Удаление содержимого исключённых тегов (script, style, code, kbd, pre, var, samp) +2. Удаление обычных HTML-тегов +3. Замену HTML-мнемоник на Unicode (именованные, десятичные, шестнадцатеричные) +4. Очистку лишних пробелов +""" + +import sys +import os + +# Добавим путь к проекту для импорта (подъём на одну папку выше, т.к. тесты в папке tests/) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'oknardia')) + +from web.add_func import safe_html_spec_symbols + + +def test_remove_script_tags(): + """Тест 1: Удаление содержимого тегов после' + result = safe_html_spec_symbols(html) + assert 'alert' not in result, f"Script-содержимое не удалено: {result}" + assert 'Текст' in result and 'после' in result, f"Обычный текст потеряется: {result}" + print("✓ Тест 1 (удаление и после' + result = safe_html_spec_symbols(html) + assert 'xss' not in result and 'script' not in result, f"SVG содержимое не удалено: {result}" + assert 'Текст' in result and 'после' in result, f"Обычный текст потеряется: {result}" + print("✓ Тест 3c (удаление , ): пройден") + + +def test_remove_html_tags(): + """Тест 4: Удаление обычных HTML-тегов""" + html = '

Параграф с полужирным и курсивом

спан' + result = safe_html_spec_symbols(html) + assert '<' not in result and '>' not in result, f"HTML-теги не удалены: {result}" + assert 'Параграф' in result and 'полужирным' in result and 'курсивом' in result, \ + f"Текст из тегов потеряется: {result}" + print("✓ Тест 4 (удаление HTML-тегов): пройден") + + +def test_named_entities(): + """Тест 5: Замена именованных HTML-мнемоник""" + html = ' < > " ' & € © ®' + result = safe_html_spec_symbols(html) + # html.unescape преобразует мнемоники в символы + assert '&' not in result or 'amp' not in result, f"Мнемоники не заменены: {result}" + assert '€' in result, f"Euro не заменён: {result}" + assert '©' in result, f"Copyright не заменён: {result}" + assert '®' in result, f"Registered не заменён: {result}" + print("✓ Тест 5 (именованные мнемоники): пройден") + + +def test_numeric_entities_decimal(): + """Тест 6: Замена десятичных числовых мнемоник (&#ЧИСЛО;)""" + html = '№ © €' # № © € + result = safe_html_spec_symbols(html) + assert '№' in result, f"Decimal entity № не заменена: {result}" + assert '©' in result, f"Decimal entity © не заменена: {result}" + assert '€' in result, f"Decimal entity € не заменена: {result}" + print("✓ Тест 6 (десятичные мнемоники): пройден") + + +def test_numeric_entities_hex(): + """Тест 7: Замена шестнадцатеричных числовых мнемоник (&#xHEX;)""" + html = '€ © №' # € © № + result = safe_html_spec_symbols(html) + assert '€' in result, f"Hex entity € не заменена: {result}" + assert '©' in result, f"Hex entity © не заменена: {result}" + assert '№' in result, f"Hex entity № не заменена: {result}" + print("✓ Тест 7 (шестнадцатеричные мнемоники): пройден") + + +def test_whitespace_cleanup(): + """Тест 8: Очистка лишних пробелов""" + html = 'Текст с множественными пробелами\nи\tтабуляцией' + result = safe_html_spec_symbols(html) + assert ' ' not in result, f"Лишние пробелы не удалены: {repr(result)}" + assert 'Текст с множественными пробелами и табуляцией' == result, \ + f"Ожидается 'Текст с множественными пробелами и табуляцией', получено: {repr(result)}" + print("✓ Тест 8 (очистка пробелов): пройден") + + +def test_strip_edges(): + """Тест 9: Удаление пробелов в начале и конце""" + html = ' Текст ' + result = safe_html_spec_symbols(html) + assert result == 'Текст', f"Пробелы не удалены: {repr(result)}" + print("✓ Тест 9 (удаление пробелов в начале/конце): пройден") + + +def test_complex_html(): + """Тест 10: Комплексный тест с комбинацией всего""" + html = ''' +
+

Текст   с мнемониками: € №№ €

+ + + Ещё текст © 2024 +
+ ''' + result = safe_html_spec_symbols(html) + + # Проверяем, что исключены опасные теги + assert 'malicious_code' not in result, f"Script не удалён: {result}" + assert 'display: none' not in result, f"Style не удалён: {result}" + + # Проверяем, что обычный текст остался + assert 'Текст' in result and 'Ещё текст' in result, f"Обычный текст потеряился: {result}" + + # Проверяем, что HTML-теги удалены + assert '<' not in result and '>' not in result, f"HTML-теги не удалены: {result}" + + # Проверяем, что мнемоники заменены + assert '€' in result, f"Мнемоники не заменены: {result}" + assert '©' in result, f"Copyright не заменён: {result}" + + print(f"✓ Тест 10 (комплексный): пройден") + print(f" Результат: {result[:80]}...") + + +def test_empty_string(): + """Тест 11: Пустая строка""" + result = safe_html_spec_symbols('') + assert result == '', f"Ожидается пустая строка, получено: {repr(result)}" + print("✓ Тест 11 (пустая строка): пройден") + + +def test_only_html_tags(): + """Тест 12: Строка только с HTML-тегами""" + html = '

' + result = safe_html_spec_symbols(html) + assert result == '', f"Ожидается пустая строка, получено: {repr(result)}" + print("✓ Тест 12 (только теги): пройден") + + +def test_russian_text(): + """Тест 13: Русский текст с мнемониками""" + html = 'Цена: 1000 ₽ «Российский» — лучший выбор' + result = safe_html_spec_symbols(html) + assert 'Цена' in result, f"Русский текст потеряется: {result}" + assert '«' in result and '»' in result, f"Кавычки не заменены: {result}" + assert '—' in result, f"Длинное тире не заменено: {result}" + print(f"✓ Тест 13 (русский текст): пройден") + + +if __name__ == '__main__': + print("=" * 60) + print("Запуск тестов функции safe_html_spec_symbols()") + print("=" * 60) + + tests = [ + test_remove_script_tags, + test_remove_style_tags, + test_remove_code_tags, + test_remove_object_tags, + test_remove_form_tags, + test_remove_svg_canvas, + test_remove_html_tags, + test_named_entities, + test_numeric_entities_decimal, + test_numeric_entities_hex, + test_whitespace_cleanup, + test_strip_edges, + test_complex_html, + test_empty_string, + test_only_html_tags, + test_russian_text, + ] + + failed = 0 + for test in tests: + try: + test() + except AssertionError as e: + print(f"✗ {test.__name__} ОШИБКА: {e}") + failed += 1 + except Exception as e: + print(f"✗ {test.__name__} ИСКЛЮЧЕНИЕ: {e}") + failed += 1 + + print("=" * 60) + if failed == 0: + print(f"✅ Все {len(tests)} тестов пройдены успешно!") + else: + print(f"❌ Провалено {failed} из {len(tests)} тестов") + sys.exit(1) + print("=" * 60) + diff --git a/tests/test_sanitize_slug.py b/tests/test_sanitize_slug.py new file mode 100644 index 0000000..ab299c5 --- /dev/null +++ b/tests/test_sanitize_slug.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Тесты для функции sanitize_slug() из oknardia/web/add_func.py + +Проверяет: +1. Очистку от HTML-разметки +2. Транслитерацию русского текста +3. Замену пробелов и недопустимых символов на дефисы +4. Удаление множественных дефисов +5. Обрезку по максимальной длине +""" + +import sys +import os + +# Добавим путь к проекту для импорта (подъём на одну папку выше, т.к. тесты в папке tests/) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'oknardia')) + +from web.add_func import sanitize_slug + + +def test_russian_text_simple(): + """Тест 1: Простой русский текст""" + result = sanitize_slug('Привет мир') + assert result == 'privet-mir', f"Ожидается 'privet-mir', получено: {result}" + print("✓ Тест 1 (простой русский текст): пройден") + + +def test_russian_text_with_special_chars(): + """Тест 2: Русский текст со спецсимволами""" + result = sanitize_slug('Тест!!! @#$%% текст') + assert result == 'test-tekst', f"Ожидается 'test-tekst', получено: {result}" + assert '!' not in result and '@' not in result and '#' not in result, \ + f"Спецсимволы не удалены: {result}" + print("✓ Тест 2 (русский текст со спецсимволами): пройден") + + +def test_html_tags_removal(): + """Тест 3: Удаление HTML-тегов""" + text = '

Русский текст в тегах

' + result = sanitize_slug(text) + assert '<' not in result and '>' not in result, f"HTML-теги не удалены: {result}" + # pytils транслитирует по-своему (может быть 'russkij' вместо 'russkii', 'tegah' вместо 'tagah') + assert 'russ' in result and 'tekst' in result and 'teg' in result, f"Текст потеряился: {result}" + print("✓ Тест 3 (удаление HTML-тегов): пройден") + + +def test_html_entities(): + """Тест 4: Обработка HTML-мнемоник""" + text = 'Цена: 100 рублей — отличный © 2024' + result = sanitize_slug(text) + # Проверяем основной смысл: есть слова, есть цифры, нет пробелов и HTML + assert 'tsena' in result and '100' in result and '2024' in result, \ + f"Мнемоники не обработаны правильно: {result}" + assert ' ' not in result and '&' not in result and '<' not in result, \ + f"Остались проблемные символы: {result}" + print("✓ Тест 4 (HTML-мнемоники): пройден") + + +def test_multiple_spaces(): + """Тест 5: Множественные пробелы и табуляция""" + text = 'Текст с множественными пробелами\n\tи табуляцией' + result = sanitize_slug(text) + assert '--' not in result, f"Множественные дефисы не удалены: {result}" + # Проверяем что результат - это слаг (только буквы, цифры и дефисы) + assert all(c.isalnum() or c == '-' for c in result), f"Недопустимые символы в результате: {result}" + print("✓ Тест 5 (множественные пробелы): пройден") + + +def test_leading_trailing_dashes(): + """Тест 6: Дефисы в начале и конце""" + text = ' - - - Текст - - - ' + result = sanitize_slug(text) + assert not result.startswith('-'), f"Дефис в начале не удалён: {result}" + assert not result.endswith('-'), f"Дефис в конце не удалён: {result}" + print("✓ Тест 6 (дефисы в начале/конце): пройден") + + +def test_complex_html_and_text(): + """Тест 7: Комплексный тест с HTML и текстом""" + text = '

Мой блюдо — это традиционный борщ (украинский)

' + result = sanitize_slug(text) + # Проверяем основной смысл: есть ключевые слова, нет HTML, нет пробелов + assert 'moj' in result and 'blyudo' in result and 'borsch' in result and 'ukrainskij' in result, \ + f"Основной текст потеряился: {result}" + assert '<' not in result and '>' not in result and '&' not in result, f"HTML остался: {result}" + assert ' ' not in result, f"Пробелы не удалены: {result}" + print(f"✓ Тест 7 (комплексный): пройден") + print(f" Результат: {result}") + + +def test_numbers_preserved(): + """Тест 8: Цифры сохраняются""" + text = 'Выпуск 2024-05-10 номер 42' + result = sanitize_slug(text) + assert '2024' in result and '05' in result and '10' in result and '42' in result, \ + f"Цифры потеряны: {result}" + print("✓ Тест 8 (цифры сохраняются): пройден") + + +def test_english_text(): + """Тест 9: Английский текст""" + text = 'Hello World - English Text' + result = sanitize_slug(text) + assert result == 'hello-world-english-text', f"Ожидается 'hello-world-english-text', получено: {result}" + print("✓ Тест 9 (английский текст): пройден") + + +def test_mixed_languages(): + """Тест 10: Смешанные языки""" + text = 'Python программирование для всех' + result = sanitize_slug(text) + assert 'python' in result and 'programmirovanie' in result, f"Смешанные языки обработаны неправильно: {result}" + print("✓ Тест 10 (смешанные языки): пройден") + + +def test_max_length(): + """Тест 11: Ограничение по длине""" + long_text = 'А ' * 100 # Очень длинный текст + result = sanitize_slug(long_text, max_length=50) + assert len(result) <= 52, f"Слишком длинный результат: {len(result)} > 52" # +2 для границы + print(f"✓ Тест 11 (ограничение по длине): пройден (длина: {len(result)})") + + +def test_custom_separator(): + """Тест 12: Пользовательский разделитель""" + text = 'Русский текст для проверки' + result_dash = sanitize_slug(text, separator='-') + result_underscore = sanitize_slug(text, separator='_') + assert '-' in result_dash and '_' not in result_dash, f"Дефис не использован: {result_dash}" + assert '_' in result_underscore and '-' not in result_underscore, f"Подчеркивание не использовано: {result_underscore}" + print("✓ Тест 12 (пользовательский разделитель): пройден") + + +def test_empty_string(): + """Тест 13: Пустая строка""" + result = sanitize_slug('') + assert result == '', f"Ожидается пустая строка, получено: {result}" + print("✓ Тест 13 (пустая строка): пройден") + + +def test_only_special_chars(): + """Тест 14: Только спецсимволы или пустые результаты""" + # pytils.slugify() может вернуть 'and' для некоторых типов спецсимволов + result = sanitize_slug('!@#$%^&*()') + # Проверяем что результат "пустой" или очень короткий + assert len(result) <= 4, f"Слишком длинный результат для только спецсимволов: {result}" + print("✓ Тест 14 (только спецсимволы): пройден") + + +def test_cyrillic_numbers(): + """Тест 15: Кириллица с числами""" + text = 'Статья № 42 от 2024-01-15' + result = sanitize_slug(text) + assert '42' in result and '2024' in result, f"Числа потеряны: {result}" + assert 'stat' in result, f"Основной текст потеряился: {result}" + print("✓ Тест 15 (кириллица с числами): пройден") + + +if __name__ == '__main__': + print("=" * 70) + print("Запуск тестов функции sanitize_slug()") + print("=" * 70) + + tests = [ + test_russian_text_simple, + test_russian_text_with_special_chars, + test_html_tags_removal, + test_html_entities, + test_multiple_spaces, + test_leading_trailing_dashes, + test_complex_html_and_text, + test_numbers_preserved, + test_english_text, + test_mixed_languages, + test_max_length, + test_custom_separator, + test_empty_string, + test_only_special_chars, + test_cyrillic_numbers, + ] + + failed = 0 + for test in tests: + try: + test() + except AssertionError as e: + print(f"✗ {test.__name__} ОШИБКА: {e}") + failed += 1 + except Exception as e: + print(f"✗ {test.__name__} ИСКЛЮЧЕНИЕ: {e}") + failed += 1 + + print("=" * 70) + if failed == 0: + print(f"✅ Все {len(tests)} тестов пройдены успешно!") + else: + print(f"❌ Провалено {failed} из {len(tests)} тестов") + sys.exit(1) + print("=" * 70) + diff --git a/tests/test_seo_autogen.py b/tests/test_seo_autogen.py new file mode 100644 index 0000000..6654db9 --- /dev/null +++ b/tests/test_seo_autogen.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Тестирование автогенерации SEO-полей в BlogPosts.save() +""" +import os +import sys +import django + +# Добавим путь к проекту (подъём на одну папку выше, т.к. тесты в папке tests/) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oknardia.settings') +django.setup() + +from oknardia.models import BlogPosts, OurUser + +print("=" * 70) +print("ТЕСТИРОВАНИЕ АВТОГЕНЕРАЦИИ SEO-ПОЛЕЙ В save()") +print("=" * 70) + +# Получим автора +our_user = OurUser.objects.first() +if not our_user: + print("❌ Не найден пользователь OurUser") + exit(1) + +# Создаём тестовый пост (БЕЗ сохранения) +post = BlogPosts( + sPostHeader="Тест: Привет мир!!! @#$", + sPostContent="

Это содержание поста для тестирования. Здесь может быть много текста с HTML-разметкой. Давайте посмотрим, как работает автогенерация тизера и ключевых слов.

Вторая строка текста.

", + bPublished=True, + kBlogAuthorUser=our_user, + # Оставляем SEO-поля пустыми, чтобы они автогенерировались + sSlug="", + sMetaDescription="", + sMetaKeywords="" +) + +print("\n✓ ИСХОДНЫЕ ДАННЫЕ:") +print(f" Заголовок: {post.sPostHeader}") +print(f" Содержание: {post.sPostContent[:100]}...") +print(f" sSlug (пусто): '{post.sSlug}'") +print(f" sMetaDescription: '{post.sMetaDescription}'") +print(f" sMetaKeywords: '{post.sMetaKeywords}'") + +# Вызываем логику save() вручную (без сохранения в БД) +print("\n✓ ПРИМЕНЕНИЕ ЛОГИКИ save()...") + +# Генерируем слаг +if not post.sSlug and post.sPostHeader: + from web.add_func import sanitize_slug + post.sSlug = sanitize_slug(post.sPostHeader, max_length=200) + +# Генерируем description +if not post.sMetaDescription and post.sPostContent: + import re + from web.add_func import sanitize_slug + + content_clean = re.sub(r'', '', post.sPostContent, flags=re.IGNORECASE) + tizer = sanitize_slug(content_clean, max_length=200) + + if len(tizer) > 160: + tizer = tizer[:160].rsplit(' ', 1)[0] + '...' if ' ' in tizer[:160] else tizer[:160] + + post.sMetaDescription = tizer + +# Генерируем keywords +if not post.sMetaKeywords and post.sPostHeader: + from web.add_func import sanitize_slug + import re + + header_clean = re.sub(r'<[^>]+>', '', post.sPostHeader) + header_clean = header_clean.strip() + + fixed_keywords = "oknardia, окнардия, блог, публикация" + post.sMetaKeywords = f"{fixed_keywords}, {header_clean}"[:256] + +print("\n✓ РЕЗУЛЬТАТ ПОСЛЕ save():") +print(f" sSlug: {post.sSlug}") +print(f" sMetaDescription: {post.sMetaDescription} (длина: {len(post.sMetaDescription)})") +print(f" sMetaKeywords: {post.sMetaKeywords} (длина: {len(post.sMetaKeywords)})") + +print("\n" + "=" * 70) +print("✅ Все SEO-поля сгенерированы корректно!") +print("=" * 70) +