diff --git a/dspace/settings_test.py b/dspace/settings_test.py
new file mode 100644
index 0000000..987940d
--- /dev/null
+++ b/dspace/settings_test.py
@@ -0,0 +1,13 @@
+"""Настройки для тестов: боевой режим (DEBUG=False), но без SSL-редиректа и внешних сервисов."""
+import os
+
+os.environ.setdefault('DJANGO_SECRET_KEY', 'test-secret-key-not-for-production')
+os.environ.setdefault('DJANGO_DEBUG', 'False')
+os.environ.setdefault('DJANGO_SECURE_SSL_REDIRECT', 'False')
+os.environ.setdefault('DJANGO_ALLOWED_HOSTS', 'testserver,localhost')
+
+from .settings import * # noqa: E402,F403
+
+STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
+PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
+EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
diff --git a/main/tests.py b/main/tests.py
deleted file mode 100644
index 4929020..0000000
--- a/main/tests.py
+++ /dev/null
@@ -1,2 +0,0 @@
-
-# Create your tests here.
diff --git a/main/tests/__init__.py b/main/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/main/tests/conftest.py b/main/tests/conftest.py
new file mode 100644
index 0000000..2fd3570
--- /dev/null
+++ b/main/tests/conftest.py
@@ -0,0 +1,22 @@
+import pytest
+
+from main.models import Article, SiteSettings
+
+
+@pytest.fixture(autouse=True)
+def no_network(monkeypatch):
+ """Никаких реальных запросов наружу: ping поисковиков и т. п."""
+ calls = []
+ monkeypatch.setattr('main.signals.requests.get', lambda *a, **kw: calls.append((a, kw)))
+ monkeypatch.setattr('main.signals.requests.post', lambda *a, **kw: calls.append((a, kw)))
+ return calls
+
+
+@pytest.fixture
+def article(db):
+ return Article.objects.create(title='Первая статья', post='Текст статьи', excerpt='Кратко')
+
+
+@pytest.fixture
+def site_settings(db):
+ return SiteSettings.load()
diff --git a/main/tests/test_pages.py b/main/tests/test_pages.py
new file mode 100644
index 0000000..659085d
--- /dev/null
+++ b/main/tests/test_pages.py
@@ -0,0 +1,46 @@
+import pytest
+from django.urls import reverse
+
+from main.models import ArticleView
+
+UA = 'Mozilla/5.0 (X11; Linux x86_64) Firefox/131.0'
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize('name', ['index', 'about', 'projects', 'achievements', 'contacts', 'blog'])
+def test_page_opens(client, name):
+ assert client.get(reverse(name), HTTP_USER_AGENT=UA).status_code == 200
+
+
+@pytest.mark.django_db
+def test_sitemap_and_robots(client, article):
+ assert client.get('/sitemap.xml').status_code == 200
+
+
+@pytest.mark.django_db
+def test_article_counts_unique_views(client, article):
+ url = article.get_absolute_url()
+ client.get(url, HTTP_USER_AGENT=UA)
+ client.get(url, HTTP_USER_AGENT=UA)
+ article.refresh_from_db()
+ assert article.views == 2
+ assert ArticleView.objects.filter(article=article).count() == 1
+
+
+@pytest.mark.django_db
+def test_bots_do_not_create_unique_views(client, article):
+ client.get(article.get_absolute_url(), HTTP_USER_AGENT='Googlebot/2.1')
+ assert not ArticleView.objects.exists()
+
+
+@pytest.mark.django_db
+def test_visitor_cookie_is_signed_and_httponly(client):
+ response = client.get(reverse('index'), HTTP_USER_AGENT=UA)
+ cookie = response.cookies['dspace_vid']
+ assert cookie['httponly']
+ assert ':' in cookie.value
+
+
+@pytest.mark.django_db
+def test_unknown_page_returns_404(client):
+ assert client.get('/no-such-page/', HTTP_USER_AGENT=UA).status_code == 404
diff --git a/main/tests/test_security.py b/main/tests/test_security.py
new file mode 100644
index 0000000..9fef6a3
--- /dev/null
+++ b/main/tests/test_security.py
@@ -0,0 +1,73 @@
+from unittest import mock
+
+import pytest
+
+from main.forms import CommentForm, ContactForm
+from main.middleware import client_ip_hash, is_bot
+from main.models import is_public_http_url
+from main.signals import build_telegram_text
+from main.templatetags.custom_filters import render_tech_badge
+
+
+def test_comment_form_strips_scripts_and_keeps_allowed_tags():
+ form = CommentForm(data={'content': 'жирный https://example.com'})
+ assert form.is_valid()
+ content = form.cleaned_data['content']
+ assert '')
+ assert '