Files
2021-cadpoint-ru/cadpoint/web/tests.py

511 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.forms import Textarea
from django.test import RequestFactory, SimpleTestCase, TestCase
from django.urls import reverse
from etpgrf.config import MODE_UNICODE, SANITIZE_ETPGRF
from taggit.models import Tag
from web.admin import AdminContentForm
from web.add_function import clean_text_to_slug, safe_html_special_symbols
from web.legacy_links import build_canonical_url, replace_legacy_links
from web.models import TbContent
# Этот файл смешивает два типа проверок:
# - простые тесты чистой логики без базы данных (`SimpleTestCase`),
# - интеграционные Django-тесты с базой и HTTP-клиентом (`TestCase`).
class LegacyLinksTests(SimpleTestCase):
# `SimpleTestCase` подходит для функций, которые не ходят в базу и не
# требуют полноценного HTTP-запроса: здесь мы просто проверяем строки.
def test_build_canonical_url_without_slug(self):
self.assertEqual(build_canonical_url(123, ''), '/item/123-')
def test_replace_legacy_links_rewrites_internal_urls(self):
text = (
'<a href="/news/1-latest-news/123-old-title.html">link</a>'
'<a href="http://www.cadpoint.ru/component/content/article/456-some-article.html">x</a>'
'<a href="https://example.com/news/1-latest-news/123-old-title.html">external</a>'
)
new_text, matches = replace_legacy_links(text, {123: 'new-title', 456: 'article-title'})
self.assertIn('/item/123-new-title', new_text)
self.assertIn('/item/456-article-title', new_text)
self.assertIn('https://example.com/news/1-latest-news/123-old-title.html', new_text)
self.assertEqual(len(matches), 2)
def test_replace_legacy_links_does_not_touch_image_urls(self):
text = (
'<a href="/news/1-latest-news/123-old-title.html">link</a>'
'<img src="/images/stories/news/photo123.jpg" alt="photo">'
)
new_text, matches = replace_legacy_links(text, {123: 'new-title'})
self.assertIn('/item/123-new-title', new_text)
self.assertIn('/images/stories/news/photo123.jpg', new_text)
self.assertEqual(len(matches), 1)
# Эти тесты тоже без базы: проверяем очистку HTML и подготовку slug'ов.
class SafeHtmlSpecialSymbolsTests(SimpleTestCase):
def test_strips_html_tags_and_decodes_entities(self):
text = '<p>&laquo;Привет&nbsp;<b>мир</b>&raquo; &shy;<script>alert(1)</script><style>p{}</style></p>'
self.assertEqual(safe_html_special_symbols(text), '«Привет мир»')
def test_clean_text_to_slug_normalizes_non_latin_symbols(self):
self.assertEqual(clean_text_to_slug('αβγ ΔΩ'), 'content')
self.assertEqual(clean_text_to_slug('₽ € $ ₴ ₿'), 'content')
# Здесь проверяем форму админки: её поля, виджеты и виртуальные настройки.
class AdminTypographFormTests(SimpleTestCase):
def test_admin_form_exposes_virtual_typograph_fields(self):
form = AdminContentForm()
self.assertIn('typograph_enabled', form.fields)
self.assertIn('typograph_strip_soft_hyphens', form.fields)
self.assertIn('typograph_mode', form.fields)
self.assertIn('typograph_hyphenation', form.fields)
self.assertIn('typograph_sanitizer', form.fields)
self.assertEqual(form.fields['typograph_mode'].initial, 'mixed')
self.assertTrue(form.fields['typograph_strip_soft_hyphens'].initial)
self.assertTrue(form.fields['typograph_hyphenation'].initial)
self.assertEqual(form.fields['typograph_sanitizer'].initial, 'None')
def test_admin_form_adds_codemirror_attrs_and_media(self):
form = AdminContentForm()
for field_name in ('szContentHead', 'szContentIntro', 'szContentBody'):
self.assertIsInstance(form.fields[field_name].widget, Textarea)
self.assertEqual(
form.fields[field_name].widget.attrs.get('data-codemirror-editor'),
'1',
)
self.assertEqual(
form.fields[field_name].widget.attrs.get('data-language'),
'html',
)
for field_name in ('szContentKeywords', 'szContentDescription'):
self.assertIsInstance(form.fields[field_name].widget, Textarea)
self.assertNotEqual(
form.fields[field_name].widget.attrs.get('data-codemirror-editor'),
'1',
)
self.assertIn('codemirror/editor.js', str(form.media))
def test_tbcontent_model_has_no_btypograf_field(self):
self.assertNotIn('bTypograf', [field.name for field in TbContent._meta.fields])
def test_tbcontent_str_uses_clean_text(self):
item = TbContent(id=7, szContentHead='<b>&laquo;Привет&nbsp;мир&raquo;</b>')
self.assertEqual(str(item), '007: «Привет мир»')
# В этих тестах уже нужна база и `client`, потому что мы проверяем Django-views
# и JSON-ответы как реальные запросы из браузера.
class TagAutocompleteTests(TestCase):
# `TestCase` поднимает тестовую БД, а `client` умеет делать полноценные запросы
# к Django, как будто их отправил браузер.
def setUp(self):
user_model = get_user_model()
self.user = user_model.objects.create_superuser(
username='admin',
email='admin@example.com',
password='password',
)
Tag.objects.create(name='alpha')
Tag.objects.create(name='beta')
Tag.objects.create(name='gamma')
self.client.force_login(self.user)
def test_returns_tag_results_for_term(self):
response = self.client.get(
reverse('web_tag_autocomplete'),
{'term': 'al'},
)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload['pagination']['more'], False)
self.assertEqual([item['text'] for item in payload['results']], ['alpha'])
def test_returns_initial_tag_batch_without_term(self):
response = self.client.get(reverse('web_tag_autocomplete'))
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(len(payload['results']), 3)
self.assertEqual(payload['pagination']['more'], False)
def test_paginates_tag_results(self):
Tag.objects.all().delete()
for index in range(30):
Tag.objects.create(name=f'tag-{index:02d}')
response = self.client.get(reverse('web_tag_autocomplete'), {'page': 1})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(len(payload['results']), 25)
self.assertEqual(payload['pagination']['more'], True)
response = self.client.get(reverse('web_tag_autocomplete'), {'page': 2})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(len(payload['results']), 5)
self.assertEqual(payload['pagination']['more'], False)
# Страница со всеми тегами — тоже обычный Django-response, поэтому тут `TestCase`.
class AllTagsPageTests(TestCase):
def setUp(self):
user_model = get_user_model()
self.user = user_model.objects.create_superuser(
username='admin',
email='admin@example.com',
password='password',
)
self.client.force_login(self.user)
item1 = TbContent.objects.create(
szContentHead='Тест 1',
szContentIntro='Анонс 1',
szContentBody='Тело 1',
szContentSlug='test-1',
bContentPublish=True,
)
item2 = TbContent.objects.create(
szContentHead='Тест 2',
szContentIntro='Анонс 2',
szContentBody='Тело 2',
szContentSlug='test-2',
bContentPublish=True,
)
item1.tags.add('alpha', 'beta')
item2.tags.add('alpha')
def test_alltags_page_lists_all_tags_with_counts(self):
response = self.client.get(reverse('web_alltags'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<main id="main-content">')
self.assertContains(response, 'Все теги сайта')
self.assertContains(response, '/tag_alpha')
self.assertContains(response, '/tag_beta')
self.assertContains(response, '<b class="_tag">2</b>')
self.assertContains(response, '<b class="_tag">1</b>')
def test_footer_counters_are_loaded_from_static_js(self):
response = self.client.get(reverse('web_alltags'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'js/footer-counters.js')
self.assertNotContains(response, 'googletagmanager.com/gtag/js?id=UA-9116991-1')
self.assertNotContains(response, 'mc.yandex.ru/metrika/tag.js')
self.assertNotContains(response, 'top-fwz1.mail.ru/js/code.js')
def test_accept_cookies_banner_loads_static_js(self):
response = self.client.get(reverse('web_alltags'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'js/accept-cookies.js')
self.assertContains(response, 'id="cookies_accept_button"')
self.assertNotContains(response, 'CookieAcceptDate = new Date()')
self.assertNotContains(response, 'onclick="CookieAcceptDate')
# Здесь проверяем поведение страницы тега, включая пустое состояние.
class TagEmptyStateTests(TestCase):
def setUp(self):
self.item = TbContent.objects.create(
szContentHead='Тест 1',
szContentIntro='Анонс 1',
szContentBody='Тело 1',
szContentSlug='test-1',
bContentPublish=True,
)
self.item.tags.add('alpha')
Tag.objects.create(name='lonely')
def test_tag_page_with_news_still_renders_entries(self):
response = self.client.get('/tag_alpha')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Тест 1')
self.assertNotContains(response, 'Новостей не найдено')
def test_tag_page_without_news_shows_empty_state(self):
response = self.client.get('/tag_lonely')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Новостей не найдено')
self.assertContains(response, 'По этому тегу пока нет опубликованных новостей.')
self.assertNotContains(response, 'Тест 1')
def test_tag_page_for_missing_slug_shows_missing_tag_message(self):
response = self.client.get('/tag_rebranded-tag')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Тег не найден')
self.assertContains(response, 'не найден или был переименован')
# Тут мы проверяем модель и `save()`: slug, типограф, счётчик просмотров и SEO-поля.
class TypographTests(TestCase):
def test_save_generates_slug_from_clean_text(self):
item = TbContent(szContentHead='<b>Привет&nbsp;мир</b>')
item.save()
self.assertEqual(item.szContentSlug, 'privet-mir')
def test_save_normalizes_non_latin_slug_to_default(self):
item = TbContent(szContentHead='αβγ ΔΩ')
item.save()
self.assertEqual(item.szContentSlug, 'content')
def test_save_uses_etpgrf_and_clears_flag(self):
item = TbContent(
szContentHead='«Привет»',
szContentIntro='<p>Абзац</p>',
szContentBody='<p>Тело</p>',
)
item._typograph_enabled = True
with patch('web.models._build_typographer') as build_mock:
build_mock.return_value.process.side_effect = lambda text: f'[{text}]'
item.save()
self.assertEqual(build_mock.call_count, 2)
self.assertEqual(item.szContentHead, '[«Привет»]')
self.assertEqual(item.szContentIntro, '[<p>Абзац</p>]')
self.assertEqual(item.szContentBody, '[<p>Тело</p>]')
def test_save_uses_virtual_typograph_options(self):
item = TbContent(
szContentHead='Привет',
szContentIntro='Текст',
szContentBody='Тело',
)
item._typograph_enabled = True
item._typograph_mode = MODE_UNICODE
item._typograph_hyphenation = False
item._typograph_sanitizer = SANITIZE_ETPGRF
with patch('web.models._build_typographer') as build_mock:
fake_typographer = build_mock.return_value
fake_typographer.process.side_effect = lambda text: text
item.save()
self.assertEqual(build_mock.call_count, 2)
self.assertEqual(
build_mock.call_args_list[0].kwargs,
{
'mode': MODE_UNICODE,
'hyphenation': False,
'sanitizer': SANITIZE_ETPGRF,
'hanging_punctuation': 'left',
},
)
self.assertEqual(
build_mock.call_args_list[1].kwargs,
{
'mode': MODE_UNICODE,
'hyphenation': False,
'sanitizer': SANITIZE_ETPGRF,
'hanging_punctuation': False,
},
)
def test_save_strips_soft_hyphens_before_typograph(self):
item = TbContent(
szContentHead='При&shy;вет\u00ad',
szContentIntro='А&#173;нонс',
szContentBody='Те&shy;ло\u00ad',
)
item._typograph_enabled = True
with patch('web.models._build_typographer') as build_mock:
build_mock.return_value.process.side_effect = lambda text: f'[{text}]'
item.save()
self.assertEqual(build_mock.call_count, 2)
self.assertEqual(item.szContentHead, '[Привет]')
self.assertEqual(item.szContentIntro, '[Анонс]')
self.assertEqual(item.szContentBody, '[Тело]')
def test_tbcontent_has_composite_indexes_for_navigation(self):
index_fields = [tuple(index.fields) for index in TbContent._meta.indexes]
self.assertIn(('bContentPublish', 'tdContentPublishUp'), index_fields)
self.assertIn(('bContentPublish', 'tdContentPublishDown'), index_fields)
def test_show_item_increments_hits_without_touching_timestamp(self):
item = TbContent.objects.create(
szContentHead='Проверка просмотра',
szContentIntro='Короткий анонс',
szContentBody='Полный текст',
szContentSlug='real-slug',
bContentPublish=True,
)
timestamp_before = item.dtContentTimeStamp
response = self.client.get(f'/item/{item.id}-wrong-slug')
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<main id="main-content">')
self.assertContains(response, '<article class="col-12 col-md-9" aria-labelledby="article-title">')
self.assertContains(response, '<title>Проверка просмотра | CADpoint</title>')
self.assertContains(
response,
f'<link rel="canonical" href="http://testserver/item/{item.id}-{item.szContentSlug}" />',
)
self.assertNotContains(response, 'wrong-slug')
self.assertContains(response, '<script type="application/ld+json">')
self.assertContains(response, '"@type": "WebSite"')
self.assertContains(response, '"@type": "Article"')
self.assertRegex(
response.content.decode(),
r'"datePublished": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}[+-]\d{2}:\d{2}"',
)
self.assertRegex(
response.content.decode(),
r'"dateModified": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}[+-]\d{2}:\d{2}"',
)
item.refresh_from_db()
self.assertEqual(item.iContentHits, 1)
self.assertEqual(item.dtContentTimeStamp, timestamp_before)
def test_show_item_keywords_prioritize_explicit_seo_values(self):
item = TbContent.objects.create(
szContentHead='SEO ключи',
szContentIntro='Анонс',
szContentBody='Текст',
szContentSlug='seo-klyuchi',
szContentKeywords='ключ1, ключ2',
bContentPublish=True,
)
item.tags.add('alpha', 'beta')
response = self.client.get(f'/item/{item.id}-{item.szContentSlug}')
self.assertEqual(response.status_code, 200)
self.assertContains(
response,
'<meta name="keywords" content="cadpoint, ключ1, ключ2, alpha, beta, новости" />',
)
self.assertNotContains(response, 'None')
def test_show_item_keywords_without_explicit_value_do_not_render_none(self):
item = TbContent.objects.create(
szContentHead='SEO пусто',
szContentIntro='Анонс',
szContentBody='Текст',
szContentSlug='seo-pusto',
szContentKeywords=None,
bContentPublish=True,
)
item.tags.add('alpha')
response = self.client.get(f'/item/{item.id}-{item.szContentSlug}')
self.assertEqual(response.status_code, 200)
self.assertContains(
response,
'<meta name="keywords" content="cadpoint, alpha, новости" />',
)
self.assertNotContains(response, 'None')
# Это тесты карты сайта: проверяем, что в XML попадают только публичные страницы.
class SitemapTests(TestCase):
def setUp(self):
self.published = TbContent.objects.create(
szContentHead='Опубликованная статья',
szContentIntro='Анонс',
szContentBody='Текст',
szContentSlug='opublikovannaya-statya',
bContentPublish=True,
)
self.published.tags.add('alpha')
TbContent.objects.create(
szContentHead='Скрытая статья',
szContentIntro='Анонс',
szContentBody='Текст',
szContentSlug='skrytaya-statya',
bContentPublish=False,
)
def test_sitemap_uses_django_framework_and_lists_public_pages(self):
response = self.client.get(reverse('web_sitemap'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, '<?xml version="1.0" encoding="UTF-8"?>', html=False)
self.assertContains(response, '<loc>http://testserver/</loc>', html=False)
self.assertContains(response, '<loc>http://testserver/alltags</loc>', html=False)
self.assertContains(
response,
f'<loc>http://testserver/item/{self.published.id}-{self.published.szContentSlug}</loc>',
html=False,
)
self.assertContains(response, '<loc>http://testserver/tag_alpha</loc>', html=False)
self.assertNotContains(response, 'skrytaya-statya')
# Для error-handlers удобно использовать `RequestFactory`: мы сами создаём
# request и вызываем функцию-обработчик напрямую, без прохода через URL-роутинг.
class ErrorHandlersTests(SimpleTestCase):
def setUp(self):
self.factory = RequestFactory()
def test_handler400_renders_modern_template(self):
from web.views import handler400
request = self.factory.get('/bad-request/')
response = handler400(request, Exception('bad request'))
self.assertEqual(response.status_code, 400)
self.assertContains(response, 'CADpoint.ru - http 400 error', status_code=400)
self.assertContains(response, '<meta name="robots" content="noindex,nofollow" />', status_code=400)
self.assertContains(response, 'запрос получился некорректным', status_code=400)
self.assertContains(response, 'Вернуться на&nbsp;главную', status_code=400)
def test_handler403_renders_modern_template(self):
from web.views import handler403
request = self.factory.get('/forbidden/')
response = handler403(request, Exception('forbidden'))
self.assertEqual(response.status_code, 403)
self.assertContains(response, 'CADpoint.ru - http 403 error', status_code=403)
self.assertContains(response, '<meta name="robots" content="noindex,nofollow" />', status_code=403)
self.assertContains(response, 'доступ к&nbsp;этой странице ограничен.', status_code=403)
def test_handler404_renders_modern_template(self):
response = self.client.get('/no-such-page/')
self.assertEqual(response.status_code, 404)
self.assertContains(response, 'CADpoint.ru - http 404 error', status_code=404)
self.assertContains(response, '<meta name="robots" content="noindex,nofollow" />', status_code=404)
self.assertContains(response, 'похоже, такой страницы или картинки больше нет.', status_code=404)
def test_handler500_renders_modern_template(self):
from web.views import handler500
request = self.factory.get('/boom/')
response = handler500(request)
self.assertEqual(response.status_code, 500)
self.assertContains(response, 'CADpoint.ru - http 500 error', status_code=500)
self.assertContains(response, '<meta name="robots" content="noindex,nofollow" />', status_code=500)
self.assertContains(response, 'подождите, скоро всё починят…', status_code=500)