add: тесты

This commit is contained in:
2026-05-14 19:45:33 +03:00
parent 370a5ed359
commit 6c59c4e0c1
5 changed files with 659 additions and 0 deletions

10
tests/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""
Пакет тестов для проекта Окнардия.
Содержит все юнит-тесты и функциональные тесты для валидации:
- Функций обработки текста (sanitize_slug, safe_html)
- Функций SEO генерации
- JSON парсинга и валидации
"""

124
tests/test_safe_html.py Normal file
View File

@@ -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 class="laquo">&laquo;</span> кавычками &raquo;',
'Текст с « кавычками »',
'Удаление span-тегов и замена кавычек'
),
(
'Текст <script>alert("вредоносный код")</script> после скрипта',
'Текст после скрипта',
'Удаление содержимого script-тега'
),
(
'Текст <style>.class { color: red; }</style> со стилем',
'Текст со стилем',
'Удаление содержимого style-тега'
),
(
'Цена: 100&nbsp;&#8470;&nbsp;(&#8470; = №)',
'Цена: 100 № ( № = №)',
'Замена числовых мнемоник (&#8470;) на Unicode'
),
(
'Символы: &mdash; &hellip; &copy; &reg;',
'Символы: — … © ®',
'Замена именованных мнемоник'
),
(
'<br>Новая<br />строка',
'Новая строка',
'Удаление br-тегов'
),
(
'<p>Текст</p><nobr>без разрывов</nobr>',
'Текст без разрывов',
'Удаление nobr-тегов'
),
(
'Множество пробелов\n\n\tи табуляций',
'Множество пробелов и табуляций',
'Очистка множественных пробелов и переносов'
),
(
'<code>function foo() { return 42; }</code> остаток',
'остаток',
'Удаление содержимого code-тега'
),
(
'<pre>preformatted\n text</pre> после',
'после',
'Удаление содержимого pre-тега'
),
(
' Текст с пробелами в начале и конце ',
'Текст с пробелами в начале и конце',
'Trim пробелов в начале/конце'
),
(
'Число &#65; (A) и &#x42; (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)

View File

@@ -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: Удаление содержимого тегов <script>"""
html = 'Текст <script>alert("hack");</script> после'
result = safe_html_spec_symbols(html)
assert 'alert' not in result, f"Script-содержимое не удалено: {result}"
assert 'Текст' in result and 'после' in result, f"Обычный текст потеряется: {result}"
print("✓ Тест 1 (удаление <script>): пройден")
def test_remove_style_tags():
"""Тест 2: Удаление содержимого тегов <style>"""
html = 'Блок <style>#id { color: red; }</style> текста'
result = safe_html_spec_symbols(html)
assert 'color' not in result, f"Style-содержимое не удалено: {result}"
assert 'Блок' in result and 'текста' in result, f"Обычный текст потеряется: {result}"
print("✓ Тест 2 (удаление <style>): пройден")
def test_remove_code_tags():
"""Тест 3: Удаление содержимого тегов <code>, <kbd>, <pre>"""
html = 'Команда <code>def foo():</code> в тексте. <kbd>Ctrl+C</kbd> текст.'
result = safe_html_spec_symbols(html)
assert 'def foo' not in result, f"Code-содержимое не удалено: {result}"
assert 'Ctrl' not in result, f"Kbd-содержимое не удалено: {result}"
assert 'Команда' in result and 'тексте' in result, f"Обычный текст потеряется: {result}"
print("✓ Тест 3 (удаление <code>, <kbd>): пройден")
def test_remove_object_tags():
"""Тест 3a: Удаление содержимого тегов <object>, <embed>"""
html = 'Текст <object data="malicious.swf"></object> и <embed src="bad.swf"/> после'
result = safe_html_spec_symbols(html)
assert 'malicious.swf' not in result, f"Object не удалён: {result}"
assert 'bad.swf' not in result, f"Embed не удалён: {result}"
assert 'Текст' in result and 'после' in result, f"Обычный текст потеряется: {result}"
print("✓ Тест 3a (удаление <object>, <embed>): пройден")
def test_remove_form_tags():
"""Тест 3b: Удаление содержимого тегов <form>, <input>, <textarea>"""
html = 'Текст <form><input type="password"/><textarea>secret</textarea></form> после'
result = safe_html_spec_symbols(html)
assert 'secret' not in result and 'password' not in result, f"Form содержимое не удалено: {result}"
assert 'password' not in result, f"Input атрибут не удалён: {result}"
assert 'Текст' in result and 'после' in result, f"Обычный текст потеряется: {result}"
print("✓ Тест 3b (удаление <form>, <input>, <textarea>): пройден")
def test_remove_svg_canvas():
"""Тест 3c: Удаление содержимого тегов <svg>, <canvas>"""
html = 'Текст <svg><script>alert("xss")</script></svg> и <canvas id="c"></canvas> после'
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 (удаление <svg>, <canvas>): пройден")
def test_remove_html_tags():
"""Тест 4: Удаление обычных HTML-тегов"""
html = '<p>Параграф <b>с полужирным</b> <i>и курсивом</i></p> <span>спан</span>'
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 = '&nbsp;&lt; &gt; &quot; &apos; &amp; &euro; &copy; &reg;'
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 = '&#8470; &#169; &#8364;' # № © €
result = safe_html_spec_symbols(html)
assert '' in result, f"Decimal entity &#8470; не заменена: {result}"
assert '©' in result, f"Decimal entity &#169; не заменена: {result}"
assert '' in result, f"Decimal entity &#8364; не заменена: {result}"
print("✓ Тест 6 (десятичные мнемоники): пройден")
def test_numeric_entities_hex():
"""Тест 7: Замена шестнадцатеричных числовых мнемоник (&#xHEX;)"""
html = '&#x20AC; &#xA9; &#x2116;' # € © №
result = safe_html_spec_symbols(html)
assert '' in result, f"Hex entity &#x20AC; не заменена: {result}"
assert '©' in result, f"Hex entity &#xA9; не заменена: {result}"
assert '' in result, f"Hex entity &#x2116; не заменена: {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 = '''
<div class="content">
<p>Текст &nbsp; с <b>мнемониками</b>: &euro; №&#8470; &#x20AC;</p>
<script>malicious_code()</script>
<style>.hide { display: none; }</style>
<span>Ещё текст &copy; 2024</span>
</div>
'''
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 = '<div><p></p></div>'
result = safe_html_spec_symbols(html)
assert result == '', f"Ожидается пустая строка, получено: {repr(result)}"
print("✓ Тест 12 (только теги): пройден")
def test_russian_text():
"""Тест 13: Русский текст с мнемониками"""
html = 'Цена: <b>1000&nbsp;₽</b> &laquo;Российский&raquo; &mdash; лучший выбор'
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)

202
tests/test_sanitize_slug.py Normal file
View File

@@ -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 = '<p>Русский <b>текст</b> в <i>тегах</i></p>'
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&nbsp;рублей &mdash; отличный &copy; 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 = '<div class="content"><p>Мой блюдо &mdash; это традиционный борщ (<b>украинский</b>)</p></div>'
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)

87
tests/test_seo_autogen.py Normal file
View File

@@ -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="Тест: <b>Привет</b> мир!!! @#$",
sPostContent="<cut text='Читать...'/><p>Это содержание поста для тестирования. Здесь может быть много текста с HTML-разметкой. Давайте посмотрим, как работает автогенерация тизера и ключевых слов.</p><p>Вторая строка текста.</p>",
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'<cut[\s\S]*?>', '', 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)